experiment with markers

This commit is contained in:
Daniel afx 2020-10-14 07:19:43 +03:00
parent 5bea016878
commit 915bc99603
129 changed files with 33427 additions and 12 deletions

BIN
assets/images/marker.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -36,7 +36,7 @@ version = 0.1
# (list) Application requirements # (list) Application requirements
# comma separated e.g. requirements = sqlite3,kivy # comma separated e.g. requirements = sqlite3,kivy
requirements = python3,requests,certifi,urllib3,chardet,idna,sqlite3,kivy,kivymd,mapview requirements = python3,requests,certifi,urllib3,chardet,idna,sqlite3,kivy,mapview
# (str) Custom source folders for requirements # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes

View file

@ -7,7 +7,7 @@
lon: 24.747571 lon: 24.747571
zoom: 15 zoom: 15
on_zoom: on_zoom:
self.zoom = 15 if self.zoom < 15 else self.zoom self.zoom = 5 if self.zoom < 5 else self.zoom
on_lat: on_lat:
self.start_get_fov_trees() self.start_get_fov_trees()
on_lon: on_lon:

View file

@ -2,8 +2,11 @@ from kivy_garden.mapview import MapView
from kivy.clock import Clock from kivy.clock import Clock
from kivy.app import App from kivy.app import App
from treemarker import TreeMarker
class ForestMapView(MapView): class ForestMapView(MapView):
get_trees_timer = None get_trees_timer = None
tree_names = []
def start_get_fov_trees(self): def start_get_fov_trees(self):
# After one second get the trees in field of view # After one second get the trees in field of view
@ -17,15 +20,31 @@ class ForestMapView(MapView):
def get_fov_trees(self, *args): def get_fov_trees(self, *args):
# Get reference to main app and the db cursor # Get reference to main app and the db cursor
app = App.get_running_app() app = App.get_running_app()
print(self.get_bbox()) # debug gps position # Gebug gps position
#print(self.get_bbox())
min_lat, min_lon, max_lat, max_lon = self.get_bbox() min_lat, min_lon, max_lat, max_lon = self.get_bbox()
sql_statement = "SELECT * FROM locations WHERE x > %s AND x < %s AND y > %s AND y < %s" % (min_lat, max_lat, min_lon, max_lon) #sql_statement = "SELECT * FROM locations" sql_statement = "SELECT * FROM locations WHERE x > %s AND x < %s AND y > %s AND y < %s" % (min_lat, max_lat, min_lon, max_lon) #sql_statement = "SELECT * FROM locations"
app.cursor.execute(sql_statement) app.cursor.execute(sql_statement)
trees = app.cursor.fetchall() trees = app.cursor.fetchall()
print(trees)
for tree in trees: for tree in trees:
self.add_tree(tree) name = tree[0]
print(tree)
print("Tree detected")
if name in self.tree_names:
continue
else:
self.add_tree(tree)
def add_tree(self, tree): def add_tree(self, tree):
pass # Create TreeMarker
name = tree[0]
lat, lon = tree[1], tree[2]
treemarker = TreeMarker(lat=lat, lon=lon, source='assets/images/marker.png')
treemarker.tree_data = treemarker
# Add TreeMarker to the map
self.add_widget(treemarker)
# Keep track of the TreeMarker's name
self.tree_names.append(name)

66
kivymd/__init__.py Normal file
View file

@ -0,0 +1,66 @@
"""
KivyMD
======
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/previous.png
Is a collection of Material Design compliant widgets for use with,
`Kivy cross-platform graphical framework <http://kivy.org/#home>`_
a framework for cross-platform, touch-enabled graphical applications.
The project's goal is to approximate Google's `Material Design spec
<https://material.io/design/introduction>`_ as close as possible without
sacrificing ease of use or application performance.
This library is a fork of the `KivyMD project
<https://gitlab.com/kivymd/KivyMD>`_ the author of which stopped supporting
this project three years ago. We found the strength and brought this project
to a new level. Currently we're in **beta** status, so things are changing
all the time and we cannot promise any kind of API stability.
However it is safe to vendor now and make use of what's currently available.
Join the project! Just fork the project, branch out and submit a pull request
when your patch is ready. If any changes are necessary, we'll guide you
through the steps that need to be done via PR comments or access to your for
may be requested to outright submit them. If you wish to become a project
developer (permission to create branches on the project without forking for
easier collaboration), have at least one PR approved and ask for it.
If you contribute regularly to the project the role may be offered to you
without asking too.
"""
import os
from kivy.logger import Logger
__version__ = "0.104.2.dev0"
"""KivyMD version."""
release = False
try:
from kivymd._version import __hash__, __short_hash__, __date__
except ImportError:
__hash__ = __short_hash__ = __date__ = ""
path = os.path.dirname(__file__)
"""Path to KivyMD package directory."""
fonts_path = os.path.join(path, f"fonts{os.sep}")
"""Path to fonts directory."""
images_path = os.path.join(path, f"images{os.sep}")
"""Path to images directory."""
_log_message = (
"KivyMD:"
+ (" Release" if release else "")
+ f" {__version__}"
+ (f", git-{__short_hash__}" if __short_hash__ else "")
+ (f", {__date__}" if __date__ else "")
+ f' (installed at "{__file__}")'
)
Logger.info(_log_message)
import kivymd.factory_registers # NOQA
import kivymd.font_definitions # NOQA
from kivymd.tools.packaging.pyinstaller import hooks_path # NOQA

90
kivymd/app.py Normal file
View file

@ -0,0 +1,90 @@
"""
Themes/Material App
===================
This module contains :class:`MDApp` class that is inherited from
:class:`~kivy.app.App`. :class:`MDApp` has some properties needed for ``KivyMD``
library (like :attr:`~MDApp.theme_cls`).
You can turn on the monitor displaying the current ``FPS`` value in your application:
.. code-block:: python
KV = '''
Screen:
MDLabel:
text: "Hello, World!"
halign: "center"
'''
from kivy.lang import Builder
from kivymd.app import MDApp
class MainApp(MDApp):
def build(self):
return Builder.load_string(KV)
def on_start(self):
self.fps_monitor_start()
MainApp().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/fps-monitor.png
:width: 350 px
:align: center
"""
__all__ = ("MDApp",)
from kivy.app import App
from kivy.properties import ObjectProperty
from kivymd.theming import ThemeManager
class FpsMonitoring:
"""Adds a monitor to display the current FPS in the toolbar."""
def fps_monitor_start(self):
from kivymd.utils.fpsmonitor import FpsMonitor
from kivy.core.window import Window
monitor = FpsMonitor()
monitor.start()
Window.add_widget(monitor)
class MDApp(App, FpsMonitoring):
theme_cls = ObjectProperty()
"""
Instance of :class:`~ThemeManager` class.
.. Warning:: The :attr:`~theme_cls` attribute is already available
in a class that is inherited from the :class:`~MDApp` class.
The following code will result in an error!
.. code-block:: python
class MainApp(MDApp):
theme_cls = ThemeManager()
theme_cls.primary_palette = "Teal"
.. Note:: Correctly do as shown below!
.. code-block:: python
class MainApp(MDApp):
def build(self):
self.theme_cls.primary_palette = "Teal"
:attr:`theme_cls` is an :class:`~kivy.properties.ObjectProperty`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.theme_cls = ThemeManager()

944
kivymd/color_definitions.py Executable file
View file

@ -0,0 +1,944 @@
"""
Themes/Color Definitions
========================
.. seealso::
`Material Design spec, The color system <https://material.io/design/color/the-color-system.html>`_
Material colors palette to use in :class:`kivymd.theming.ThemeManager`.
:data:`~colors` is a dict-in-dict where the first key is a value from
:data:`~palette` and the second key is a value from :data:`~hue`. Color is a hex
value, a string of 6 characters (0-9, A-F) written in uppercase.
For example, ``colors["Red"]["900"]`` is ``"B71C1C"``.
"""
colors = {
"Red": {
"50": "FFEBEE",
"100": "FFCDD2",
"200": "EF9A9A",
"300": "E57373",
"400": "EF5350",
"500": "F44336",
"600": "E53935",
"700": "D32F2F",
"800": "C62828",
"900": "B71C1C",
"A100": "FF8A80",
"A200": "FF5252",
"A400": "FF1744",
"A700": "D50000",
},
"Pink": {
"50": "FCE4EC",
"100": "F8BBD0",
"200": "F48FB1",
"300": "F06292",
"400": "EC407A",
"500": "E91E63",
"600": "D81B60",
"700": "C2185B",
"800": "AD1457",
"900": "880E4F",
"A100": "FF80AB",
"A200": "FF4081",
"A400": "F50057",
"A700": "C51162",
},
"Purple": {
"50": "F3E5F5",
"100": "E1BEE7",
"200": "CE93D8",
"300": "BA68C8",
"400": "AB47BC",
"500": "9C27B0",
"600": "8E24AA",
"700": "7B1FA2",
"800": "6A1B9A",
"900": "4A148C",
"A100": "EA80FC",
"A200": "E040FB",
"A400": "D500F9FF",
},
"DeepPurple": {
"50": "EDE7F6",
"100": "D1C4E9",
"200": "B39DDB",
"300": "9575CD",
"400": "7E57C2",
"500": "673AB7",
"600": "5E35B1",
"700": "512DA8",
"800": "4527A0",
"900": "311B92",
"A100": "B388FF",
"A200": "7C4DFF",
"A400": "651FFF",
"A700": "6200EA",
},
"Indigo": {
"50": "E8EAF6",
"100": "C5CAE9",
"200": "9FA8DA",
"300": "7986CB",
"400": "5C6BC0",
"500": "3F51B5",
"600": "3949AB",
"700": "303F9F",
"800": "283593",
"900": "1A237E",
"A100": "8C9EFF",
"A200": "536DFE",
"A400": "3D5AFE",
"A700": "304FFE",
},
"Blue": {
"50": "E3F2FD",
"100": "BBDEFB",
"200": "90CAF9",
"300": "64B5F6",
"400": "42A5F5",
"500": "2196F3",
"600": "1E88E5",
"700": "1976D2",
"800": "1565C0",
"900": "0D47A1",
"A100": "82B1FF",
"A200": "448AFF",
"A400": "2979FF",
"A700": "2962FF",
},
"LightBlue": {
"50": "E1F5FE",
"100": "B3E5FC",
"200": "81D4FA",
"300": "4FC3F7",
"400": "29B6F6",
"500": "03A9F4",
"600": "039BE5",
"700": "0288D1",
"800": "0277BD",
"900": "01579B",
"A100": "80D8FF",
"A200": "40C4FF",
"A400": "00B0FF",
"A700": "0091EA",
},
"Cyan": {
"50": "E0F7FA",
"100": "B2EBF2",
"200": "80DEEA",
"300": "4DD0E1",
"400": "26C6DA",
"500": "00BCD4",
"600": "00ACC1",
"700": "0097A7",
"800": "00838F",
"900": "006064",
"A100": "84FFFF",
"A200": "18FFFF",
"A400": "00E5FF",
"A700": "00B8D4",
},
"Teal": {
"50": "E0F2F1",
"100": "B2DFDB",
"200": "80CBC4",
"300": "4DB6AC",
"400": "26A69A",
"500": "009688",
"600": "00897B",
"700": "00796B",
"800": "00695C",
"900": "004D40",
"A100": "A7FFEB",
"A200": "64FFDA",
"A400": "1DE9B6",
"A700": "00BFA5",
},
"Green": {
"50": "E8F5E9",
"100": "C8E6C9",
"200": "A5D6A7",
"300": "81C784",
"400": "66BB6A",
"500": "4CAF50",
"600": "43A047",
"700": "388E3C",
"800": "2E7D32",
"900": "1B5E20",
"A100": "B9F6CA",
"A200": "69F0AE",
"A400": "00E676",
"A700": "00C853",
},
"LightGreen": {
"50": "F1F8E9",
"100": "DCEDC8",
"200": "C5E1A5",
"300": "AED581",
"400": "9CCC65",
"500": "8BC34A",
"600": "7CB342",
"700": "689F38",
"800": "558B2F",
"900": "33691E",
"A100": "CCFF90",
"A200": "B2FF59",
"A400": "76FF03",
"A700": "64DD17",
},
"Lime": {
"50": "F9FBE7",
"100": "F0F4C3",
"200": "E6EE9C",
"300": "DCE775",
"400": "D4E157",
"500": "CDDC39",
"600": "C0CA33",
"700": "AFB42B",
"800": "9E9D24",
"900": "827717",
"A100": "F4FF81",
"A200": "EEFF41",
"A400": "C6FF00",
"A700": "AEEA00",
},
"Yellow": {
"50": "FFFDE7",
"100": "FFF9C4",
"200": "FFF59D",
"300": "FFF176",
"400": "FFEE58",
"500": "FFEB3B",
"600": "FDD835",
"700": "FBC02D",
"800": "F9A825",
"900": "F57F17",
"A100": "FFFF8D",
"A200": "FFFF00",
"A400": "FFEA00",
"A700": "FFD600",
},
"Amber": {
"50": "FFF8E1",
"100": "FFECB3",
"200": "FFE082",
"300": "FFD54F",
"400": "FFCA28",
"500": "FFC107",
"600": "FFB300",
"700": "FFA000",
"800": "FF8F00",
"900": "FF6F00",
"A100": "FFE57F",
"A200": "FFD740",
"A400": "FFC400",
"A700": "FFAB00",
},
"Orange": {
"50": "FFF3E0",
"100": "FFE0B2",
"200": "FFCC80",
"300": "FFB74D",
"400": "FFA726",
"500": "FF9800",
"600": "FB8C00",
"700": "F57C00",
"800": "EF6C00",
"900": "E65100",
"A100": "FFD180",
"A200": "FFAB40",
"A400": "FF9100",
"A700": "FF6D00",
},
"DeepOrange": {
"50": "FBE9E7",
"100": "FFCCBC",
"200": "FFAB91",
"300": "FF8A65",
"400": "FF7043",
"500": "FF5722",
"600": "F4511E",
"700": "E64A19",
"800": "D84315",
"900": "BF360C",
"A100": "FF9E80",
"A200": "FF6E40",
"A400": "FF3D00",
"A700": "DD2C00",
},
"Brown": {
"50": "EFEBE9",
"100": "D7CCC8",
"200": "BCAAA4",
"300": "A1887F",
"400": "8D6E63",
"500": "795548",
"600": "6D4C41",
"700": "5D4037",
"800": "4E342E",
"900": "3E2723",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"Gray": {
"50": "FAFAFA",
"100": "F5F5F5",
"200": "EEEEEE",
"300": "E0E0E0",
"400": "BDBDBD",
"500": "9E9E9E",
"600": "757575",
"700": "616161",
"800": "424242",
"900": "212121",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"BlueGray": {
"50": "ECEFF1",
"100": "CFD8DC",
"200": "B0BEC5",
"300": "90A4AE",
"400": "78909C",
"500": "607D8B",
"600": "546E7A",
"700": "455A64",
"800": "37474F",
"900": "263238",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"Light": {
"StatusBar": "E0E0E0",
"AppBar": "F5F5F5",
"Background": "FAFAFA",
"CardsDialogs": "FFFFFF",
"FlatButtonDown": "cccccc",
},
"Dark": {
"StatusBar": "000000",
"AppBar": "1f1f1f",
"Background": "121212",
"CardsDialogs": "212121",
"FlatButtonDown": "999999",
},
}
"""Color palette. Taken from `2014 Material Design color palettes
<https://material.io/design/color/the-color-system.html>`_.
To demonstrate the shades of the palette, you can run the following code:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.utils import get_color_from_hex
from kivy.properties import ListProperty, StringProperty
from kivymd.color_definitions import colors
from kivymd.uix.tab import MDTabsBase
demo = '''
<Root@BoxLayout>
orientation: 'vertical'
MDToolbar:
title: app.title
MDTabs:
id: android_tabs
on_tab_switch: app.on_tab_switch(*args)
size_hint_y: None
height: "48dp"
tab_indicator_anim: False
ScrollView:
MDList:
id: box
<ItemColor>:
size_hint_y: None
height: "42dp"
canvas:
Color:
rgba: root.color
Rectangle:
size: self.size
pos: self.pos
MDLabel:
text: root.text
halign: "center"
<Tab>:
'''
from kivy.factory import Factory
from kivymd.app import MDApp
class Tab(BoxLayout, MDTabsBase):
pass
class ItemColor(BoxLayout):
text = StringProperty()
color = ListProperty()
class Palette(MDApp):
title = "Colors definitions"
def build(self):
Builder.load_string(demo)
self.screen = Factory.Root()
for name_tab in colors.keys():
tab = Tab(text=name_tab)
self.screen.ids.android_tabs.add_widget(tab)
return self.screen
def on_tab_switch(self, instance_tabs, instance_tab, instance_tabs_label, tab_text):
self.screen.ids.box.clear_widgets()
for value_color in colors[tab_text]:
self.screen.ids.box.add_widget(
ItemColor(
color=get_color_from_hex(colors[tab_text][value_color]),
text=value_color,
)
)
def on_start(self):
self.on_tab_switch(
None,
None,
None,
self.screen.ids.android_tabs.ids.layout.children[-1].text,
)
Palette().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/palette.gif
:align: center
"""
palette = [
"Red",
"Pink",
"Purple",
"DeepPurple",
"Indigo",
"Blue",
"LightBlue",
"Cyan",
"Teal",
"Green",
"LightGreen",
"Lime",
"Yellow",
"Amber",
"Orange",
"DeepOrange",
"Brown",
"Gray",
"BlueGray",
]
"""Valid values for color palette selecting."""
hue = [
"50",
"100",
"200",
"300",
"400",
"500",
"600",
"700",
"800",
"900",
"A100",
"A200",
"A400",
"A700",
]
"""Valid values for color hue selecting."""
light_colors = {
"Red": ["50", "100", "200", "300", "A100"],
"Pink": ["50", "100", "200", "A100"],
"Purple": ["50", "100", "200", "A100"],
"DeepPurple": ["50", "100", "200", "A100"],
"Indigo": ["50", "100", "200", "A100"],
"Blue": ["50", "100", "200", "300", "400", "A100"],
"LightBlue": [
"50",
"100",
"200",
"300",
"400",
"500",
"A100",
"A200",
"A400",
],
"Cyan": [
"50",
"100",
"200",
"300",
"400",
"500",
"600",
"A100",
"A200",
"A400",
"A700",
],
"Teal": ["50", "100", "200", "300", "400", "A100", "A200", "A400", "A700"],
"Green": [
"50",
"100",
"200",
"300",
"400",
"500",
"A100",
"A200",
"A400",
"A700",
],
"LightGreen": [
"50",
"100",
"200",
"300",
"400",
"500",
"600",
"A100",
"A200",
"A400",
"A700",
],
"Lime": [
"50",
"100",
"200",
"300",
"400",
"500",
"600",
"700",
"800",
"A100",
"A200",
"A400",
"A700",
],
"Yellow": [
"50",
"100",
"200",
"300",
"400",
"500",
"600",
"700",
"800",
"900",
"A100",
"A200",
"A400",
"A700",
],
"Amber": [
"50",
"100",
"200",
"300",
"400",
"500",
"600",
"700",
"800",
"900",
"A100",
"A200",
"A400",
"A700",
],
"Orange": [
"50",
"100",
"200",
"300",
"400",
"500",
"600",
"700",
"A100",
"A200",
"A400",
"A700",
],
"DeepOrange": ["50", "100", "200", "300", "400", "A100", "A200"],
"Brown": ["50", "100", "200"],
"Gray": ["51", "100", "200", "300", "400", "500"],
"BlueGray": ["50", "100", "200", "300"],
"Dark": [],
"Light": ["White", "MainBackground", "DialogBackground"],
}
"""Which colors are light. Other are dark."""
text_colors = {
"Red": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "FFFFFF",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "FFFFFF",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
"Pink": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "FFFFFF",
"400": "FFFFFF",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "FFFFFF",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
"Purple": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "FFFFFF",
"400": "FFFFFF",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "FFFFFF",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
"DeepPurple": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "FFFFFF",
"400": "FFFFFF",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "FFFFFF",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
"Indigo": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "FFFFFF",
"400": "FFFFFF",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "FFFFFF",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
"Blue": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "FFFFFF",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
"LightBlue": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "000000",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "FFFFFF",
},
"Cyan": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "000000",
"600": "000000",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"Teal": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"Green": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "000000",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"LightGreen": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "000000",
"600": "000000",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"Lime": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "000000",
"600": "000000",
"700": "000000",
"800": "000000",
"900": "FFFFFF",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"Yellow": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "000000",
"600": "000000",
"700": "000000",
"800": "000000",
"900": "000000",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"Amber": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "000000",
"600": "000000",
"700": "000000",
"800": "000000",
"900": "000000",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"Orange": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "000000",
"600": "000000",
"700": "000000",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "000000",
"A400": "000000",
"A700": "000000",
},
"DeepOrange": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "000000",
"A200": "000000",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
"Brown": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "FFFFFF",
"400": "FFFFFF",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "FFFFFF",
"A200": "FFFFFF",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
"Gray": {
"50": "FFFFFF",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "000000",
"500": "000000",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "FFFFFF",
"A200": "FFFFFF",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
"BlueGray": {
"50": "000000",
"100": "000000",
"200": "000000",
"300": "000000",
"400": "FFFFFF",
"500": "FFFFFF",
"600": "FFFFFF",
"700": "FFFFFF",
"800": "FFFFFF",
"900": "FFFFFF",
"A100": "FFFFFF",
"A200": "FFFFFF",
"A400": "FFFFFF",
"A700": "FFFFFF",
},
}
"""Text colors generated from :data:`~light_colors`. "000000" for light and
"FFFFFF" for dark.
How to generate text_colors dict
.. code-block:: python
text_colors = {}
for p in palette:
text_colors[p] = {}
for h in hue:
if h in light_colors[p]:
text_colors[p][h] = "000000"
else:
text_colors[p][h] = "FFFFFF"
"""
theme_colors = [
"Primary",
"Secondary",
"Background",
"Surface",
"Error",
"On_Primary",
"On_Secondary",
"On_Background",
"On_Surface",
"On_Error",
]
"""Valid theme colors."""

113
kivymd/factory_registers.py Normal file
View file

@ -0,0 +1,113 @@
"""
Register KivyMD widgets to use without import
"""
from kivy.factory import Factory
r = Factory.register
r("MDCarousel", module="kivymd.uix.carousel")
r("MDFloatLayout", module="kivymd.uix.floatlayout")
r("MDScreen", module="kivymd.uix.screen")
r("MDBoxLayout", module="kivymd.uix.boxlayout")
r("MDRelativeLayout", module="kivymd.uix.relativelayout")
r("MDGridLayout", module="kivymd.uix.gridlayout")
r("MDStackLayout", module="kivymd.uix.stacklayout")
r("MDExpansionPanel", module="kivymd.uix.expansionpanel")
r("MDExpansionPanelOneLine", module="kivymd.uix.expansionpanel")
r("MDExpansionPanelTwoLine", module="kivymd.uix.expansionpanel")
r("MDExpansionPanelThreeLine", module="kivymd.uix.expansionpanel")
r("FitImage", module="kivymd.utils.fitimage")
r("MDBackdrop", module="kivymd.uix.backdrop")
r("MDBanner", module="kivymd.uix.banner")
r("MDTooltip", module="kivymd.uix.tooltip")
r("MDBottomNavigation", module="kivymd.uix.bottomnavigation")
r("MDBottomNavigationItem", module="kivymd.uix.bottomnavigation")
r("MDBottomNavigationHeader", module="kivymd.uix.bottomnavigation")
r("MDBottomNavigationBar", module="kivymd.uix.bottomnavigation")
r("MDTab", module="kivymd.uix.bottomnavigation")
r("MDBottomSheet", module="kivymd.uix.bottomsheet")
r("MDListBottomSheet", module="kivymd.uix.bottomsheet")
r("MDGridBottomSheet", module="kivymd.uix.bottomsheet")
r("MDFloatingActionButtonSpeedDial", module="kivymd.uix.button")
r("MDIconButton", module="kivymd.uix.button")
r("MDRoundImageButton", module="kivymd.uix.button")
r("MDFlatButton", module="kivymd.uix.button")
r("MDRaisedButton", module="kivymd.uix.button")
r("MDFloatingActionButton", module="kivymd.uix.button")
r("MDRectangleFlatButton", module="kivymd.uix.button")
r("MDTextButton", module="kivymd.uix.button")
r("MDCustomRoundIconButton", module="kivymd.uix.button")
r("MDRoundFlatButton", module="kivymd.uix.button")
r("MDFillRoundFlatButton", module="kivymd.uix.button")
r("MDRectangleFlatIconButton", module="kivymd.uix.button")
r("MDRoundFlatIconButton", module="kivymd.uix.button")
r("MDFillRoundFlatIconButton", module="kivymd.uix.button")
r("MDCard", module="kivymd.uix.card")
r("MDSeparator", module="kivymd.uix.card")
r("MDChip", module="kivymd.uix.chip")
r("MDChooseChip", module="kivymd.uix.chip")
r("MDDialog", module="kivymd.uix.dialog")
r("MDInputDialog", module="kivymd.uix.dialog")
r("MDFileManager", module="kivymd.uix.filemanager")
r("Tile", module="kivymd.uix.imagelist")
r("SmartTile", module="kivymd.uix.imagelist")
r("SmartTileWithLabel", module="kivymd.uix.imagelist")
r("SmartTileWithStar", module="kivymd.uix.imagelist")
r("MDLabel", module="kivymd.uix.label")
r("MDIcon", module="kivymd.uix.label")
r("MDList", module="kivymd.uix.list")
r("ILeftBody", module="kivymd.uix.list")
r("ILeftBodyTouch", module="kivymd.uix.list")
r("IRightBody", module="kivymd.uix.list")
r("IRightBodyTouch", module="kivymd.uix.list")
r("ContainerSupport", module="kivymd.uix.list")
r("OneLineListItem", module="kivymd.uix.list")
r("TwoLineListItem", module="kivymd.uix.list")
r("ThreeLineListItem", module="kivymd.uix.list")
r("OneLineAvatarListItem", module="kivymd.uix.list")
r("TwoLineAvatarListItem", module="kivymd.uix.list")
r("ThreeLineAvatarListItem", module="kivymd.uix.list")
r("OneLineIconListItem", module="kivymd.uix.list")
r("TwoLineIconListItem", module="kivymd.uix.list")
r("ThreeLineIconListItem", module="kivymd.uix.list")
r("OneLineRightIconListItem", module="kivymd.uix.list")
r("TwoLineRightIconListItem", module="kivymd.uix.list")
r("ThreeLineRightIconListItem", module="kivymd.uix.list")
r("OneLineAvatarIconListItem", module="kivymd.uix.list")
r("TwoLineAvatarIconListItem", module="kivymd.uix.list")
r("ThreeLineAvatarIconListItem", module="kivymd.uix.list")
r("MDMenu", module="kivymd.uix.menu")
r("MDDropdownMenu", module="kivymd.uix.menu")
r("MDContextMenu", module="kivymd.uix.context_menu")
r("MDMenuItem", module="kivymd.uix.menu")
r("HoverBehavior", module="kivymd.uix.behaviors.hover_behavior")
r("FocusBehavior", module="kivymd.uix.behaviors.hover_behavior")
r("MDNavigationDrawer", module="kivymd.uix.navigationdrawer")
r("NavigationLayout", module="kivymd.uix.navigationdrawer")
r("MDDatePicker", module="kivymd.uix.picker")
r("MDTimePicker", module="kivymd.uix.picker")
r("MDThemePicker", module="kivymd.uix.picker")
r("MDProgressBar", module="kivymd.uix.progressbar")
r("MDProgressLoader", module="kivymd.uix.progressloader")
r("MDScrollViewRefreshLayout", module="kivymd.uix.refreshlayout")
r("MDCheckbox", module="kivymd.uix.selectioncontrol")
r("Thumb", module="kivymd.uix.selectioncontrol")
r("MDSwitch", module="kivymd.uix.selectioncontrol")
r("MDSlider", module="kivymd.uix.slider")
r("Snackbar", module="kivymd.uix.snackbar")
r("MDSpinner", module="kivymd.uix.spinner")
r("MDFloatingLabel", module="kivymd.uix.tab")
r("MDTabsLabel", module="kivymd.uix.tab")
r("MDTabsBase", module="kivymd.uix.tab")
r("MDTabsMain", module="kivymd.uix.tab")
r("MDTabsCarousel", module="kivymd.uix.tab")
r("MDTabsScrollView", module="kivymd.uix.tab")
r("MDTabsBar", module="kivymd.uix.tab")
r("MDTabs", module="kivymd.uix.tab")
r("MDTextField", module="kivymd.uix.textfield")
r("MDTextField", module="kivymd.uix.textfield")
r("MDTextFieldRound", module="kivymd.uix.textfield")
r("MDTextFieldRect", module="kivymd.uix.textfield")
r("MDToolbar", module="kivymd.uix.toolbar")
r("MDBottomAppBar", module="kivymd.uix.toolbar")
r("MDDropDownItem", module="kivymd.uix.dropdownitem")

69
kivymd/font_definitions.py Executable file
View file

@ -0,0 +1,69 @@
"""
Themes/Font Definitions
=======================
.. seealso::
`Material Design spec, The type system <https://material.io/design/typography/the-type-system.html>`_
"""
from kivy.core.text import LabelBase
from kivymd import fonts_path
fonts = [
{
"name": "Roboto",
"fn_regular": fonts_path + "Roboto-Regular.ttf",
"fn_bold": fonts_path + "Roboto-Bold.ttf",
"fn_italic": fonts_path + "Roboto-Italic.ttf",
"fn_bolditalic": fonts_path + "Roboto-BoldItalic.ttf",
},
{
"name": "RobotoThin",
"fn_regular": fonts_path + "Roboto-Thin.ttf",
"fn_italic": fonts_path + "Roboto-ThinItalic.ttf",
},
{
"name": "RobotoLight",
"fn_regular": fonts_path + "Roboto-Light.ttf",
"fn_italic": fonts_path + "Roboto-LightItalic.ttf",
},
{
"name": "RobotoMedium",
"fn_regular": fonts_path + "Roboto-Medium.ttf",
"fn_italic": fonts_path + "Roboto-MediumItalic.ttf",
},
{
"name": "RobotoBlack",
"fn_regular": fonts_path + "Roboto-Black.ttf",
"fn_italic": fonts_path + "Roboto-BlackItalic.ttf",
},
{
"name": "Icons",
"fn_regular": fonts_path + "materialdesignicons-webfont.ttf",
},
]
for font in fonts:
LabelBase.register(**font)
theme_font_styles = [
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"Subtitle1",
"Subtitle2",
"Body1",
"Body2",
"Button",
"Caption",
"Overline",
"Icon",
]
"""
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-styles-2.png
"""

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

5782
kivymd/icon_definitions.py Executable file

File diff suppressed because it is too large Load diff

BIN
kivymd/images/folder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1 @@
{"quad_shadow-1.png": {"20": [2, 136, 128, 128], "21": [132, 136, 128, 128], "22": [262, 136, 128, 128], "23": [2, 6, 128, 128], "19": [132, 266, 128, 128], "18": [2, 266, 128, 128], "1": [262, 266, 128, 128], "3": [262, 6, 128, 128], "2": [132, 6, 128, 128]}, "quad_shadow-0.png": {"11": [262, 266, 128, 128], "10": [132, 266, 128, 128], "13": [132, 136, 128, 128], "12": [2, 136, 128, 128], "15": [2, 6, 128, 128], "14": [262, 136, 128, 128], "17": [262, 6, 128, 128], "16": [132, 6, 128, 128], "0": [2, 266, 128, 128]}, "quad_shadow-2.png": {"5": [132, 266, 128, 128], "4": [2, 266, 128, 128], "7": [2, 136, 128, 128], "6": [262, 266, 128, 128], "9": [262, 136, 128, 128], "8": [132, 136, 128, 128]}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -0,0 +1 @@
{"rec_shadow-1.png": {"20": [2, 266, 256, 128], "21": [260, 266, 256, 128], "22": [518, 266, 256, 128], "23": [776, 266, 256, 128], "3": [260, 136, 256, 128], "2": [2, 136, 256, 128], "5": [776, 136, 256, 128], "4": [518, 136, 256, 128], "7": [260, 6, 256, 128], "6": [2, 6, 256, 128], "9": [776, 6, 256, 128], "8": [518, 6, 256, 128]}, "rec_shadow-0.png": {"11": [518, 266, 256, 128], "10": [260, 266, 256, 128], "13": [2, 136, 256, 128], "12": [776, 266, 256, 128], "15": [518, 136, 256, 128], "14": [260, 136, 256, 128], "17": [2, 6, 256, 128], "16": [776, 136, 256, 128], "19": [518, 6, 256, 128], "18": [260, 6, 256, 128], "1": [776, 6, 256, 128], "0": [2, 266, 256, 128]}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View file

@ -0,0 +1 @@
{"rec_st_shadow-0.png": {"11": [262, 138, 128, 256], "10": [132, 138, 128, 256], "13": [522, 138, 128, 256], "12": [392, 138, 128, 256], "15": [782, 138, 128, 256], "14": [652, 138, 128, 256], "16": [912, 138, 128, 256], "0": [2, 138, 128, 256]}, "rec_st_shadow-1.png": {"20": [522, 138, 128, 256], "21": [652, 138, 128, 256], "17": [2, 138, 128, 256], "23": [912, 138, 128, 256], "19": [262, 138, 128, 256], "18": [132, 138, 128, 256], "22": [782, 138, 128, 256], "1": [392, 138, 128, 256]}, "rec_st_shadow-2.png": {"3": [132, 138, 128, 256], "2": [2, 138, 128, 256], "5": [392, 138, 128, 256], "4": [262, 138, 128, 256], "7": [652, 138, 128, 256], "6": [522, 138, 128, 256], "9": [912, 138, 128, 256], "8": [782, 138, 128, 256]}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1 @@
{"round_shadow-1.png": {"20": [2, 136, 128, 128], "21": [132, 136, 128, 128], "22": [262, 136, 128, 128], "23": [2, 6, 128, 128], "19": [132, 266, 128, 128], "18": [2, 266, 128, 128], "1": [262, 266, 128, 128], "3": [262, 6, 128, 128], "2": [132, 6, 128, 128]}, "round_shadow-0.png": {"11": [262, 266, 128, 128], "10": [132, 266, 128, 128], "13": [132, 136, 128, 128], "12": [2, 136, 128, 128], "15": [2, 6, 128, 128], "14": [262, 136, 128, 128], "17": [262, 6, 128, 128], "16": [132, 6, 128, 128], "0": [2, 266, 128, 128]}, "round_shadow-2.png": {"5": [132, 266, 128, 128], "4": [2, 266, 128, 128], "7": [2, 136, 128, 128], "6": [262, 266, 128, 128], "9": [262, 136, 128, 128], "8": [132, 136, 128, 128]}}

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

38
kivymd/material_resources.py Executable file
View file

@ -0,0 +1,38 @@
"""
Material Resources
==================
"""
import os
from kivy.core.window import Window
from kivy.metrics import dp
from kivy.utils import platform
if "KIVY_DOC_INCLUDE" in os.environ:
dp = lambda x: x # NOQA: F811
# Feel free to override this const if you're designing for a device such as
# a GNU/Linux tablet.
DEVICE_IOS = platform == "ios" or platform == "macosx"
if platform != "android" and platform != "ios":
DEVICE_TYPE = "desktop"
elif Window.width >= dp(600) and Window.height >= dp(600):
DEVICE_TYPE = "tablet"
else:
DEVICE_TYPE = "mobile"
if DEVICE_TYPE == "mobile":
MAX_NAV_DRAWER_WIDTH = dp(300)
HORIZ_MARGINS = dp(16)
STANDARD_INCREMENT = dp(56)
PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT
LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT - dp(8)
else:
MAX_NAV_DRAWER_WIDTH = dp(400)
HORIZ_MARGINS = dp(24)
STANDARD_INCREMENT = dp(64)
PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT
LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT
TOUCH_TARGET_HEIGHT = dp(48)

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2014 LogicalDash
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,24 @@
stiffscroll
===========
A ScrollEffect for use with a Kivy ScrollView. It makes scrolling more
laborious as you reach the edge of the scrollable area.
A ScrollView constructed with StiffScrollEffect,
eg. ScrollView(effect_cls=StiffScrollEffect), will get harder to
scroll as you get nearer to its edges. You can scroll all the way to
the edge if you want to, but it will take more finger-movement than
usual.
Unlike DampedScrollEffect, it is impossible to overscroll with
StiffScrollEffect. That means you cannot push the contents of the
ScrollView far enough to see what's beneath them. This is appropriate
if the ScrollView contains, eg., a background image, like a desktop
wallpaper. Overscrolling may give the impression that there is some
reason to overscroll, even if just to take a peek beneath, and that
impression may be misleading.
StiffScrollEffect was written by Zachary Spector. His other stuff is at:
https://github.com/LogicalDash/
He can be reached, and possibly hired, at:
zacharyspector@gmail.com

View file

@ -0,0 +1,215 @@
"""
Stiff Scroll Effect
===================
An Effect to be used with ScrollView to prevent scrolling beyond
the bounds, but politely.
A ScrollView constructed with StiffScrollEffect,
eg. ScrollView(effect_cls=StiffScrollEffect), will get harder to
scroll as you get nearer to its edges. You can scroll all the way to
the edge if you want to, but it will take more finger-movement than
usual.
Unlike DampedScrollEffect, it is impossible to overscroll with
StiffScrollEffect. That means you cannot push the contents of the
ScrollView far enough to see what's beneath them. This is appropriate
if the ScrollView contains, eg., a background image, like a desktop
wallpaper. Overscrolling may give the impression that there is some
reason to overscroll, even if just to take a peek beneath, and that
impression may be misleading.
StiffScrollEffect was written by Zachary Spector. His other stuff is at:
https://github.com/LogicalDash/
He can be reached, and possibly hired, at:
zacharyspector@gmail.com
"""
from time import time
from kivy.animation import AnimationTransition
from kivy.effects.kinetic import KineticEffect
from kivy.properties import NumericProperty, ObjectProperty
from kivy.uix.widget import Widget
class StiffScrollEffect(KineticEffect):
drag_threshold = NumericProperty("20sp")
"""Minimum distance to travel before the movement is considered as a
drag.
:attr:`drag_threshold` is an :class:`~kivy.properties.NumericProperty`
and defaults to `'20sp'`.
"""
min = NumericProperty(0)
"""Minimum boundary to stop the scrolling at.
:attr:`min` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
"""
max = NumericProperty(0)
"""Maximum boundary to stop the scrolling at.
:attr:`max` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
"""
max_friction = NumericProperty(1)
"""How hard should it be to scroll, at the worst?
:attr:`max_friction` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
body = NumericProperty(0.7)
"""Proportion of the range in which you can scroll unimpeded.
:attr:`body` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.7`.
"""
scroll = NumericProperty(0.0)
"""Computed value for scrolling
:attr:`scroll` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.0`.
"""
transition_min = ObjectProperty(AnimationTransition.in_cubic)
"""The AnimationTransition function to use when adjusting the friction
near the minimum end of the effect.
:attr:`transition_min` is an :class:`~kivy.properties.ObjectProperty`
and defaults to :class:`kivy.animation.AnimationTransition`.
"""
transition_max = ObjectProperty(AnimationTransition.in_cubic)
"""The AnimationTransition function to use when adjusting the friction
near the maximum end of the effect.
:attr:`transition_max` is an :class:`~kivy.properties.ObjectProperty`
and defaults to :class:`kivy.animation.AnimationTransition`.
"""
target_widget = ObjectProperty(None, allownone=True, baseclass=Widget)
"""The widget to apply the effect to.
:attr:`target_widget` is an :class:`~kivy.properties.ObjectProperty`
and defaults to ``None``.
"""
displacement = NumericProperty(0)
"""The absolute distance moved in either direction.
:attr:`displacement` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0`.
"""
def __init__(self, **kwargs):
"""Set ``self.base_friction`` to the value of ``self.friction`` just
after instantiation, so that I can reset to that value later.
"""
super().__init__(**kwargs)
self.base_friction = self.friction
def update_velocity(self, dt):
"""Before actually updating my velocity, meddle with ``self.friction``
to make it appropriate to where I'm at, currently.
"""
hard_min = self.min
hard_max = self.max
if hard_min > hard_max:
hard_min, hard_max = hard_max, hard_min
margin = (1.0 - self.body) * (hard_max - hard_min)
soft_min = hard_min + margin
soft_max = hard_max - margin
if self.value < soft_min:
try:
prop = (soft_min - self.value) / (soft_min - hard_min)
self.friction = self.base_friction + abs(
self.max_friction - self.base_friction
) * self.transition_min(prop)
except ZeroDivisionError:
pass
elif self.value > soft_max:
try:
# normalize how far past soft_max I've gone as a
# proportion of the distance between soft_max and hard_max
prop = (self.value - soft_max) / (hard_max - soft_max)
self.friction = self.base_friction + abs(
self.max_friction - self.base_friction
) * self.transition_min(prop)
except ZeroDivisionError:
pass
else:
self.friction = self.base_friction
return super().update_velocity(dt)
def on_value(self, *args):
"""Prevent moving beyond my bounds, and update ``self.scroll``"""
if self.value > self.min:
self.velocity = 0
self.scroll = self.min
elif self.value < self.max:
self.velocity = 0
self.scroll = self.max
else:
self.scroll = self.value
def start(self, val, t=None):
"""Start movement with ``self.friction`` = ``self.base_friction``"""
self.is_manual = True
t = t or time()
self.velocity = self.displacement = 0
self.friction = self.base_friction
self.history = [(t, val)]
def update(self, val, t=None):
"""Reduce the impact of whatever change has been made to me, in
proportion with my current friction.
"""
t = t or time()
hard_min = self.min
hard_max = self.max
if hard_min > hard_max:
hard_min, hard_max = hard_max, hard_min
gamut = hard_max - hard_min
margin = (1.0 - self.body) * gamut
soft_min = hard_min + margin
soft_max = hard_max - margin
distance = val - self.history[-1][1]
reach = distance + self.value
if (distance < 0 and reach < soft_min) or (
distance > 0 and soft_max < reach
):
distance -= distance * self.friction
self.apply_distance(distance)
self.history.append((t, val))
if len(self.history) > self.max_history:
self.history.pop(0)
self.displacement += abs(distance)
self.trigger_velocity_update()
def stop(self, val, t=None):
"""Work out whether I've been flung."""
self.is_manual = False
self.displacement += abs(val - self.history[-1][1])
if self.displacement <= self.drag_threshold:
self.velocity = 0
return super().stop(val, t)

21
kivymd/tests/test_app.py Normal file
View file

@ -0,0 +1,21 @@
from kivy import lang
from kivy.clock import Clock
from kivy.tests.common import GraphicUnitTest
from kivymd.app import MDApp
from kivymd.theming import ThemeManager
class AppTest(GraphicUnitTest):
def test_start_raw_app(self):
lang._delayed_start = None
a = MDApp()
Clock.schedule_once(a.stop, 0.1)
a.run()
def test_theme_manager_existance(self):
lang._delayed_start = None
a = MDApp()
Clock.schedule_once(a.stop, 0.1)
a.run()
assert isinstance(a.theme_cls, ThemeManager)

View file

@ -0,0 +1,15 @@
def test_fonts_registration():
# This should register fonts:
import kivymd # NOQA
from kivy.core.text import LabelBase
fonts = [
"Roboto",
"RobotoThin",
"RobotoLight",
"RobotoMedium",
"RobotoBlack",
"Icons",
]
for font in fonts:
assert font in LabelBase._fonts.keys()

View file

@ -0,0 +1,9 @@
def test_icons_have_size():
from kivymd.icon_definitions import md_icons
from kivy.core.text import Label
lbl = Label(font_name="Icons")
for icon_name, icon_value in md_icons.items():
assert len(icon_value) == 1
lbl.refresh()
assert lbl.get_extents(icon_value) is not None

908
kivymd/theming.py Executable file
View file

@ -0,0 +1,908 @@
"""
Themes/Theming
==============
.. seealso::
`Material Design spec, Material theming <https://material.io/design/material-theming>`_
Material App
------------
The main class of your application, which in `Kivy` inherits from the App class,
in `KivyMD` must inherit from the `MDApp` class. The `MDApp` class has
properties that allow you to control application properties
such as :attr:`color/style/font` of interface elements and much more.
Control material properties
---------------------------
The main application class inherited from the `MDApp` class has the :attr:`theme_cls`
attribute, with which you control the material properties of your application.
"""
from kivy.app import App
from kivy.atlas import Atlas
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.event import EventDispatcher
from kivy.metrics import dp
from kivy.properties import (
AliasProperty,
BooleanProperty,
DictProperty,
ListProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.utils import get_color_from_hex
from kivymd import images_path
from kivymd.color_definitions import colors, hue, palette
from kivymd.material_resources import DEVICE_IOS, DEVICE_TYPE
from kivymd.font_definitions import theme_font_styles # NOQA: F401
class ThemeManager(EventDispatcher):
primary_palette = OptionProperty("Blue", options=palette)
"""
The name of the color scheme that the application will use.
All major `material` components will have the color
of the specified color theme.
Available options are: `'Red'`, `'Pink'`, `'Purple'`, `'DeepPurple'`,
`'Indigo'`, `'Blue'`, `'LightBlue'`, `'Cyan'`, `'Teal'`, `'Green'`,
`'LightGreen'`, `'Lime'`, `'Yellow'`, `'Amber'`, `'Orange'`, `'DeepOrange'`,
`'Brown'`, `'Gray'`, `'BlueGray'`.
To change the color scheme of an application:
.. code-block:: python
from kivy.uix.screenmanager import Screen
from kivymd.app import MDApp
from kivymd.uix.button import MDRectangleFlatButton
class MainApp(MDApp):
def build(self):
self.theme_cls.primary_palette = "Green" # "Purple", "Red"
screen = Screen()
screen.add_widget(
MDRectangleFlatButton(
text="Hello, World",
pos_hint={"center_x": 0.5, "center_y": 0.5},
)
)
return screen
MainApp().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-palette.png
:attr:`primary_palette` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'Blue'`.
"""
primary_hue = OptionProperty("500", options=hue)
"""
The color hue of the application.
Available options are: `'50'`, `'100'`, `'200'`, `'300'`, `'400'`, `'500'`,
`'600'`, `'700'`, `'800'`, `'900'`, `'A100'`, `'A200'`, `'A400'`, `'A700'`.
To change the hue color scheme of an application:
.. code-block:: python
from kivy.uix.screenmanager import Screen
from kivymd.app import MDApp
from kivymd.uix.button import MDRectangleFlatButton
class MainApp(MDApp):
def build(self):
self.theme_cls.primary_palette = "Green" # "Purple", "Red"
self.theme_cls.primary_hue = "200" # "500"
screen = Screen()
screen.add_widget(
MDRectangleFlatButton(
text="Hello, World",
pos_hint={"center_x": 0.5, "center_y": 0.5},
)
)
return screen
MainApp().run()
With a value of ``self.theme_cls.primary_hue = "500"``:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-palette.png
With a value of ``self.theme_cls.primary_hue = "200"``:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-hue.png
:attr:`primary_hue` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'500'`.
"""
primary_light_hue = OptionProperty("200", options=hue)
"""
Hue value for :attr:`primary_light`.
:attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'200'`.
"""
primary_dark_hue = OptionProperty("700", options=hue)
"""
Hue value for :attr:`primary_dark`.
:attr:`primary_light_hue` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'700'`.
"""
def _get_primary_color(self):
return get_color_from_hex(
colors[self.primary_palette][self.primary_hue]
)
primary_color = AliasProperty(
_get_primary_color, bind=("primary_palette", "primary_hue")
)
"""
The color of the current application theme in ``rgba`` format.
:attr:`primary_color` is an :class:`~kivy.properties.AliasProperty` that
returns the value of the current application theme, property is readonly.
"""
def _get_primary_light(self):
return get_color_from_hex(
colors[self.primary_palette][self.primary_light_hue]
)
primary_light = AliasProperty(
_get_primary_light, bind=("primary_palette", "primary_light_hue")
)
"""
Colors of the current application color theme in ``rgba`` format
(in lighter color).
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
KV = '''
Screen:
MDRaisedButton:
text: "primary_light"
pos_hint: {"center_x": 0.5, "center_y": 0.7}
md_bg_color: app.theme_cls.primary_light
MDRaisedButton:
text: "primary_color"
pos_hint: {"center_x": 0.5, "center_y": 0.5}
MDRaisedButton:
text: "primary_dark"
pos_hint: {"center_x": 0.5, "center_y": 0.3}
md_bg_color: app.theme_cls.primary_dark
'''
class MainApp(MDApp):
def build(self):
self.theme_cls.primary_palette = "Green"
return Builder.load_string(KV)
MainApp().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-colors-light-dark.png
:align: center
:attr:`primary_light` is an :class:`~kivy.properties.AliasProperty` that
returns the value of the current application theme (in lighter color),
property is readonly.
"""
def _get_primary_dark(self):
return get_color_from_hex(
colors[self.primary_palette][self.primary_dark_hue]
)
primary_dark = AliasProperty(
_get_primary_dark, bind=("primary_palette", "primary_dark_hue")
)
"""
Colors of the current application color theme
in ``rgba`` format (in darker color).
:attr:`primary_dark` is an :class:`~kivy.properties.AliasProperty` that
returns the value of the current application theme (in darker color),
property is readonly.
"""
accent_palette = OptionProperty("Amber", options=palette)
"""
The application color palette used for items such as the tab indicator
in the :attr:`MDTabsBar` class and so on...
The image below shows the color schemes with the values
``self.theme_cls.accent_palette = 'Blue'``, ``Red'`` and ``Yellow'``:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/accent-palette.png
:attr:`primary_hue` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'Amber'`.
"""
accent_hue = OptionProperty("500", options=hue)
"""Similar to :attr:`primary_hue`,
but returns a value for :attr:`accent_palette`.
:attr:`accent_hue` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'500'`.
"""
accent_light_hue = OptionProperty("200", options=hue)
"""
Hue value for :attr:`accent_light`.
:attr:`accent_light_hue` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'200'`.
"""
accent_dark_hue = OptionProperty("700", options=hue)
"""
Hue value for :attr:`accent_dark`.
:attr:`accent_dark_hue` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'700'`.
"""
def _get_accent_color(self):
return get_color_from_hex(colors[self.accent_palette][self.accent_hue])
accent_color = AliasProperty(
_get_accent_color, bind=["accent_palette", "accent_hue"]
)
"""Similar to :attr:`primary_color`,
but returns a value for :attr:`accent_color`.
:attr:`accent_color` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`accent_color`,
property is readonly.
"""
def _get_accent_light(self):
return get_color_from_hex(
colors[self.accent_palette][self.accent_light_hue]
)
accent_light = AliasProperty(
_get_accent_light, bind=["accent_palette", "accent_light_hue"]
)
"""Similar to :attr:`primary_light`,
but returns a value for :attr:`accent_light`.
:attr:`accent_light` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`accent_light`,
property is readonly.
"""
def _get_accent_dark(self):
return get_color_from_hex(
colors[self.accent_palette][self.accent_dark_hue]
)
accent_dark = AliasProperty(
_get_accent_dark, bind=["accent_palette", "accent_dark_hue"]
)
"""Similar to :attr:`primary_dark`,
but returns a value for :attr:`accent_dark`.
:attr:`accent_dark` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`accent_dark`,
property is readonly.
"""
theme_style = OptionProperty("Light", options=["Light", "Dark"])
"""App theme style.
.. code-block:: python
from kivy.uix.screenmanager import Screen
from kivymd.app import MDApp
from kivymd.uix.button import MDRectangleFlatButton
class MainApp(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark" # "Light"
screen = Screen()
screen.add_widget(
MDRectangleFlatButton(
text="Hello, World",
pos_hint={"center_x": 0.5, "center_y": 0.5},
)
)
return screen
MainApp().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/theme-style.png
:attr:`theme_style` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'Light'`.
"""
def _get_theme_style(self, opposite):
if opposite:
return "Light" if self.theme_style == "Dark" else "Dark"
else:
return self.theme_style
def _get_bg_darkest(self, opposite=False):
theme_style = self._get_theme_style(opposite)
if theme_style == "Light":
return get_color_from_hex(colors["Light"]["StatusBar"])
elif theme_style == "Dark":
return get_color_from_hex(colors["Dark"]["StatusBar"])
bg_darkest = AliasProperty(_get_bg_darkest, bind=["theme_style"])
"""
Similar to :attr:`bg_dark`,
but the color values are a tone lower (darker) than :attr:`bg_dark`.
.. code-block:: python
KV = '''
<Box@BoxLayout>:
bg: 0, 0, 0, 0
canvas:
Color:
rgba: root.bg
Rectangle:
pos: self.pos
size: self.size
BoxLayout:
Box:
bg: app.theme_cls.bg_light
Box:
bg: app.theme_cls.bg_normal
Box:
bg: app.theme_cls.bg_dark
Box:
bg: app.theme_cls.bg_darkest
'''
from kivy.lang import Builder
from kivymd.app import MDApp
class MainApp(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark" # "Light"
return Builder.load_string(KV)
MainApp().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bg-normal-dark-darkest.png
:attr:`bg_darkest` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`bg_darkest`,
property is readonly.
"""
def _get_op_bg_darkest(self):
return self._get_bg_darkest(True)
opposite_bg_darkest = AliasProperty(
_get_op_bg_darkest, bind=["theme_style"]
)
"""
The opposite value of color in the :attr:`bg_darkest`.
:attr:`opposite_bg_darkest` is an :class:`~kivy.properties.AliasProperty`
that returns the value in ``rgba`` format for :attr:`opposite_bg_darkest`,
property is readonly.
"""
def _get_bg_dark(self, opposite=False):
theme_style = self._get_theme_style(opposite)
if theme_style == "Light":
return get_color_from_hex(colors["Light"]["AppBar"])
elif theme_style == "Dark":
return get_color_from_hex(colors["Dark"]["AppBar"])
bg_dark = AliasProperty(_get_bg_dark, bind=["theme_style"])
"""
Similar to :attr:`bg_normal`,
but the color values are one tone lower (darker) than :attr:`bg_normal`.
:attr:`bg_dark` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`bg_dark`,
property is readonly.
"""
def _get_op_bg_dark(self):
return self._get_bg_dark(True)
opposite_bg_dark = AliasProperty(_get_op_bg_dark, bind=["theme_style"])
"""
The opposite value of color in the :attr:`bg_dark`.
:attr:`opposite_bg_dark` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`opposite_bg_dark`,
property is readonly.
"""
def _get_bg_normal(self, opposite=False):
theme_style = self._get_theme_style(opposite)
if theme_style == "Light":
return get_color_from_hex(colors["Light"]["Background"])
elif theme_style == "Dark":
return get_color_from_hex(colors["Dark"]["Background"])
bg_normal = AliasProperty(_get_bg_normal, bind=["theme_style"])
"""
Similar to :attr:`bg_light`,
but the color values are one tone lower (darker) than :attr:`bg_light`.
:attr:`bg_normal` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`bg_normal`,
property is readonly.
"""
def _get_op_bg_normal(self):
return self._get_bg_normal(True)
opposite_bg_normal = AliasProperty(_get_op_bg_normal, bind=["theme_style"])
"""
The opposite value of color in the :attr:`bg_normal`.
:attr:`opposite_bg_normal` is an :class:`~kivy.properties.AliasProperty`
that returns the value in ``rgba`` format for :attr:`opposite_bg_normal`,
property is readonly.
"""
def _get_bg_light(self, opposite=False):
theme_style = self._get_theme_style(opposite)
if theme_style == "Light":
return get_color_from_hex(colors["Light"]["CardsDialogs"])
elif theme_style == "Dark":
return get_color_from_hex(colors["Dark"]["CardsDialogs"])
bg_light = AliasProperty(_get_bg_light, bind=["theme_style"])
""""
Depending on the style of the theme (`'Dark'` or `'Light`')
that the application uses, :attr:`bg_light` contains the color value
in ``rgba`` format for the widgets background.
:attr:`bg_light` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`bg_light`,
property is readonly.
"""
def _get_op_bg_light(self):
return self._get_bg_light(True)
opposite_bg_light = AliasProperty(_get_op_bg_light, bind=["theme_style"])
"""
The opposite value of color in the :attr:`bg_light`.
:attr:`opposite_bg_light` is an :class:`~kivy.properties.AliasProperty`
that returns the value in ``rgba`` format for :attr:`opposite_bg_light`,
property is readonly.
"""
def _get_divider_color(self, opposite=False):
theme_style = self._get_theme_style(opposite)
if theme_style == "Light":
color = get_color_from_hex("000000")
elif theme_style == "Dark":
color = get_color_from_hex("FFFFFF")
color[3] = 0.12
return color
divider_color = AliasProperty(_get_divider_color, bind=["theme_style"])
"""
Color for dividing lines such as :class:`~kivymd.uix.card.MDSeparator`.
:attr:`divider_color` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`divider_color`,
property is readonly.
"""
def _get_op_divider_color(self):
return self._get_divider_color(True)
opposite_divider_color = AliasProperty(
_get_op_divider_color, bind=["theme_style"]
)
"""
The opposite value of color in the :attr:`divider_color`.
:attr:`opposite_divider_color` is an :class:`~kivy.properties.AliasProperty`
that returns the value in ``rgba`` format for :attr:`opposite_divider_color`,
property is readonly.
"""
def _get_text_color(self, opposite=False):
theme_style = self._get_theme_style(opposite)
if theme_style == "Light":
color = get_color_from_hex("000000")
color[3] = 0.87
elif theme_style == "Dark":
color = get_color_from_hex("FFFFFF")
return color
text_color = AliasProperty(_get_text_color, bind=["theme_style"])
"""
Color of the text used in the :class:`~kivymd.uix.label.MDLabel`.
:attr:`text_color` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`text_color`,
property is readonly.
"""
def _get_op_text_color(self):
return self._get_text_color(True)
opposite_text_color = AliasProperty(
_get_op_text_color, bind=["theme_style"]
)
"""
The opposite value of color in the :attr:`text_color`.
:attr:`opposite_text_color` is an :class:`~kivy.properties.AliasProperty`
that returns the value in ``rgba`` format for :attr:`opposite_text_color`,
property is readonly.
"""
def _get_secondary_text_color(self, opposite=False):
theme_style = self._get_theme_style(opposite)
if theme_style == "Light":
color = get_color_from_hex("000000")
color[3] = 0.54
elif theme_style == "Dark":
color = get_color_from_hex("FFFFFF")
color[3] = 0.70
return color
secondary_text_color = AliasProperty(
_get_secondary_text_color, bind=["theme_style"]
)
"""
The color for the secondary text that is used in classes
from the module :class:`~kivymd/uix/list.TwoLineListItem`.
:attr:`secondary_text_color` is an :class:`~kivy.properties.AliasProperty`
that returns the value in ``rgba`` format for :attr:`secondary_text_color`,
property is readonly.
"""
def _get_op_secondary_text_color(self):
return self._get_secondary_text_color(True)
opposite_secondary_text_color = AliasProperty(
_get_op_secondary_text_color, bind=["theme_style"]
)
"""
The opposite value of color in the :attr:`secondary_text_color`.
:attr:`opposite_secondary_text_color`
is an :class:`~kivy.properties.AliasProperty` that returns the value
in ``rgba`` format for :attr:`opposite_secondary_text_color`,
property is readonly.
"""
def _get_icon_color(self, opposite=False):
theme_style = self._get_theme_style(opposite)
if theme_style == "Light":
color = get_color_from_hex("000000")
color[3] = 0.54
elif theme_style == "Dark":
color = get_color_from_hex("FFFFFF")
return color
icon_color = AliasProperty(_get_icon_color, bind=["theme_style"])
"""
Color of the icon used in the :class:`~kivymd.uix.button.MDIconButton`.
:attr:`icon_color` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`icon_color`,
property is readonly.
"""
def _get_op_icon_color(self):
return self._get_icon_color(True)
opposite_icon_color = AliasProperty(
_get_op_icon_color, bind=["theme_style"]
)
"""
The opposite value of color in the :attr:`icon_color`.
:attr:`opposite_icon_color` is an :class:`~kivy.properties.AliasProperty`
that returns the value in ``rgba`` format for :attr:`opposite_icon_color`,
property is readonly.
"""
def _get_disabled_hint_text_color(self, opposite=False):
theme_style = self._get_theme_style(opposite)
if theme_style == "Light":
color = get_color_from_hex("000000")
color[3] = 0.38
elif theme_style == "Dark":
color = get_color_from_hex("FFFFFF")
color[3] = 0.50
return color
disabled_hint_text_color = AliasProperty(
_get_disabled_hint_text_color, bind=["theme_style"]
)
"""
Color of the disabled text used in the :class:`~kivymd.uix.textfield.MDTextField`.
:attr:`disabled_hint_text_color`
is an :class:`~kivy.properties.AliasProperty` that returns the value
in ``rgba`` format for :attr:`disabled_hint_text_color`,
property is readonly.
"""
def _get_op_disabled_hint_text_color(self):
return self._get_disabled_hint_text_color(True)
opposite_disabled_hint_text_color = AliasProperty(
_get_op_disabled_hint_text_color, bind=["theme_style"]
)
"""
The opposite value of color in the :attr:`disabled_hint_text_color`.
:attr:`opposite_disabled_hint_text_color`
is an :class:`~kivy.properties.AliasProperty` that returns the value
in ``rgba`` format for :attr:`opposite_disabled_hint_text_color`,
property is readonly.
"""
# Hardcoded because muh standard
def _get_error_color(self):
return get_color_from_hex(colors["Red"]["A700"])
error_color = AliasProperty(_get_error_color)
"""
Color of the error text used
in the :class:`~kivymd.uix.textfield.MDTextField`.
:attr:`error_color` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`error_color`,
property is readonly.
"""
def _get_ripple_color(self):
return self._ripple_color
def _set_ripple_color(self, value):
self._ripple_color = value
_ripple_color = ListProperty(get_color_from_hex(colors["Gray"]["400"]))
"""Private value."""
ripple_color = AliasProperty(
_get_ripple_color, _set_ripple_color, bind=["_ripple_color"]
)
"""
Color of ripple effects.
:attr:`ripple_color` is an :class:`~kivy.properties.AliasProperty` that
returns the value in ``rgba`` format for :attr:`ripple_color`,
property is readonly.
"""
def _determine_device_orientation(self, _, window_size):
if window_size[0] > window_size[1]:
self.device_orientation = "landscape"
elif window_size[1] >= window_size[0]:
self.device_orientation = "portrait"
device_orientation = StringProperty("")
"""
Device orientation.
:attr:`device_orientation` is an :class:`~kivy.properties.StringProperty`.
"""
def _get_standard_increment(self):
if DEVICE_TYPE == "mobile":
if self.device_orientation == "landscape":
return dp(48)
else:
return dp(56)
else:
return dp(64)
standard_increment = AliasProperty(
_get_standard_increment, bind=["device_orientation"]
)
"""
Value of standard increment.
:attr:`standard_increment` is an :class:`~kivy.properties.AliasProperty`
that returns the value in ``rgba`` format for :attr:`standard_increment`,
property is readonly.
"""
def _get_horizontal_margins(self):
if DEVICE_TYPE == "mobile":
return dp(16)
else:
return dp(24)
horizontal_margins = AliasProperty(_get_horizontal_margins)
"""
Value of horizontal margins.
:attr:`horizontal_margins` is an :class:`~kivy.properties.AliasProperty`
that returns the value in ``rgba`` format for :attr:`horizontal_margins`,
property is readonly.
"""
def on_theme_style(self, instance, value):
if (
hasattr(App.get_running_app(), "theme_cls")
and App.get_running_app().theme_cls == self
):
self.set_clearcolor_by_theme_style(value)
set_clearcolor = BooleanProperty(True)
def set_clearcolor_by_theme_style(self, theme_style):
if not self.set_clearcolor:
return
if theme_style == "Light":
Window.clearcolor = get_color_from_hex(
colors["Light"]["Background"]
)
elif theme_style == "Dark":
Window.clearcolor = get_color_from_hex(colors["Dark"]["Background"])
# font name, size (sp), always caps, letter spacing (sp)
font_styles = DictProperty(
{
"H1": ["RobotoLight", 96, False, -1.5],
"H2": ["RobotoLight", 60, False, -0.5],
"H3": ["Roboto", 48, False, 0],
"H4": ["Roboto", 34, False, 0.25],
"H5": ["Roboto", 24, False, 0],
"H6": ["RobotoMedium", 20, False, 0.15],
"Subtitle1": ["Roboto", 16, False, 0.15],
"Subtitle2": ["RobotoMedium", 14, False, 0.1],
"Body1": ["Roboto", 16, False, 0.5],
"Body2": ["Roboto", 14, False, 0.25],
"Button": ["RobotoMedium", 14, True, 1.25],
"Caption": ["Roboto", 12, False, 0.4],
"Overline": ["Roboto", 10, True, 1.5],
"Icon": ["Icons", 24, False, 0],
}
)
"""
Data of default font styles.
Add custom font:
.. code-block:: python
KV = '''
Screen:
MDLabel:
text: "JetBrainsMono"
halign: "center"
font_style: "JetBrainsMono"
'''
from kivy.core.text import LabelBase
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.font_definitions import theme_font_styles
class MainApp(MDApp):
def build(self):
LabelBase.register(
name="JetBrainsMono",
fn_regular="JetBrainsMono-Regular.ttf")
theme_font_styles.append('JetBrainsMono')
self.theme_cls.font_styles["JetBrainsMono"] = [
"JetBrainsMono",
16,
False,
0.15,
]
return Builder.load_string(KV)
MainApp().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-styles.png
:attr:`font_styles` is an :class:`~kivy.properties.DictProperty`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.rec_shadow = Atlas(f"{images_path}rec_shadow.atlas")
self.rec_st_shadow = Atlas(f"{images_path}rec_st_shadow.atlas")
self.quad_shadow = Atlas(f"{images_path}quad_shadow.atlas")
self.round_shadow = Atlas(f"{images_path}round_shadow.atlas")
Clock.schedule_once(lambda x: self.on_theme_style(0, self.theme_style))
self._determine_device_orientation(None, Window.size)
Window.bind(size=self._determine_device_orientation)
class ThemableBehavior(EventDispatcher):
theme_cls = ObjectProperty()
"""
Instance of :class:`~ThemeManager` class.
:attr:`theme_cls` is an :class:`~kivy.properties.ObjectProperty`.
"""
device_ios = BooleanProperty(DEVICE_IOS)
"""
``True`` if device is ``iOS``.
:attr:`device_ios` is an :class:`~kivy.properties.BooleanProperty`.
"""
opposite_colors = BooleanProperty(False)
def __init__(self, **kwargs):
if self.theme_cls is not None:
pass
else:
try:
if not isinstance(
App.get_running_app().property("theme_cls", True),
ObjectProperty,
):
raise ValueError(
"KivyMD: App object must be inherited from "
"`kivymd.app.MDApp`. See "
"https://github.com/kivymd/KivyMD/blob/master/README.md#api-breaking-changes"
)
except AttributeError:
raise ValueError(
"KivyMD: App object must be initialized before loading "
"root widget. See "
"https://github.com/kivymd/KivyMD/wiki/Modules-Material-App#exceptions"
)
self.theme_cls = App.get_running_app().theme_cls
super().__init__(**kwargs)

89
kivymd/theming_dynamic_text.py Executable file
View file

@ -0,0 +1,89 @@
"""
Theming Dynamic Text
====================
Two implementations. The first is based on color brightness obtained from-
https://www.w3.org/TR/AERT#color-contrast
The second is based on relative luminance calculation for sRGB obtained from-
https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
and contrast ratio calculation obtained from-
https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
Preliminary testing suggests color brightness more closely matches the
`Material Design spec` suggested text colors, but the alternative implementation
is both newer and the current 'correct' recommendation, so is included here
as an option.
"""
def _color_brightness(color):
# Implementation of color brightness method
brightness = color[0] * 299 + color[1] * 587 + color[2] * 114
brightness = brightness
return brightness
def _black_or_white_by_color_brightness(color):
if _color_brightness(color) >= 500:
return "black"
else:
return "white"
def _normalized_channel(color):
# Implementation of contrast ratio and relative luminance method
if color <= 0.03928:
return color / 12.92
else:
return ((color + 0.055) / 1.055) ** 2.4
def _luminance(color):
rg = _normalized_channel(color[0])
gg = _normalized_channel(color[1])
bg = _normalized_channel(color[2])
return 0.2126 * rg + 0.7152 * gg + 0.0722 * bg
def _black_or_white_by_contrast_ratio(color):
l_color = _luminance(color)
l_black = 0.0
l_white = 1.0
b_contrast = (l_color + 0.05) / (l_black + 0.05)
w_contrast = (l_white + 0.05) / (l_color + 0.05)
return "white" if w_contrast >= b_contrast else "black"
def get_contrast_text_color(color, use_color_brightness=True):
if use_color_brightness:
contrast_color = _black_or_white_by_color_brightness(color)
else:
contrast_color = _black_or_white_by_contrast_ratio(color)
if contrast_color == "white":
return 1, 1, 1, 1
else:
return 0, 0, 0, 1
if __name__ == "__main__":
from kivy.utils import get_color_from_hex
from kivymd.color_definitions import colors, text_colors
for c in colors.items():
if c[0] in ["Light", "Dark"]:
continue
color = c[0]
print(f"For the {color} color palette:")
for name, hex_color in c[1].items():
if hex_color:
col = get_color_from_hex(hex_color)
col_bri = get_contrast_text_color(col)
con_rat = get_contrast_text_color(
col, use_color_brightness=False
)
text_color = text_colors[c[0]][name]
print(
f" The {name} hue gives {col_bri} using color "
f"brightness, {con_rat} using contrast ratio, and "
f"{text_color} from the MD spec"
)

21
kivymd/toast/LICENSE Executable file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2013 Brian - androidtoast library
Copyright (c) 2019 Ivanov Yuri - kivytoast library
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

41
kivymd/toast/README.md Executable file
View file

@ -0,0 +1,41 @@
KivyToast
========
A package for working with messages like Toast on Android. It is intended for use in applications written using the Kivy framework.
This package is an improved version of the package https://github.com/knappador/kivy-toaster in which human toasts are written, written on Kivy.
<img src="https://raw.githubusercontent.com/HeaTTheatR/KivyToast/master/Screenshot.png" align="center"/>
The package modules are written using the framework for cross-platform development of <Kivy>.
Information about the <Kivy> framework is available at http://kivy.org.
An example of usage (note that with this import the native implementation of toasts will be used for the Android platform and implementation on Kivy for others:
```python
from toast import toast
...
# And then in the code, toasts are available
# by calling the toast function:
toast ('Your message')
```
To force the Kivy implementation on the Android platform, use the import of the form:
```python
from toast.kivytoast import toast
```
PROGRAMMING LANGUAGE
--------------------
Python 2.7 +
DEPENDENCE
----------
The [Kivy] framework (http://kivy.org/docs/installation/installation.html)
LICENSE
-------
MIT

11
kivymd/toast/__init__.py Executable file
View file

@ -0,0 +1,11 @@
__all__ = ("toast",)
from kivy.utils import platform
if platform == "android":
try:
from .androidtoast import toast
except BaseException:
from .kivytoast import toast
else:
from .kivytoast import toast

View file

@ -0,0 +1,12 @@
"""
Toast for Android device
========================
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toast.png
:align: center
"""
__all__ = ("toast",)
from .androidtoast import toast

View file

@ -0,0 +1,68 @@
"""
AndroidToast
============
.. rubric:: Native implementation of toast for Android devices.
.. code-block:: python
from kivymd.app import MDApp
# Will be automatically used native implementation of the toast
# if your application is running on an Android device.
# Otherwise, will be used toast implementation
# from the kivymd/toast/kivytoast package.
from kivymd.toast import toast
KV = '''
BoxLayout:
orientation:'vertical'
MDToolbar:
id: toolbar
title: 'Test Toast'
md_bg_color: app.theme_cls.primary_color
left_action_items: [['menu', lambda x: '']]
FloatLayout:
MDRaisedButton:
text: 'TEST KIVY TOAST'
on_release: app.show_toast()
pos_hint: {'center_x': .5, 'center_y': .5}
'''
class Test(MDApp):
def show_toast(self):
'''Displays a toast on the screen.'''
toast('Test Kivy Toast')
def build(self):
return Builder.load_string(KV)
Test().run()
"""
__all__ = ("toast",)
from android.runnable import run_on_ui_thread
from jnius import autoclass, cast
Toast = autoclass("android.widget.Toast")
context = autoclass("org.kivy.android.PythonActivity").mActivity
@run_on_ui_thread
def toast(text, length_long=False):
"""Displays a toast.
:length_long: The amount of time (in seconds) that the toast is visible on the screen.
"""
duration = Toast.LENGTH_LONG if length_long else Toast.LENGTH_SHORT
String = autoclass("java.lang.String")
c = cast("java.lang.CharSequence", String(text))
t = Toast.makeText(context, c, duration)
t.show()

View file

@ -0,0 +1,3 @@
__all__ = ("toast",)
from .kivytoast import toast

View file

@ -0,0 +1,133 @@
"""
KivyToast
=========
.. rubric:: Implementation of toasts for desktop.
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.toast import toast
KV = '''
BoxLayout:
orientation:'vertical'
MDToolbar:
id: toolbar
title: 'Test Toast'
md_bg_color: app.theme_cls.primary_color
left_action_items: [['menu', lambda x: '']]
FloatLayout:
MDRaisedButton:
text: 'TEST KIVY TOAST'
on_release: app.show_toast()
pos_hint: {'center_x': .5, 'center_y': .5}
'''
class Test(MDApp):
def show_toast(self):
'''Displays a toast on the screen.'''
toast('Test Kivy Toast')
def build(self):
return Builder.load_string(KV)
Test().run()
"""
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import NumericProperty
from kivy.uix.label import Label
from kivy.uix.modalview import ModalView
from kivymd import images_path
Builder.load_string(
"""
<Toast>:
canvas:
Color:
rgba: .2, .2, .2, 1
RoundedRectangle:
pos: self.pos
size: self.size
radius: [15,]
"""
)
class Toast(ModalView):
duration = NumericProperty(2.5)
"""
The amount of time (in seconds) that the toast is visible on the screen.
:attr:`duration` is an :class:`~kivy.properties.NumericProperty`
and defaults to `2.5`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.size_hint = (None, None)
self.pos_hint = {"center_x": 0.5, "center_y": 0.1}
self.background_color = [0, 0, 0, 0]
self.background = f"{images_path}transparent.png"
self.opacity = 0
self.auto_dismiss = True
self.label_toast = Label(size_hint=(None, None), opacity=0)
self.label_toast.bind(texture_size=self.label_check_texture_size)
self.add_widget(self.label_toast)
def label_check_texture_size(self, instance, texture_size):
texture_width, texture_height = texture_size
if texture_width > Window.width:
instance.text_size = (Window.width - dp(10), None)
instance.texture_update()
texture_width, texture_height = instance.texture_size
self.size = (texture_width + 25, texture_height + 25)
def toast(self, text_toast):
self.label_toast.text = text_toast
self.open()
def on_open(self):
self.fade_in()
Clock.schedule_once(self.fade_out, self.duration)
def fade_in(self):
Animation(opacity=1, duration=0.4).start(self.label_toast)
Animation(opacity=1, duration=0.4).start(self)
def fade_out(self, interval):
Animation(opacity=0, duration=0.4).start(self.label_toast)
anim_body = Animation(opacity=0, duration=0.4)
anim_body.bind(on_complete=lambda *x: self.dismiss())
anim_body.start(self)
def on_touch_down(self, touch):
if not self.collide_point(*touch.pos):
if self.auto_dismiss:
self.dismiss()
return False
super(ModalView, self).on_touch_down(touch)
return True
def toast(text: str, duration=2.5):
"""Displays a toast.
:duration: The amount of time (in seconds) that the toast is visible on the screen.
"""
Toast(duration=duration).toast(text)

0
kivymd/tools/__init__.py Normal file
View file

View file

View file

@ -0,0 +1,63 @@
"""
PyInstaller hooks
=================
Add ``hookspath=[kivymd.hooks_path]`` to your .spec file.
Example of .spec file
=====================
.. code-block:: python
# -*- mode: python ; coding: utf-8 -*-
import sys
import os
from kivy_deps import sdl2, glew
from kivymd import hooks_path as kivymd_hooks_path
path = os.path.abspath(".")
a = Analysis(
["main.py"],
pathex=[path],
hookspath=[kivymd_hooks_path],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=None,
noarchive=False,
)
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
*[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
debug=False,
strip=False,
upx=True,
name="app_name",
console=True,
)
"""
__all__ = ("hooks_path", "datas", "hiddenimports")
from os.path import abspath, basename, dirname, join
import kivymd
hooks_path = dirname(abspath(__file__))
"""Path to hook directory to use with PyInstaller.
See :mod:`kivymd.tools.packaging.pyinstaller` for more information."""
datas = [
(kivymd.fonts_path, join("kivymd", basename(dirname(kivymd.fonts_path)))),
(kivymd.images_path, join("kivymd", basename(dirname(kivymd.images_path)))),
]
hiddenimports = ["PIL"]

View file

@ -0,0 +1 @@
from kivymd.tools.packaging.pyinstaller import * # NOQA

View file

View file

@ -0,0 +1,92 @@
# Copyright (c) 2019-2020 Artem Bulgakov
#
# This file is distributed under the terms of the same license,
# as the Kivy framework.
import argparse
import sys
class ArgumentParserWithHelp(argparse.ArgumentParser):
def parse_args(self, args=None, namespace=None):
# Add help when no arguments specified
if not args and not len(sys.argv) > 1:
self.print_help()
self.exit(1)
return super().parse_args(args, namespace)
def error(self, message):
# Add full help on error
self.print_help()
self.exit(2, f"\nError: {message}\n")
def format_help(self):
# Add subparsers usage and help to full help text
formatter = self._get_formatter()
# Get subparsers
subparsers_actions = [
action
for action in self._actions
if isinstance(action, argparse._SubParsersAction)
]
# Description
formatter.add_text(self.description)
# Usage
formatter.add_usage(
self.usage,
self._actions,
self._mutually_exclusive_groups,
prefix="Usage:\n",
)
# Subparsers usage
for subparsers_action in subparsers_actions:
for choice, subparser in subparsers_action.choices.items():
formatter.add_usage(
subparser.usage,
subparser._actions,
subparser._mutually_exclusive_groups,
prefix="",
)
# Positionals, optionals and user-defined groups
for action_group in self._action_groups:
if not any(
[
action in subparsers_actions
for action in action_group._group_actions
]
):
formatter.start_section(action_group.title)
formatter.add_text(action_group.description)
formatter.add_arguments(action_group._group_actions)
formatter.end_section()
else:
# Process subparsers differently
# Just show list of choices
formatter.start_section(action_group.title)
# formatter.add_text(action_group.description)
for action in action_group._group_actions:
for choice in action.choices:
formatter.add_text(choice)
formatter.end_section()
# Subparsers help
for subparsers_action in subparsers_actions:
for choice, subparser in subparsers_action.choices.items():
formatter.start_section(choice)
for action_group in subparser._action_groups:
formatter.start_section(action_group.title)
formatter.add_text(action_group.description)
formatter.add_arguments(action_group._group_actions)
formatter.end_section()
formatter.end_section()
# Epilog
formatter.add_text(self.epilog)
# Determine help from format above
return formatter.format_help()

View file

@ -0,0 +1,88 @@
# Copyright (c) 2019-2020 Artem Bulgakov
#
# This file is distributed under the terms of the same license,
# as the Kivy framework.
import subprocess
def command(cmd: list, capture_output: bool = False) -> str:
"""Run system command."""
print(f"Command: {subprocess.list2cmdline(cmd)}")
if capture_output:
out = subprocess.check_output(cmd)
out = out.decode("utf-8")
print(out.strip())
return out
else:
subprocess.check_call(cmd)
return ""
def get_previous_version() -> str:
"""Returns latest tag in git."""
command(["git", "checkout", "master"])
old_version = command(
["git", "describe", "--abbrev=0", "--tags"], capture_output=True
)
old_version = old_version[:-1] # Remove \n
return old_version
def git_clean(ask: bool = True):
"""Clean git repository from untracked and changed files."""
# Check what files will be removed
files_to_clean = command(
["git", "clean", "-dx", "--force", "--dry-run"], capture_output=True
).strip()
# Ask before removing
if ask and files_to_clean:
while True:
ans = input("Do you want to remove these files? (yes/no)").lower()
if ans == "y" or ans == "yes":
break
elif ans == "n" or ans == "no":
print("git clean is required. Exit")
exit(0)
# Remove all untracked files
command(["git", "clean", "-dx", "--force"])
command(["git", "reset", "--hard"])
def git_commit(message: str, allow_error: bool = False, add_files: list = None):
"""Make commit."""
add_files = add_files if add_files else ["-A"]
command(["git", "add", *add_files])
try:
command(["git", "commit", "--all", "-m", message])
except subprocess.CalledProcessError as e:
if not allow_error:
raise e
def git_tag(name: str):
"""Create tag."""
command(["git", "tag", name])
def git_push(branches_to_push: list, ask: bool = True, push: bool = False):
"""Push all changes."""
if ask:
push = input("Do you want to push changes? (y)") in (
"",
"y",
"yes",
)
cmd = ["git", "push", "--tags", "origin", "master", *branches_to_push]
if push:
command(cmd)
else:
print(
f"Changes are not pushed. Command for manual pushing: {subprocess.list2cmdline(cmd)}"
)
if __name__ == "__main__":
git_clean(ask=True)

View file

@ -0,0 +1,337 @@
# Copyright (c) 2019-2020 Artem Bulgakov
#
# This file is distributed under the terms of the same license,
# as the Kivy framework.
"""
Script to make release
======================
Run this script before release (before deploying).
What this script does:
* Undo all local changes in repository
* Update version in __init__.py, README
* Format files
* Rename file "unreleased.rst" to version, add to index.rst
* Commit "Version ..."
* Create tag
* Add "unreleased.rst" to Change Log, add to index.rst
* Commit
* Git push
"""
import os
import re
import subprocess
import sys
from kivymd.tools.release.argument_parser import ArgumentParserWithHelp
from kivymd.tools.release.git_commands import (
command,
get_previous_version,
git_clean,
git_commit,
git_push,
git_tag,
)
from kivymd.tools.release.update_icons import update_icons
def run_pre_commit():
"""Run pre-commit."""
try:
command(["pre-commit", "run", "--all-files"])
except subprocess.CalledProcessError:
pass
git_commit("Run pre-commit", allow_error=True)
def replace_in_file(pattern, repl, file):
"""Replace one `pattern` match to `repl` in file `file`."""
file_content = open(file, "rt", encoding="utf-8").read()
new_file_content = re.sub(pattern, repl, file_content, 1, re.M)
open(file, "wt", encoding="utf-8").write(new_file_content)
return not file_content == new_file_content
def update_init_py(version, is_release, test: bool = False):
"""Change version in `kivymd/__init__.py`."""
init_file = os.path.abspath("kivymd/__init__.py")
init_version_regex = r"(?<=^__version__ = ['\"])[^'\"]+(?=['\"]$)"
success = replace_in_file(init_version_regex, version, init_file)
if test and not success:
print("Couldn't update __init__.py file.", file=sys.stderr)
init_version_regex = r"(?<=^release = )(True|False)(?=$)"
success = replace_in_file(init_version_regex, str(is_release), init_file)
if test and not success:
print("Couldn't update __init__.py file.", file=sys.stderr)
def update_readme(previous_version, version, test: bool = False):
"""Change version in README."""
readme_file = os.path.abspath("README.md")
readme_version_regex = rf"(?<=\[){previous_version}[ \-*\w^\]\n]*(?=\])"
success = replace_in_file(readme_version_regex, version, readme_file)
if test and not success:
print("Couldn't update README.md file.", file=sys.stderr)
readme_install_version_regex = (
rf"(?<=pip install kivymd==){previous_version}(?=\n```)"
)
success = replace_in_file(
readme_install_version_regex, version, readme_file
)
if test and not success:
print("Couldn't update README.md file.", file=sys.stderr)
readme_buildozer_version_regex = (
rf"(?<=, kivymd==){previous_version}(?=\n```)"
)
success = replace_in_file(
readme_buildozer_version_regex, version, readme_file
)
if test and not success:
print("Couldn't update README.md file.", file=sys.stderr)
def move_changelog(
index_file,
unreleased_file,
previous_version,
version_file,
version,
test: bool = False,
):
"""Edit unreleased.rst and rename to <version>.rst."""
# Read unreleased changelog
changelog = open(unreleased_file, "rt", encoding="utf-8").read()
# Edit changelog
changelog = re.sub(
r"Unreleased\n----------",
f"{version}\n{'-' * (1 + len(version))}",
changelog,
1,
re.M,
)
changelog = re.sub(
r"(?<=See on GitHub: `)branch master",
f"tag {version}",
changelog,
1,
re.M,
)
changelog = re.sub(r"(?<=/tree/)master", f"{version}", changelog, 1, re.M)
changelog = re.sub(
rf"(?<=compare {previous_version}/)master",
f"{version}",
changelog,
1,
re.M,
)
changelog = re.sub(
rf"(?<=compare/{previous_version}...)master",
f"{version}",
changelog,
1,
re.M,
)
changelog = re.sub(
r"(?<=pip install )https[\S]*/master.zip(?=\n)",
f"kivymd=={version}",
changelog,
1,
re.M,
)
# Write changelog
open(version_file, "wt", encoding="utf-8").write(changelog)
# Remove unreleased changelog
os.remove(unreleased_file)
# Update index file
success = replace_in_file(
"/changelog/unreleased.rst", f"/changelog/{version}.rst", index_file
)
if test and not success:
print("Couldn't update changelog file.", file=sys.stderr)
def create_unreleased_changelog(
index_file,
unreleased_file,
version,
ask: bool = True,
test: bool = False,
):
"""Create unreleased.rst by template."""
# Check if unreleased file exists
if os.path.exists(unreleased_file):
if ask and input(
f'Do you want to rewrite "{unreleased_file}"? (y)'
) not in (
"",
"y",
"yes",
):
exit(0)
# Generate unreleased changelog
changelog = f"""Unreleased
----------
See on GitHub: `branch master <https://github.com/kivymd/KivyMD/tree/master>`_ | `compare {version}/master <https://github.com/kivymd/KivyMD/compare/{version}...master>`_
.. code-block:: bash
pip install https://github.com/kivymd/KivyMD/archive/master.zip
* Bug fixes and other minor improvements.
"""
# Create unreleased file
open(unreleased_file, "wt", encoding="utf-8").write(changelog)
# Update index file
success = replace_in_file(
r"(?<=Change Log\n==========\n\n)",
".. include:: /changelog/unreleased.rst\n",
index_file,
)
if test and not success:
print("Couldn't update changelog index file.", file=sys.stderr)
def main():
parser = create_argument_parser()
args = parser.parse_args()
release = args.command == "release"
version = args.version or "0.0.0"
next_version = args.next_version or (
(version[:-1] + str(int(version[-1]) + 1) + ".dev0")
if "rc" not in version
else version
)
prepare = args.command == "prepare"
test = args.command == "test"
ask = args.yes is not True
push = args.push is True
if release and version == "0.0.0":
parser.error("Please specify new version.")
version_re = r"[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}(rc[\d]{1,3})?"
if not re.match(version_re, version):
parser.error(f'Version "{version}" doesn\'t match template.')
next_version_re = (
r"[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}(\.dev[\d]{1,3}|rc[\d]{1,3})?"
)
if not re.match(next_version_re, next_version):
parser.error(f'Next version "{next_version}" doesn\'t match template.')
if test and push:
parser.error("Don't use --push with test.")
repository_root = os.path.normpath(
os.path.dirname(
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
)
)
# Change directory to repository root
os.chdir(repository_root)
previous_version = get_previous_version()
# Print info
print(f"Previous version: {previous_version}")
print(f"New version: {version}")
print(f"Next version: {next_version}")
update_icons(make_commit=True)
git_clean(ask=ask)
run_pre_commit()
if prepare:
git_push([], ask=ask, push=push)
return
update_init_py(version, is_release=True, test=test)
update_readme(previous_version, version, test=test)
changelog_index_file = os.path.join(
repository_root, "docs", "sources", "changelog", "index.rst"
)
changelog_unreleased_file = os.path.join(
repository_root, "docs", "sources", "changelog", "unreleased.rst"
)
changelog_version_file = os.path.join(
repository_root, "docs", "sources", "changelog", f"{version}.rst"
)
move_changelog(
changelog_index_file,
changelog_unreleased_file,
previous_version,
changelog_version_file,
version,
test=test,
)
git_commit(f"KivyMD {version}")
git_tag(version)
branches_to_push = []
# Move branch stable to stable-x.x.x
# command(["git", "branch", "-m", "stable", f"stable-{old_version}"])
# branches_to_push.append(f"stable-{old_version}")
# Create branch stable
# command(["git", "branch", "stable"])
# command(["git", "push", "--force", "origin", "master:stable"])
# branches_to_push.append("stable")
create_unreleased_changelog(
changelog_index_file,
changelog_unreleased_file,
version,
test=test,
)
update_init_py(next_version, is_release=False, test=test)
git_commit(f"KivyMD {next_version}")
git_push(branches_to_push, ask=ask, push=push)
def create_argument_parser():
parser = ArgumentParserWithHelp(
prog="make_release.py",
allow_abbrev=False,
# usage="%(prog)s command [options] extensions [--exclude extensions]",
)
parser.add_argument(
"--yes",
action="store_true",
help="remove and modify files without asking.",
)
parser.add_argument(
"--push",
action="store_true",
help="push changes to remote repository. Use only with release and prepare.",
)
parser.add_argument(
"command",
choices=["release", "prepare", "test"],
help="release will update icons, modify files and make tag.\n"
"prepare will update icons and format files.\n"
"test will check if script can modify each file correctly.",
)
parser.add_argument(
"version",
type=str,
nargs="?",
help="new version in format n.n.n (1.111.11).",
)
parser.add_argument(
"next_version",
type=str,
nargs="?",
help="development version in format n.n.n.devn (1.111.11.dev0).",
)
return parser
if __name__ == "__main__":
main()

View file

@ -0,0 +1,172 @@
# Copyright (c) 2019-2020 Artem Bulgakov
#
# This file is distributed under the terms of the same license,
# as the Kivy framework.
"""
Tool for updating Iconic font
=============================
Downloads archive from https://github.com/Templarian/MaterialDesign-Webfont and
updates font file with icon_definitions.
"""
import json
import os
import re
import shutil
import sys
import zipfile
import requests
from kivymd.tools.release.git_commands import git_commit
# Paths to files in kivymd repository
kivymd_path = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
font_path = os.path.join(
kivymd_path, "fonts", "materialdesignicons-webfont.ttf"
)
icon_definitions_path = os.path.join(kivymd_path, "icon_definitions.py")
font_version = "master"
# URL to download new archive (set None if already downloaded)
url = (
f"https://github.com/Templarian/MaterialDesign-Webfont"
f"/archive/{font_version}.zip"
)
# url = None
# Paths to files in loaded archive
temp_path = os.path.join(os.path.dirname(__file__), "temp")
temp_repo_path = os.path.join(
temp_path, f"MaterialDesign-Webfont-{font_version}"
)
temp_font_path = os.path.join(
temp_repo_path, "fonts", "materialdesignicons-webfont.ttf"
)
temp_preview_path = os.path.join(temp_repo_path, "preview.html")
# Regex
re_icons_json = re.compile(r"(?<=var icons = )[\S ]+(?=;)")
re_additional_icons = re.compile(r"(?<=icons\.push\()[\S ]+(?=\);)")
re_version = re.compile(r"(?<=<span class=\"version\">)[\d.]+(?=</span>)")
re_quote_keys = re.compile(r"([{\s,])(\w+)(:)")
re_icon_definitions = re.compile(r"md_icons = {\n([ ]{4}[\s\S]*,\n)*}")
re_version_in_file = re.compile(r"(?<=LAST UPDATED: Version )[\d.]+(?=\n)")
def download_file(url, path):
response = requests.get(url, stream=True)
if response.status_code != 200:
return False
with open(path, "wb") as f:
shutil.copyfileobj(response.raw, f)
return True
def unzip_archive(archive_path, dir_path):
with zipfile.ZipFile(archive_path, "r") as zip_ref:
zip_ref.extractall(dir_path)
def get_icons_list():
# There is js array with icons in file preview.html
with open(temp_preview_path, "r") as f:
preview_file = f.read()
# Find version
version = re_version.findall(preview_file)[0]
# Load icons
jsons_icons = re_icons_json.findall(preview_file)[0]
json_icons = re_quote_keys.sub(r'\1"\2"\3', jsons_icons)
icons = json.loads(json_icons)
# Find additional icons (like a blank icon)
# jsons_additional_icons = re_additional_icons.findall(preview_file)
# for j in jsons_additional_icons:
# json_additional_icons = re_quote_keys.sub(r'\1"\2"\3', j)
# icons.append(json.loads(json_additional_icons))
return icons, version
def make_icon_definitions(icons):
# Make python dict ("name": hex)
icon_definitions = "md_icons = {\n"
for i in icons:
icon_definitions += " " * 4
if len(i["hex"]) != 4:
# Some icons has 5-digit unicode
i["hex"] = "0" * (8 - len(i["hex"])) + i["hex"]
icon_definitions += f'"{i["name"]}": "\\U{i["hex"].upper()}",\n'
else:
icon_definitions += f'"{i["name"]}": "\\u{i["hex"].upper()}",\n'
icon_definitions += " " * 4 + '"blank": " ",\n' # Add blank icon (space)
icon_definitions += "}"
return icon_definitions
def export_icon_definitions(icon_definitions, version):
with open(icon_definitions_path, "r") as f:
icon_definitions_file = f.read()
# Change md_icons list
new_icon_definitions = re_icon_definitions.sub(
icon_definitions.replace("\\", "\\\\"), icon_definitions_file, 1
)
# Change version
new_icon_definitions = re_version_in_file.sub(
version, new_icon_definitions, 1
)
with open(icon_definitions_path, "w") as f:
f.write(new_icon_definitions)
def update_icons(make_commit: bool = False):
if url is not None:
print(f"Downloading Material Design Icons from {url}")
if download_file(url, "iconic-font.zip"):
print("Archive downloaded")
else:
print("Error: Could not download archive", file=sys.stderr)
else:
print("URL is None. Do not download archive")
if os.path.exists("iconic-font.zip"):
unzip_archive("iconic-font.zip", temp_path)
print("Unzip successful")
os.remove("iconic-font.zip")
if os.path.exists(temp_repo_path):
shutil.copy2(temp_font_path, font_path)
print("Font copied")
icons, version = get_icons_list()
print(f"Version {version}. {len(icons)} icons loaded")
icon_definitions = make_icon_definitions(icons)
export_icon_definitions(icon_definitions, version)
print("File icon_definitions.py updated")
shutil.rmtree(temp_path, ignore_errors=True)
if make_commit:
git_commit(
f"Update Iconic font (v{version})",
allow_error=True,
add_files=[
"kivymd/icon_definitions.py",
"kivymd/fonts/materialdesignicons-webfont.ttf",
],
)
print("\nSuccessful. You can now push changes")
else:
print(
f'\nSuccessful. Commit message: "Update Iconic font (v{version})"'
)
else:
print(f"Error: {temp_repo_path} not exists", file=sys.stderr)
exit(1)
def main():
make_commit = "--commit" in sys.argv
if "--commit" in sys.argv:
sys.argv.remove("--commit")
update_icons(make_commit=make_commit)
if __name__ == "__main__":
main()

61
kivymd/uix/__init__.py Executable file
View file

@ -0,0 +1,61 @@
from kivy.properties import BooleanProperty
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import Screen
from kivymd.uix.behaviors import SpecificBackgroundColorBehavior
class MDAdaptiveWidget(SpecificBackgroundColorBehavior):
adaptive_height = BooleanProperty(False)
"""
If `True`, the following properties will be applied to the widget:
.. code-block:: kv
size_hint_y: None
height: self.minimum_height
:attr:`adaptive_height` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
adaptive_width = BooleanProperty(False)
"""
If `True`, the following properties will be applied to the widget:
.. code-block:: kv
size_hint_x: None
width: self.minimum_width
:attr:`adaptive_width` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
adaptive_size = BooleanProperty(False)
"""
If `True`, the following properties will be applied to the widget:
.. code-block:: kv
size_hint: None, None
size: self.minimum_size
:attr:`adaptive_size` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
def on_adaptive_height(self, instance, value):
if not isinstance(self, (FloatLayout, Screen)):
self.size_hint_y = None
self.bind(minimum_height=self.setter("height"))
def on_adaptive_width(self, instance, value):
if not isinstance(self, (FloatLayout, Screen)):
self.size_hint_x = None
self.bind(minimum_width=self.setter("width"))
def on_adaptive_size(self, instance, value):
if not isinstance(self, (FloatLayout, Screen)):
self.size_hint = (None, None)
self.bind(minimum_size=self.setter("size"))

395
kivymd/uix/backdrop.py Normal file
View file

@ -0,0 +1,395 @@
"""
Components/Backdrop
===================
.. seealso::
`Material Design spec, Backdrop <https://material.io/components/backdrop>`_
.. rubric:: Skeleton layout for using :class:`~MDBackdrop`:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop.png
:align: center
Usage
-----
.. code-block:: kv
<Root>:
MDBackdrop:
MDBackdropBackLayer:
ContentForBackdropBackLayer:
MDBackdropFrontLayer:
ContentForBackdropFrontLayer:
Example
-------
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.screenmanager import Screen
from kivymd.app import MDApp
# Your layouts.
Builder.load_string(
'''
#:import Window kivy.core.window.Window
#:import IconLeftWidget kivymd.uix.list.IconLeftWidget
<ItemBackdropFrontLayer@TwoLineAvatarListItem>
icon: "android"
IconLeftWidget:
icon: root.icon
<MyBackdropFrontLayer@ItemBackdropFrontLayer>
backdrop: None
text: "Lower the front layer"
secondary_text: " by 50 %"
icon: "transfer-down"
on_press: root.backdrop.open(-Window.height / 2)
pos_hint: {"top": 1}
_no_ripple_effect: True
<MyBackdropBackLayer@Image>
size_hint: .8, .8
source: "data/logo/kivy-icon-512.png"
pos_hint: {"center_x": .5, "center_y": .6}
'''
)
# Usage example of MDBackdrop.
Builder.load_string(
'''
<ExampleBackdrop>
MDBackdrop:
id: backdrop
left_action_items: [['menu', lambda x: self.open()]]
title: "Example Backdrop"
radius_left: "25dp"
radius_right: "0dp"
header_text: "Menu:"
MDBackdropBackLayer:
MyBackdropBackLayer:
id: backlayer
MDBackdropFrontLayer:
MyBackdropFrontLayer:
backdrop: backdrop
'''
)
class ExampleBackdrop(Screen):
pass
class TestBackdrop(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
def build(self):
return ExampleBackdrop()
TestBackdrop().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop.gif
:width: 280 px
:align: center
.. Note:: `See full example <https://github.com/kivymd/KivyMD/wiki/Components-Backdrop>`_
"""
__all__ = (
"MDBackdropToolbar",
"MDBackdropFrontLayer",
"MDBackdropBackLayer",
"MDBackdrop",
)
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
StringProperty,
)
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivymd.theming import ThemableBehavior
from kivymd.uix.card import MDCard
from kivymd.uix.toolbar import MDToolbar
Builder.load_string(
"""
<MDBackdrop>
canvas:
Color:
rgba:
root.theme_cls.primary_color if not root.background_color \
else root.background_color
Rectangle:
pos: self.pos
size: self.size
MDBackdropToolbar:
id: toolbar
title: root.title
elevation: 0
md_bg_color:
root.theme_cls.primary_color if not root.background_color \
else root.background_color
left_action_items: root.left_action_items
right_action_items: root.right_action_items
pos_hint: {'top': 1}
_BackLayer:
id: back_layer
y: -toolbar.height
padding: 0, 0, 0, toolbar.height + dp(10)
_FrontLayer:
id: _front_layer
md_bg_color: 0, 0, 0, 0
orientation: "vertical"
size_hint_y: None
height: root.height - toolbar.height
padding: root.padding
canvas:
Color:
rgba: root.theme_cls.bg_normal
RoundedRectangle:
pos: self.pos
size: self.size
radius:
[
(root.radius_left, root.radius_left),
(root.radius_right, root.radius_right),
(0, 0),
(0, 0)
]
OneLineListItem:
id: header_button
text: root.header_text
divider: None
_no_ripple_effect: True
on_press: root.open()
BoxLayout:
id: front_layer
padding: 0, 0, 0, "10dp"
"""
)
class MDBackdrop(ThemableBehavior, FloatLayout):
"""
:Events:
:attr:`on_open`
When the front layer drops.
:attr:`on_close`
When the front layer rises.
"""
padding = ListProperty([0, 0, 0, 0])
"""Padding for contents of the front layer.
:attr:`padding` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0]`.
"""
left_action_items = ListProperty()
"""The icons and methods left of the :class:`kivymd.uix.toolbar.MDToolbar`
in back layer. For more information, see the :class:`kivymd.uix.toolbar.MDToolbar` module
and :attr:`left_action_items` parameter.
:attr:`left_action_items` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
right_action_items = ListProperty()
"""Works the same way as :attr:`left_action_items`.
:attr:`right_action_items` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
title = StringProperty()
"""See the :class:`kivymd.uix.toolbar.MDToolbar.title` parameter.
:attr:`title` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
background_color = ListProperty()
"""Background color of back layer.
:attr:`background_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
radius_left = NumericProperty("16dp")
"""The value of the rounding radius of the upper left corner
of the front layer.
:attr:`radius_left` is an :class:`~kivy.properties.NumericProperty`
and defaults to `16dp`.
"""
radius_right = NumericProperty("16dp")
"""The value of the rounding radius of the upper right corner
of the front layer.
:attr:`radius_right` is an :class:`~kivy.properties.NumericProperty`
and defaults to `16dp`.
"""
header = BooleanProperty(True)
"""Whether to use a header above the contents of the front layer.
:attr:`header` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
header_text = StringProperty("Header")
"""Text of header.
:attr:`header_text` is an :class:`~kivy.properties.StringProperty`
and defaults to `'Header'`.
"""
close_icon = StringProperty("close")
"""The name of the icon that will be installed on the toolbar
on the left when opening the front layer.
:attr:`close_icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `'close'`.
"""
_open_icon = ""
_front_layer_open = False
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.register_event_type("on_open")
self.register_event_type("on_close")
Clock.schedule_once(
lambda x: self.on_left_action_items(self, self.left_action_items)
)
def on_open(self):
"""When the front layer drops."""
def on_close(self):
"""When the front layer rises."""
def on_left_action_items(self, instance, value):
if value:
self.left_action_items = [value[0]]
else:
self.left_action_items = [["menu", lambda x: self.open()]]
self._open_icon = self.left_action_items[0][0]
def on_header(self, instance, value):
if not value:
self.ids._front_layer.remove_widget(self.ids.header_button)
def open(self, open_up_to=0):
"""
Opens the front layer.
:open_up_to:
the height to which the front screen will be lowered;
if equal to zero - falls to the bottom of the screen;
"""
self.animtion_icon_menu()
if self._front_layer_open:
self.close()
return
if open_up_to:
if open_up_to < (
self.ids.header_button.height - self.ids._front_layer.height
):
y = self.ids.header_button.height - self.ids._front_layer.height
elif open_up_to > 0:
y = 0
else:
y = open_up_to
else:
y = self.ids.header_button.height - self.ids._front_layer.height
Animation(y=y, d=0.2, t="out_quad").start(self.ids._front_layer)
self._front_layer_open = True
self.dispatch("on_open")
def close(self):
"""Opens the front layer."""
Animation(y=0, d=0.2, t="out_quad").start(self.ids._front_layer)
self._front_layer_open = False
self.dispatch("on_close")
def animtion_icon_menu(self):
icon_menu = self.ids.toolbar.ids.left_actions.children[0]
anim = Animation(opacity=0, d=0.2, t="out_quad")
anim.bind(on_complete=self.animtion_icon_close)
anim.start(icon_menu)
def animtion_icon_close(self, instance_animation, instance_icon_menu):
instance_icon_menu.icon = (
self.close_icon
if instance_icon_menu.icon == self._open_icon
else self._open_icon
)
Animation(opacity=1, d=0.2).start(instance_icon_menu)
def add_widget(self, widget, index=0, canvas=None):
if widget.__class__ in (MDBackdropToolbar, _BackLayer, _FrontLayer):
return super().add_widget(widget)
else:
if widget.__class__ is MDBackdropBackLayer:
self.ids.back_layer.add_widget(widget)
elif widget.__class__ is MDBackdropFrontLayer:
self.ids.front_layer.add_widget(widget)
class MDBackdropToolbar(MDToolbar):
pass
class MDBackdropFrontLayer(BoxLayout):
pass
class MDBackdropBackLayer(BoxLayout):
pass
class _BackLayer(BoxLayout):
pass
class _FrontLayer(MDCard):
pass

455
kivymd/uix/banner.py Normal file
View file

@ -0,0 +1,455 @@
"""
Components/Banner
=================
.. seealso::
`Material Design spec, Banner <https://material.io/components/banners>`_
.. rubric:: A banner displays a prominent message and related optional actions.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner.png
:align: center
Usage
=====
.. code-block:: python
from kivy.lang import Builder
from kivy.factory import Factory
from kivymd.app import MDApp
Builder.load_string('''
<ExampleBanner@Screen>
MDBanner:
id: banner
text: ["One line string text example without actions."]
# The widget that is under the banner.
# It will be shifted down to the height of the banner.
over_widget: screen
vertical_pad: toolbar.height
MDToolbar:
id: toolbar
title: "Example Banners"
elevation: 10
pos_hint: {'top': 1}
BoxLayout:
id: screen
orientation: "vertical"
size_hint_y: None
height: Window.height - toolbar.height
OneLineListItem:
text: "Banner without actions"
on_release: banner.show()
Widget:
''')
class Test(MDApp):
def build(self):
return Factory.ExampleBanner()
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-example-1.gif
:align: center
.. rubric:: Banner type.
By default, the banner is of the type ``'one-line'``:
.. code-block:: kv
MDBanner:
text: ["One line string text example without actions."]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-one-line.png
:align: center
To use a two-line banner, specify the ``'two-line'`` :attr:`MDBanner.type` for the banner
and pass the list of two lines to the :attr:`MDBanner.text` parameter:
.. code-block:: kv
MDBanner:
type: "two-line"
text:
["One line string text example without actions.", "This is the second line of the banner message."]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-two-line.png
:align: center
Similarly, create a three-line banner:
.. code-block:: kv
MDBanner:
type: "three-line"
text:
["One line string text example without actions.", "This is the second line of the banner message." "and this is the third line of the banner message."]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-three-line.png
:align: center
To add buttons to any type of banner,
use the :attr:`MDBanner.left_action` and :attr:`MDBanner.right_action` parameters,
which should take a list ['Button name', function]:
.. code-block:: kv
MDBanner:
text: ["One line string text example without actions."]
left_action: ["CANCEL", lambda x: None]
Or two buttons:
.. code-block:: kv
MDBanner:
text: ["One line string text example without actions."]
left_action: ["CANCEL", lambda x: None]
right_action: ["CLOSE", lambda x: None]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-actions.png
:align: center
If you want to use the icon on the left in the banner,
add the prefix `'-icon'` to the banner type:
.. code-block:: kv
MDBanner:
type: "one-line-icon"
icon: f"{images_path}/kivymd.png"
text: ["One line string text example without actions."]
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/banner-icon.png
:align: center
.. Note:: `See full example <https://github.com/kivymd/KivyMD/wiki/Components-Banner>`_
"""
__all__ = ("MDBanner",)
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.widget import Widget
from kivymd.uix.button import MDFlatButton
from kivymd.uix.card import MDCard
from kivymd.uix.list import (
OneLineAvatarListItem,
OneLineListItem,
ThreeLineAvatarListItem,
ThreeLineListItem,
TwoLineAvatarListItem,
TwoLineListItem,
)
Builder.load_string(
"""
#:import Window kivy.core.window.Window
#:import Clock kivy.clock.Clock
<ThreeLineIconBanner>
text: root.text_message[0]
secondary_text: root.text_message[1]
tertiary_text: root.text_message[2]
divider: None
_no_ripple_effect: True
ImageLeftWidget:
source: root.icon
<TwoLineIconBanner>
text: root.text_message[0]
secondary_text: root.text_message[1]
divider: None
_no_ripple_effect: True
ImageLeftWidget:
source: root.icon
<OneLineIconBanner>
text: root.text_message[0]
divider: None
_no_ripple_effect: True
ImageLeftWidget:
source: root.icon
<ThreeLineBanner>
text: root.text_message[0]
secondary_text: root.text_message[1]
tertiary_text: root.text_message[2]
divider: None
_no_ripple_effect: True
<TwoLineBanner>
text: root.text_message[0]
secondary_text: root.text_message[1]
divider: None
_no_ripple_effect: True
<OneLineBanner>
text: root.text_message[0]
divider: None
_no_ripple_effect: True
<MDBanner>
size_hint_y: None
height: self.minimum_height
banner_y: 0
orientation: "vertical"
y: Window.height - self.banner_y
canvas:
Color:
rgba: 0, 0, 0, 0
Rectangle:
pos: self.pos
size: self.size
BoxLayout:
id: container_message
size_hint_y: None
height: self.minimum_height
BoxLayout:
size_hint: None, None
size: self.minimum_size
pos_hint: {"right": 1}
padding: 0, 0, "8dp", "8dp"
spacing: "8dp"
BoxLayout:
id: left_action_box
size_hint: None, None
size: self.minimum_size
BoxLayout:
id: right_action_box
size_hint: None, None
size: self.minimum_size
"""
)
class MDBanner(MDCard):
vertical_pad = NumericProperty(dp(68))
"""
Indent the banner at the top of the screen.
:attr:`vertical_pad` is an :class:`~kivy.properties.NumericProperty`
and defaults to `dp(68)`.
"""
opening_transition = StringProperty("in_quad")
"""
The name of the animation transition.
:attr:`opening_transition` is an :class:`~kivy.properties.StringProperty`
and defaults to `'in_quad'`.
"""
icon = StringProperty("data/logo/kivy-icon-128.png")
"""Icon banner.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `'data/logo/kivy-icon-128.png'`.
"""
over_widget = ObjectProperty()
"""
The widget that is under the banner.
It will be shifted down to the height of the banner.
:attr:`over_widget` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
text = ListProperty()
"""List of lines for banner text.
Must contain no more than three lines for a
`'one-line'`, `'two-line'` and `'three-line'` banner, respectively.
:attr:`text` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
left_action = ListProperty()
"""The action of banner.
To add one action, make a list [`'name_action'`, callback]
where `'name_action'` is a string that corresponds to an action name and
``callback`` is the function called on a touch release event.
:attr:`left_action` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
right_action = ListProperty()
"""Works the same way as :attr:`left_action`.
:attr:`right_action` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
type = OptionProperty(
"one-line",
options=[
"one-line",
"two-line",
"three-line",
"one-line-icon",
"two-line-icon",
"three-line-icon",
],
allownone=True,
)
"""Banner type. . Available options are: (`"one-line"`, `"two-line"`,
`"three-line"`, `"one-line-icon"`, `"two-line-icon"`, `"three-line-icon"`).
:attr:`type` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'one-line'`.
"""
_type_message = None
_progress = False
def add_actions_buttons(self, box, data):
if data:
name_action_button, function_action_button = data
action_button = MDFlatButton(
text=f"[b]{name_action_button}[/b]",
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
on_release=function_action_button,
)
action_button.markup = True
box.add_widget(action_button)
def set_left_action(self):
self.add_actions_buttons(self.ids.left_action_box, self.left_action)
def set_right_action(self):
self.add_actions_buttons(self.ids.right_action_box, self.right_action)
def set_type_banner(self):
self._type_message = {
"three-line-icon": ThreeLineIconBanner,
"two-line-icon": TwoLineIconBanner,
"one-line-icon": OneLineIconBanner,
"three-line": ThreeLineBanner,
"two-line": TwoLineBanner,
"one-line": OneLineBanner,
}[self.type]
def add_banner_to_container(self):
self.ids.container_message.add_widget(
self._type_message(text_message=self.text, icon=self.icon)
)
def show(self):
def show(interval):
self.set_type_banner()
self.set_left_action()
self.set_right_action()
self.add_banner_to_container()
Clock.schedule_once(self.animation_display_banner, 0.1)
if self._progress:
return
self._progress = True
if self.ids.container_message.children:
self.hide()
Clock.schedule_once(show, 0.7)
def animation_display_banner(self, i):
Animation(
banner_y=self.height + self.vertical_pad,
d=0.15,
t=self.opening_transition,
).start(self)
anim = Animation(
y=self.over_widget.y - self.height,
d=0.15,
t=self.opening_transition,
)
anim.bind(on_complete=self._reset_progress)
anim.start(self.over_widget)
def hide(self):
def hide(interval):
anim = Animation(banner_y=0, d=0.15)
anim.bind(on_complete=self._remove_banner)
anim.start(self)
Animation(y=self.over_widget.y + self.height, d=0.15).start(
self.over_widget
)
Clock.schedule_once(hide, 0.5)
def _remove_banner(self, *args):
self.ids.container_message.clear_widgets()
self.ids.left_action_box.clear_widgets()
self.ids.right_action_box.clear_widgets()
def _reset_progress(self, *args):
self._progress = False
class BaseBanner(Widget):
text_message = ListProperty(["", "", ""])
icon = StringProperty()
def on_touch_down(self, touch):
self.parent.parent.hide()
class ThreeLineIconBanner(ThreeLineAvatarListItem, BaseBanner):
pass
class TwoLineIconBanner(TwoLineAvatarListItem, BaseBanner):
pass
class OneLineIconBanner(OneLineAvatarListItem, BaseBanner):
pass
class ThreeLineBanner(ThreeLineListItem, BaseBanner):
pass
class TwoLineBanner(TwoLineListItem, BaseBanner):
pass
class OneLineBanner(OneLineListItem, BaseBanner):
pass

View file

@ -0,0 +1,22 @@
"""
Behaviors
=========
Modules and classes implementing various behaviors for buttons etc.
"""
# flake8: NOQA
from .hover_behavior import HoverBehavior # isort:skip
from .backgroundcolorbehavior import (
BackgroundColorBehavior,
SpecificBackgroundColorBehavior,
)
from .elevation import (
CircularElevationBehavior,
CommonElevationBehavior,
RectangularElevationBehavior,
)
from .focus_behavior import FocusBehavior
from .magic_behavior import MagicBehavior
from .ripplebehavior import CircularRippleBehavior, RectangularRippleBehavior
from .touch_behavior import TouchBehavior

View file

@ -0,0 +1,168 @@
"""
Behaviors/Background Color
==========================
.. note:: The following classes are intended for in-house use of the library.
"""
__all__ = ("BackgroundColorBehavior", "SpecificBackgroundColorBehavior")
from kivy.lang import Builder
from kivy.properties import (
BoundedNumericProperty,
ListProperty,
OptionProperty,
ReferenceListProperty,
)
from kivy.uix.widget import Widget
from kivy.utils import get_color_from_hex
from kivymd.color_definitions import hue, palette, text_colors
Builder.load_string(
"""
#:import RelativeLayout kivy.uix.relativelayout.RelativeLayout
<BackgroundColorBehavior>
canvas:
Color:
rgba: self.md_bg_color
RoundedRectangle:
size: self.size
pos: self.pos if not isinstance(self, RelativeLayout) else (0, 0)
radius: root.radius
"""
)
class BackgroundColorBehavior(Widget):
r = BoundedNumericProperty(1.0, min=0.0, max=1.0)
"""The value of ``red`` in the ``rgba`` palette.
:attr:`r` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `1.0`.
"""
g = BoundedNumericProperty(1.0, min=0.0, max=1.0)
"""The value of ``green`` in the ``rgba`` palette.
:attr:`g` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `1.0`.
"""
b = BoundedNumericProperty(1.0, min=0.0, max=1.0)
"""The value of ``blue`` in the ``rgba`` palette.
:attr:`b` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `1.0`.
"""
a = BoundedNumericProperty(0.0, min=0.0, max=1.0)
"""The value of ``alpha channel`` in the ``rgba`` palette.
:attr:`a` is an :class:`~kivy.properties.BoundedNumericProperty`
and defaults to `0.0`.
"""
radius = ListProperty([0, 0, 0, 0])
"""Canvas radius.
.. code-block:: python
# Top left corner slice.
MDBoxLayout:
md_bg_color: app.theme_cls.primary_color
radius: [25, 0, 0, 0]
:attr:`radius` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0]`.
"""
md_bg_color = ReferenceListProperty(r, g, b, a)
"""The background color of the widget (:class:`~kivy.uix.widget.Widget`)
that will be inherited from the :attr:`BackgroundColorBehavior` class.
For example:
.. code-block:: kv
Widget:
canvas:
Color:
rgba: 0, 1, 1, 1
Rectangle:
size: self.size
pos: self.pos
similar to code:
.. code-block:: kv
<MyWidget@BackgroundColorBehavior>
md_bg_color: 0, 1, 1, 1
:attr:`md_bg_color` is an :class:`~kivy.properties.ReferenceListProperty`
and defaults to :attr:`r`, :attr:`g`, :attr:`b`, :attr:`a`.
"""
class SpecificBackgroundColorBehavior(BackgroundColorBehavior):
background_palette = OptionProperty(
"Primary", options=["Primary", "Accent", *palette]
)
"""See :attr:`kivymd.color_definitions.palette`.
:attr:`background_palette` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'Primary'`.
"""
background_hue = OptionProperty("500", options=hue)
"""See :attr:`kivymd.color_definitions.hue`.
:attr:`background_hue` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'500'`.
"""
specific_text_color = ListProperty([0, 0, 0, 0.87])
""":attr:`specific_text_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0.87]`.
"""
specific_secondary_text_color = ListProperty([0, 0, 0, 0.87])
""":attr:`specific_secondary_text_color`is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0.87]`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
if hasattr(self, "theme_cls"):
self.theme_cls.bind(
primary_palette=self._update_specific_text_color
)
self.theme_cls.bind(accent_palette=self._update_specific_text_color)
self.theme_cls.bind(theme_style=self._update_specific_text_color)
self.bind(background_hue=self._update_specific_text_color)
self.bind(background_palette=self._update_specific_text_color)
self._update_specific_text_color(None, None)
def _update_specific_text_color(self, instance, value):
if hasattr(self, "theme_cls"):
palette = {
"Primary": self.theme_cls.primary_palette,
"Accent": self.theme_cls.accent_palette,
}.get(self.background_palette, self.background_palette)
else:
palette = {"Primary": "Blue", "Accent": "Amber"}.get(
self.background_palette, self.background_palette
)
color = get_color_from_hex(text_colors[palette][self.background_hue])
secondary_color = color[:]
# Check for black text (need to adjust opacity).
if (color[0] + color[1] + color[2]) == 0:
color[3] = 0.87
secondary_color[3] = 0.54
else:
secondary_color[3] = 0.7
self.specific_text_color = color
self.specific_secondary_text_color = secondary_color

303
kivymd/uix/behaviors/elevation.py Executable file
View file

@ -0,0 +1,303 @@
"""
Behaviors/Elevation
===================
.. rubric:: Classes implements a circular and rectangular elevation effects.
To create a widget with rectangular or circular elevation effect,
you must create a new class that inherits from the
:class:`~RectangularElevationBehavior` or :class:`~CircularElevationBehavior`
class.
For example, let's create an button with a rectangular elevation effect:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import (
RectangularRippleBehavior,
BackgroundColorBehavior,
RectangularElevationBehavior,
)
KV = '''
<RectangularElevationButton>:
size_hint: None, None
size: "250dp", "50dp"
Screen:
# With elevation effect
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 11
# Without elevation effect
RectangularElevationButton:
pos_hint: {"center_x": .5, "center_y": .4}
'''
class RectangularElevationButton(
RectangularRippleBehavior,
RectangularElevationBehavior,
ButtonBehavior,
BackgroundColorBehavior,
):
md_bg_color = [0, 0, 1, 1]
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-elevation-effect.gif
:align: center
Similarly, create a button with a circular elevation effect:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.image import Image
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import (
CircularRippleBehavior,
CircularElevationBehavior,
)
KV = '''
#:import images_path kivymd.images_path
<CircularElevationButton>:
size_hint: None, None
size: "100dp", "100dp"
source: f"{images_path}/kivymd.png"
Screen:
# With elevation effect
CircularElevationButton:
pos_hint: {"center_x": .5, "center_y": .6}
elevation: 5
# Without elevation effect
CircularElevationButton:
pos_hint: {"center_x": .5, "center_y": .4}
elevation: 0
'''
class CircularElevationButton(
CircularRippleBehavior,
CircularElevationBehavior,
ButtonBehavior,
Image,
):
md_bg_color = [0, 0, 1, 1]
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/circular-elevation-effect.gif
:align: center
"""
__all__ = (
"CommonElevationBehavior",
"RectangularElevationBehavior",
"CircularElevationBehavior",
)
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import ListProperty, NumericProperty, ObjectProperty
from kivymd.app import MDApp
Builder.load_string(
"""
<RectangularElevationBehavior>
canvas.before:
Color:
a: self._soft_shadow_a
Rectangle:
texture: self._soft_shadow_texture
size: self._soft_shadow_size
pos: self._soft_shadow_pos
Color:
a: self._hard_shadow_a
Rectangle:
texture: self._hard_shadow_texture
size: self._hard_shadow_size
pos: self._hard_shadow_pos
Color:
a: 1
<CircularElevationBehavior>
canvas.before:
Color:
a: self._soft_shadow_a
Rectangle:
texture: self._soft_shadow_texture
size: self._soft_shadow_size
pos: self._soft_shadow_pos
Color:
a: self._hard_shadow_a
Rectangle:
texture: self._hard_shadow_texture
size: self._hard_shadow_size
pos: self._hard_shadow_pos
Color:
a: 1
"""
)
class CommonElevationBehavior(object):
"""Common base class for rectangular and circular elevation behavior."""
elevation = NumericProperty(1)
"""
Elevation value.
:attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
and defaults to 1.
"""
_elevation = NumericProperty(0)
_soft_shadow_texture = ObjectProperty()
_soft_shadow_size = ListProperty((0, 0))
_soft_shadow_pos = ListProperty((0, 0))
_soft_shadow_a = NumericProperty(0)
_hard_shadow_texture = ObjectProperty()
_hard_shadow_size = ListProperty((0, 0))
_hard_shadow_pos = ListProperty((0, 0))
_hard_shadow_a = NumericProperty(0)
def __init__(self, **kwargs):
self.bind(
elevation=self._update_elevation,
pos=self._update_shadow,
size=self._update_shadow,
)
super().__init__(**kwargs)
def _update_shadow(self, *args):
raise NotImplementedError
def _update_elevation(self, instance, value):
if not self._elevation:
self._elevation = value
self._update_shadow(instance, value)
class RectangularElevationBehavior(CommonElevationBehavior):
"""Base class for rectangular elevation behavior.
Controls the size and position of the shadow."""
def _update_shadow(self, *args):
if self._elevation > 0:
# Set shadow size.
ratio = self.width / (self.height if self.height != 0 else 1)
if -2 < ratio < 2:
self._shadow = MDApp.get_running_app().theme_cls.quad_shadow
width = soft_width = self.width * 1.9
height = soft_height = self.height * 1.9
elif ratio <= -2:
self._shadow = MDApp.get_running_app().theme_cls.rec_st_shadow
ratio = abs(ratio)
if ratio > 5:
ratio = ratio * 22
else:
ratio = ratio * 11.5
width = soft_width = self.width * 1.9
height = self.height + dp(ratio)
soft_height = (
self.height + dp(ratio) + dp(self._elevation) * 0.5
)
else:
self._shadow = MDApp.get_running_app().theme_cls.quad_shadow
width = soft_width = self.width * 1.8
height = soft_height = self.height * 1.8
self._soft_shadow_size = (soft_width, soft_height)
self._hard_shadow_size = (width, height)
# Set ``soft_shadow`` parameters.
self._soft_shadow_pos = (
self.center_x - soft_width / 2,
self.center_y
- soft_height / 2
- dp(0.1 * 1.5 ** self._elevation),
)
self._soft_shadow_a = 0.1 * 1.1 ** self._elevation
self._soft_shadow_texture = self._shadow.textures[
str(int(round(self._elevation)))
]
# Set ``hard_shadow`` parameters.
self._hard_shadow_pos = (
self.center_x - width / 2,
self.center_y - height / 2 - dp(0.5 * 1.18 ** self._elevation),
)
self._hard_shadow_a = 0.4 * 0.9 ** self._elevation
self._hard_shadow_texture = self._shadow.textures[
str(int(round(self._elevation)))
]
else:
self._soft_shadow_a = 0
self._hard_shadow_a = 0
class CircularElevationBehavior(CommonElevationBehavior):
"""Base class for circular elevation behavior.
Controls the size and position of the shadow."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._shadow = MDApp.get_running_app().theme_cls.round_shadow
def _update_shadow(self, *args):
if self.elevation > 0:
# Set shadow size.
width = self.width * 2
height = self.height * 2
x = self.center_x - width / 2
self._soft_shadow_size = (width, height)
self._hard_shadow_size = (width, height)
# Set ``soft_shadow`` parameters.
y = self.center_y - height / 2 - dp(0.1 * 1.5 ** self._elevation)
self._soft_shadow_pos = (x, y)
self._soft_shadow_a = 0.1 * 1.1 ** self._elevation
if hasattr(self, "_shadow"):
self._soft_shadow_texture = self._shadow.textures[
str(int(round(self._elevation)))
]
# Set ``hard_shadow`` parameters.
y = self.center_y - height / 2 - dp(0.5 * 1.18 ** self._elevation)
self._hard_shadow_pos = (x, y)
self._hard_shadow_a = 0.4 * 0.9 ** self._elevation
if hasattr(self, "_shadow"):
self._hard_shadow_texture = self._shadow.textures[
str(int(round(self._elevation)))
]
else:
self._soft_shadow_a = 0
self._hard_shadow_a = 0

View file

@ -0,0 +1,122 @@
"""
Behaviors/Focus
===============
.. rubric:: Changing the background color when the mouse is on the widget.
To apply focus behavior, you must create a new class that is inherited from the
widget to which you apply the behavior and from the :class:`FocusBehavior` class.
Usage
-----
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import RectangularElevationBehavior, FocusBehavior
from kivymd.uix.boxlayout import MDBoxLayout
KV = '''
MDScreen:
md_bg_color: 1, 1, 1, 1
FocusWidget:
size_hint: .5, .3
pos_hint: {"center_x": .5, "center_y": .5}
md_bg_color: app.theme_cls.bg_light
MDLabel:
text: "Label"
theme_text_color: "Primary"
pos_hint: {"center_y": .5}
halign: "center"
'''
class FocusWidget(MDBoxLayout, RectangularElevationBehavior, FocusBehavior):
pass
class Test(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/focus-widget.gif
:align: center
Color change at focus/defocus
.. code-block:: kv
FocusWidget:
focus_color: 1, 0, 1, 1
unfocus_color: 0, 0, 1, 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/focus-defocus-color.gif
:align: center
"""
__all__ = ("FocusBehavior",)
from kivy.app import App
from kivy.properties import BooleanProperty, ListProperty
from kivy.uix.behaviors import ButtonBehavior
from kivymd.uix.behaviors import HoverBehavior
class FocusBehavior(HoverBehavior, ButtonBehavior):
focus_behavior = BooleanProperty(True)
"""
Using focus when hovering over a widget.
:attr:`focus_behavior` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
focus_color = ListProperty()
"""
The color of the widget when the mouse enters the bbox of the widget.
:attr:`focus_color` is a :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
unfocus_color = ListProperty()
"""
The color of the widget when the mouse exits the bbox widget.
:attr:`unfocus_color` is a :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
def on_enter(self):
"""Called when mouse enter the bbox of the widget."""
if hasattr(self, "md_bg_color") and self.focus_behavior:
if hasattr(self, "theme_cls") and not self.focus_color:
self.md_bg_color = self.theme_cls.bg_normal
else:
if not self.focus_color:
self.md_bg_color = App.get_running_app().theme_cls.bg_normal
else:
self.md_bg_color = self.focus_color
def on_leave(self):
"""Called when the mouse exit the widget."""
if hasattr(self, "md_bg_color") and self.focus_behavior:
if hasattr(self, "theme_cls") and not self.unfocus_color:
self.md_bg_color = self.theme_cls.bg_light
else:
if not self.unfocus_color:
self.md_bg_color = App.get_running_app().theme_cls.bg_light
else:
self.md_bg_color = self.unfocus_color

View file

@ -0,0 +1,141 @@
"""
Behaviors/Hover
===============
.. rubric:: Changing when the mouse is on the widget.
To apply hover behavior, you must create a new class that is inherited from the
widget to which you apply the behavior and from the :attr:`HoverBehavior` class.
In `KV file`:
.. code-block:: kv
<HoverItem@MDBoxLayout+ThemableBehavior+HoverBehavior>
In `python file`:
.. code-block:: python
class HoverItem(MDBoxLayout, ThemableBehavior, HoverBehavior):
'''Custom item implementing hover behavior.'''
After creating a class, you must define two methods for it:
:attr:`HoverBehavior.on_enter` and :attr:`HoverBehavior.on_leave`, which will be automatically called
when the mouse cursor is over the widget and when the mouse cursor goes beyond
the widget.
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import HoverBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.theming import ThemableBehavior
KV = '''
Screen
MDBoxLayout:
id: box
pos_hint: {'center_x': .5, 'center_y': .5}
size_hint: .8, .8
md_bg_color: app.theme_cls.bg_darkest
'''
class HoverItem(MDBoxLayout, ThemableBehavior, HoverBehavior):
'''Custom item implementing hover behavior.'''
def on_enter(self, *args):
'''The method will be called when the mouse cursor
is within the borders of the current widget.'''
self.md_bg_color = (1, 1, 1, 1)
def on_leave(self, *args):
'''The method will be called when the mouse cursor goes beyond
the borders of the current widget.'''
self.md_bg_color = self.theme_cls.bg_darkest
class Test(MDApp):
def build(self):
self.screen = Builder.load_string(KV)
for i in range(5):
self.screen.ids.box.add_widget(HoverItem())
return self.screen
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hover-behavior.gif
:width: 250 px
:align: center
"""
__all__ = ("HoverBehavior",)
from kivy.core.window import Window
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty
class HoverBehavior(object):
"""
:Events:
:attr:`on_enter`
Call when mouse enter the bbox of the widget.
:attr:`on_leave`
Call when the mouse exit the widget.
"""
hovered = BooleanProperty(False)
"""
`True`, if the mouse cursor is within the borders of the widget.
:attr:`hovered` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
border_point = ObjectProperty(None)
"""Contains the last relevant point received by the Hoverable.
This can be used in :attr:`on_enter` or :attr:`on_leave` in order
to know where was dispatched the event.
:attr:`border_point` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
def __init__(self, **kwargs):
self.register_event_type("on_enter")
self.register_event_type("on_leave")
Window.bind(mouse_pos=self.on_mouse_pos)
super(HoverBehavior, self).__init__(**kwargs)
def on_mouse_pos(self, *args):
if not self.get_root_window():
return # do proceed if I'm not displayed <=> If have no parent
pos = args[1]
# Next line to_widget allow to compensate for relative layout
inside = self.collide_point(*self.to_widget(*pos))
if self.hovered == inside:
# We have already done what was needed
return
self.border_point = pos
self.hovered = inside
if inside:
self.dispatch("on_enter")
else:
self.dispatch("on_leave")
def on_enter(self):
"""Call when mouse enter the bbox of the widget."""
def on_leave(self):
"""Call when the mouse exit the widget."""
Factory.register("HoverBehavior", HoverBehavior)

View file

@ -0,0 +1,172 @@
"""
Behaviors/Magic
===============
.. rubric:: Magical effects for buttons.
.. warning:: Magic effects do not work correctly with `KivyMD` buttons!
To apply magic effects, you must create a new class that is inherited from the
widget to which you apply the effect and from the :attr:`MagicBehavior` class.
In `KV file`:
.. code-block:: kv
<MagicButton@MagicBehavior+MDRectangleFlatButton>
In `python file`:
.. code-block:: python
class MagicButton(MagicBehavior, MDRectangleFlatButton):
pass
.. rubric:: The :attr:`MagicBehavior` class provides five effects:
- :attr:`MagicBehavior.wobble`
- :attr:`MagicBehavior.grow`
- :attr:`MagicBehavior.shake`
- :attr:`MagicBehavior.twist`
- :attr:`MagicBehavior.shrink`
Example:
.. code-block:: python
from kivymd.app import MDApp
from kivy.lang import Builder
KV = '''
#:import MagicBehavior kivymd.uix.behaviors.MagicBehavior
<MagicButton@MagicBehavior+MDRectangleFlatButton>
FloatLayout:
MagicButton:
text: "WOBBLE EFFECT"
on_release: self.wobble()
pos_hint: {"center_x": .5, "center_y": .3}
MagicButton:
text: "GROW EFFECT"
on_release: self.grow()
pos_hint: {"center_x": .5, "center_y": .4}
MagicButton:
text: "SHAKE EFFECT"
on_release: self.shake()
pos_hint: {"center_x": .5, "center_y": .5}
MagicButton:
text: "TWIST EFFECT"
on_release: self.twist()
pos_hint: {"center_x": .5, "center_y": .6}
MagicButton:
text: "SHRINK EFFECT"
on_release: self.shrink()
pos_hint: {"center_x": .5, "center_y": .7}
'''
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/magic-button.gif
:width: 250 px
:align: center
"""
__all__ = ("MagicBehavior",)
from kivy.animation import Animation
from kivy.factory import Factory
from kivy.lang import Builder
Builder.load_string(
"""
<MagicBehavior>
translate_x: 0
translate_y: 0
scale_x: 1
scale_y: 1
rotate: 0
canvas.before:
PushMatrix
Translate:
x: self.translate_x or 0
y: self.translate_y or 0
Rotate:
origin: self.center
angle: self.rotate or 0
Scale:
origin: self.center
x: self.scale_x or 1
y: self.scale_y or 1
canvas.after:
PopMatrix
"""
)
class MagicBehavior:
def grow(self):
"""Grow effect animation."""
Animation.stop_all(self)
(
Animation(scale_x=1.2, scale_y=1.2, t="out_quad", d=0.03)
+ Animation(scale_x=1, scale_y=1, t="out_elastic", d=0.4)
).start(self)
def shake(self):
"""Shake effect animation."""
Animation.stop_all(self)
(
Animation(translate_x=50, t="out_quad", d=0.02)
+ Animation(translate_x=0, t="out_elastic", d=0.5)
).start(self)
def wobble(self):
"""Wobble effect animation."""
Animation.stop_all(self)
(
(
Animation(scale_y=0.7, t="out_quad", d=0.03)
& Animation(scale_x=1.4, t="out_quad", d=0.03)
)
+ (
Animation(scale_y=1, t="out_elastic", d=0.5)
& Animation(scale_x=1, t="out_elastic", d=0.4)
)
).start(self)
def twist(self):
"""Twist effect animation."""
Animation.stop_all(self)
(
Animation(rotate=25, t="out_quad", d=0.05)
+ Animation(rotate=0, t="out_elastic", d=0.5)
).start(self)
def shrink(self):
"""Shrink effect animation."""
Animation.stop_all(self)
Animation(scale_x=0.95, scale_y=0.95, t="out_quad", d=0.1).start(self)
Factory.register("MagicBehavior", cls=MagicBehavior)

View file

@ -0,0 +1,391 @@
"""
Behaviors/Ripple
================
.. rubric:: Classes implements a circular and rectangular ripple effects.
To create a widget with сircular ripple effect, you must create a new class
that inherits from the :class:`~CircularRippleBehavior` class.
For example, let's create an image button with a circular ripple effect:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.image import Image
from kivymd.app import MDApp
from kivymd.uix.behaviors import CircularRippleBehavior
KV = '''
#:import images_path kivymd.images_path
Screen:
CircularRippleButton:
source: f"{images_path}/kivymd.png"
size_hint: None, None
size: "250dp", "250dp"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class CircularRippleButton(CircularRippleBehavior, ButtonBehavior, Image):
def __init__(self, **kwargs):
self.ripple_scale = 0.85
super().__init__(**kwargs)
class Example(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/circular-ripple-effect.gif
:align: center
To create a widget with rectangular ripple effect, you must create a new class
that inherits from the :class:`~RectangularRippleBehavior` class:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.behaviors import ButtonBehavior
from kivymd.app import MDApp
from kivymd.uix.behaviors import RectangularRippleBehavior, BackgroundColorBehavior
KV = '''
Screen:
RectangularRippleButton:
size_hint: None, None
size: "250dp", "50dp"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class RectangularRippleButton(
RectangularRippleBehavior, ButtonBehavior, BackgroundColorBehavior
):
md_bg_color = [0, 0, 1, 1]
class Example(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/rectangular-ripple-effect.gif
:align: center
"""
__all__ = (
"CommonRipple",
"RectangularRippleBehavior",
"CircularRippleBehavior",
)
from kivy.animation import Animation
from kivy.graphics import (
Color,
Ellipse,
Rectangle,
StencilPop,
StencilPush,
StencilUnUse,
StencilUse,
)
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
StringProperty,
)
class CommonRipple(object):
"""Base class for ripple effect."""
ripple_rad_default = NumericProperty(1)
"""
Default value of the ripple effect radius.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-rad-default.gif
:align: center
:attr:`ripple_rad_default` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
ripple_color = ListProperty()
"""
Ripple color in ``rgba`` format.
:attr:`ripple_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
ripple_alpha = NumericProperty(0.5)
"""
Alpha channel values for ripple effect.
:attr:`ripple_alpha` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.5`.
"""
ripple_scale = NumericProperty(None)
"""
Ripple effect scale.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-scale-1.gif
:align: center
:attr:`ripple_scale` is an :class:`~kivy.properties.NumericProperty`
and defaults to `None`.
"""
ripple_duration_in_fast = NumericProperty(0.3)
"""
Ripple duration when touching to widget.
:attr:`ripple_duration_in_fast` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.3`.
"""
ripple_duration_in_slow = NumericProperty(2)
"""
Ripple duration when long touching to widget.
:attr:`ripple_duration_in_slow` is an :class:`~kivy.properties.NumericProperty`
and defaults to `2`.
"""
ripple_duration_out = NumericProperty(0.3)
"""
The duration of the disappearance of the wave effect.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-duration-out.gif
:align: center
:attr:`ripple_duration_out` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.3`.
"""
ripple_func_in = StringProperty("out_quad")
"""
Type of animation for ripple in effect.
:attr:`ripple_func_in` is an :class:`~kivy.properties.StringProperty`
and defaults to `'out_quad'`.
"""
ripple_func_out = StringProperty("out_quad")
"""
Type of animation for ripple out effect.
:attr:`ripple_func_in` is an :class:`~kivy.properties.StringProperty`
and defaults to `'ripple_func_out'`.
"""
_ripple_rad = NumericProperty()
_doing_ripple = BooleanProperty(False)
_finishing_ripple = BooleanProperty(False)
_fading_out = BooleanProperty(False)
_no_ripple_effect = BooleanProperty(False)
def lay_canvas_instructions(self):
raise NotImplementedError
def start_ripple(self):
if not self._doing_ripple:
self._doing_ripple = True
anim = Animation(
_ripple_rad=self.finish_rad,
t="linear",
duration=self.ripple_duration_in_slow,
)
anim.bind(on_complete=self.fade_out)
anim.start(self)
def finish_ripple(self):
if self._doing_ripple and not self._finishing_ripple:
self._finishing_ripple = True
self._doing_ripple = False
Animation.cancel_all(self, "_ripple_rad")
anim = Animation(
_ripple_rad=self.finish_rad,
t=self.ripple_func_in,
duration=self.ripple_duration_in_fast,
)
anim.bind(on_complete=self.fade_out)
anim.start(self)
def fade_out(self, *args):
rc = self.ripple_color
if not self._fading_out:
self._fading_out = True
Animation.cancel_all(self, "ripple_color")
anim = Animation(
ripple_color=[rc[0], rc[1], rc[2], 0.0],
t=self.ripple_func_out,
duration=self.ripple_duration_out,
)
anim.bind(on_complete=self.anim_complete)
anim.start(self)
def anim_complete(self, *args):
self._doing_ripple = False
self._finishing_ripple = False
self._fading_out = False
self.canvas.after.clear()
def on_touch_down(self, touch):
if touch.is_mouse_scrolling:
return False
if not self.collide_point(touch.x, touch.y):
return False
if not self.disabled:
if self._doing_ripple:
Animation.cancel_all(
self, "_ripple_rad", "ripple_color", "rect_color"
)
self.anim_complete()
self._ripple_rad = self.ripple_rad_default
self.ripple_pos = (touch.x, touch.y)
if self.ripple_color:
pass
elif hasattr(self, "theme_cls"):
self.ripple_color = self.theme_cls.ripple_color
else:
# If no theme, set Gray 300.
self.ripple_color = [
0.8784313725490196,
0.8784313725490196,
0.8784313725490196,
self.ripple_alpha,
]
self.ripple_color[3] = self.ripple_alpha
self.lay_canvas_instructions()
self.finish_rad = max(self.width, self.height) * self.ripple_scale
self.start_ripple()
return super().on_touch_down(touch)
def on_touch_move(self, touch, *args):
if not self.collide_point(touch.x, touch.y):
if not self._finishing_ripple and self._doing_ripple:
self.finish_ripple()
return super().on_touch_move(touch, *args)
def on_touch_up(self, touch):
if self.collide_point(touch.x, touch.y) and self._doing_ripple:
self.finish_ripple()
return super().on_touch_up(touch)
def _set_ellipse(self, instance, value):
self.ellipse.size = (self._ripple_rad, self._ripple_rad)
# Adjust ellipse pos here
def _set_color(self, instance, value):
self.col_instruction.a = value[3]
class RectangularRippleBehavior(CommonRipple):
"""Class implements a rectangular ripple effect."""
ripple_scale = NumericProperty(2.75)
"""
See :class:`~CommonRipple.ripple_scale`.
:attr:`ripple_scale` is an :class:`~kivy.properties.NumericProperty`
and defaults to `2.75`.
"""
def lay_canvas_instructions(self):
if self._no_ripple_effect:
return
with self.canvas.after:
StencilPush()
Rectangle(pos=self.pos, size=self.size)
StencilUse()
self.col_instruction = Color(rgba=self.ripple_color)
self.ellipse = Ellipse(
size=(self._ripple_rad, self._ripple_rad),
pos=(
self.ripple_pos[0] - self._ripple_rad / 2.0,
self.ripple_pos[1] - self._ripple_rad / 2.0,
),
)
StencilUnUse()
Rectangle(pos=self.pos, size=self.size)
StencilPop()
self.bind(ripple_color=self._set_color, _ripple_rad=self._set_ellipse)
def _set_ellipse(self, instance, value):
super()._set_ellipse(instance, value)
self.ellipse.pos = (
self.ripple_pos[0] - self._ripple_rad / 2.0,
self.ripple_pos[1] - self._ripple_rad / 2.0,
)
class CircularRippleBehavior(CommonRipple):
"""Class implements a circular ripple effect."""
ripple_scale = NumericProperty(1)
"""
See :class:`~CommonRipple.ripple_scale`.
:attr:`ripple_scale` is an :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
def lay_canvas_instructions(self):
with self.canvas.after:
StencilPush()
self.stencil = Ellipse(
size=(
self.width * self.ripple_scale,
self.height * self.ripple_scale,
),
pos=(
self.center_x - (self.width * self.ripple_scale) / 2,
self.center_y - (self.height * self.ripple_scale) / 2,
),
)
StencilUse()
self.col_instruction = Color(rgba=self.ripple_color)
self.ellipse = Ellipse(
size=(self._ripple_rad, self._ripple_rad),
pos=(
self.center_x - self._ripple_rad / 2.0,
self.center_y - self._ripple_rad / 2.0,
),
)
StencilUnUse()
Ellipse(pos=self.pos, size=self.size)
StencilPop()
self.bind(
ripple_color=self._set_color, _ripple_rad=self._set_ellipse
)
def _set_ellipse(self, instance, value):
super()._set_ellipse(instance, value)
if self.ellipse.size[0] > self.width * 0.6 and not self._fading_out:
self.fade_out()
self.ellipse.pos = (
self.center_x - self._ripple_rad / 2.0,
self.center_y - self._ripple_rad / 2.0,
)

View file

@ -0,0 +1,202 @@
"""
Behaviors/ToggleButton
======================
This behavior must always be inherited after the button's Widget class since it
works with the inherited properties of the button class.
example:
.. code-block:: python
class MyToggleButtonWidget(MDFlatButton, MDToggleButton):
# [...]
pass
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors.toggle_behavior import MDToggleButton
from kivymd.uix.button import MDRectangleFlatButton
KV = '''
Screen:
MDBoxLayout:
adaptive_size: True
pos_hint: {"center_x": .5, "center_y": .5}
MyToggleButton:
text: "Show ads"
group: "x"
MyToggleButton:
text: "Do not show ads"
group: "x"
MyToggleButton:
text: "Does not matter"
group: "x"
'''
class MyToggleButton(MDRectangleFlatButton, MDToggleButton):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.background_down = self.theme_cls.primary_light
class Test(MDApp):
def build(self):
return Builder.load_string(KV)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toggle-button-1.gif
:align: center
.. code-block:: python
class MyToggleButton(MDFillRoundFlatButton, MDToggleButton):
def __init__(self, **kwargs):
self.background_down = MDApp.get_running_app().theme_cls.primary_dark
super().__init__(**kwargs)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toggle-button-2.gif
:align: center
You can inherit the ``MyToggleButton`` class only from the following classes
----------------------------------------------------------------------------
- :class:`~kivymd.uix.button.MDRaisedButton`
- :class:`~kivymd.uix.button.MDFlatButton`
- :class:`~kivymd.uix.button.MDRectangleFlatButton`
- :class:`~kivymd.uix.button.MDRectangleFlatIconButton`
- :class:`~kivymd.uix.button.MDRoundFlatButton`
- :class:`~kivymd.uix.button.MDRoundFlatIconButton`
- :class:`~kivymd.uix.button.MDFillRoundFlatButton`
- :class:`~kivymd.uix.button.MDFillRoundFlatIconButton`
"""
__all__ = ("MDToggleButton",)
from kivy.properties import BooleanProperty, ListProperty
from kivy.uix.behaviors import ToggleButtonBehavior
from kivymd.uix.button import (
MDFillRoundFlatButton,
MDFillRoundFlatIconButton,
MDFlatButton,
MDRaisedButton,
MDRectangleFlatButton,
MDRectangleFlatIconButton,
MDRoundFlatButton,
MDRoundFlatIconButton,
)
class MDToggleButton(ToggleButtonBehavior):
background_normal = ListProperty()
"""
Color of the button in ``rgba`` format for the 'normal' state.
:attr:`background_normal` is a :class:`~kivy.properties.ListProperty`
and is defaults to `[]`.
"""
background_down = ListProperty()
"""
Color of the button in ``rgba`` format for the 'down' state.
:attr:`background_down` is a :class:`~kivy.properties.ListProperty`
and is defaults to `[]`.
"""
font_color_normal = ListProperty()
"""
Color of the font's button in ``rgba`` format for the 'normal' state.
:attr:`font_color_normal` is a :class:`~kivy.properties.ListProperty`
and is defaults to `[]`.
"""
font_color_down = ListProperty([1, 1, 1, 1])
"""
Color of the font's button in ``rgba`` format for the 'down' state.
:attr:`font_color_down` is a :class:`~kivy.properties.ListProperty`
and is defaults to `[1, 1, 1, 1]`.
"""
__is_filled = BooleanProperty(False)
def __init__(self, **kwargs):
super().__init__(**kwargs)
classinfo = (
MDRaisedButton,
MDFlatButton,
MDRectangleFlatButton,
MDRectangleFlatIconButton,
MDRoundFlatButton,
MDRoundFlatIconButton,
MDFillRoundFlatButton,
MDFillRoundFlatIconButton,
)
# Do the object inherited from the "supported" buttons?
if not issubclass(self.__class__, classinfo):
raise ValueError(
f"Class {self.__class__} must be inherited from one of the classes in the list {classinfo}"
)
if (
not self.background_normal
): # This means that if the value == [] or None will return True.
# If the object inherits from buttons with background:
if isinstance(
self,
(
MDRaisedButton,
MDFillRoundFlatButton,
MDFillRoundFlatIconButton,
),
):
self.__is_filled = True
self.background_normal = self.theme_cls.primary_color
# If not the background_normal must be the same as the inherited one:
else:
self.background_normal = self.md_bg_color[:]
# If no background_down is setted:
if (
not self.background_down
): # This means that if the value == [] or None will return True.
self.background_down = (
self.theme_cls.primary_dark
) # get the primary_color dark from theme_cls
if not self.font_color_normal:
self.font_color_normal = self.theme_cls.primary_color
# Alternative to bind the function to the property.
# self.bind(state=self._update_bg)
self.fbind("state", self._update_bg)
def _update_bg(self, ins, val):
"""Updates the color of the background."""
if val == "down":
self.md_bg_color = self.background_down
if (
self.__is_filled is False
): # If the background is transparent, and the button it toggled,
# the font color must be withe [1, 1, 1, 1].
self.text_color = self.font_color_down
if self.group:
self._release_group(self)
else:
self.md_bg_color = self.background_normal
if (
self.__is_filled is False
): # If the background is transparent, the font color must be the
# primary color.
self.text_color = self.font_color_normal

View file

@ -0,0 +1,100 @@
"""
Behaviors/Touch
===============
.. rubric:: Provides easy access to events.
The following events are available:
- on_long_touch
- on_double_tap
- on_triple_tap
Usage
-----
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.behaviors import TouchBehavior
from kivymd.uix.button import MDRaisedButton
KV = '''
Screen:
MyButton:
text: "PRESS ME"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class MyButton(MDRaisedButton, TouchBehavior):
def on_long_touch(self, *args):
print("<on_long_touch> event")
def on_double_tap(self, *args):
print("<on_double_tap> event")
def on_triple_tap(self, *args):
print("<on_triple_tap> event")
class MainApp(MDApp):
def build(self):
return Builder.load_string(KV)
MainApp().run()
"""
__all__ = ("TouchBehavior",)
from functools import partial
from kivy.clock import Clock
from kivy.properties import NumericProperty
class TouchBehavior:
duration_long_touch = NumericProperty(0.4)
"""
Time for a long touch.
:attr:`duration_long_touch` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.4`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(
on_touch_down=self.create_clock, on_touch_up=self.delete_clock
)
def create_clock(self, widget, touch, *args):
if self.collide_point(touch.x, touch.y):
callback = partial(self.on_long_touch, touch)
Clock.schedule_once(callback, self.duration_long_touch)
touch.ud["event"] = callback
def delete_clock(self, widget, touch, *args):
if self.collide_point(touch.x, touch.y):
try:
Clock.unschedule(touch.ud["event"])
except KeyError:
pass
if touch.is_double_tap:
self.on_double_tap(touch, *args)
if touch.is_triple_tap:
self.on_triple_tap(touch, *args)
def on_long_touch(self, touch, *args):
"""Called when the widget is pressed for a long time."""
def on_double_tap(self, touch, *args):
"""Called by double clicking on the widget."""
def on_triple_tap(self, touch, *args):
"""Called by triple clicking on the widget."""

646
kivymd/uix/bottomnavigation.py Executable file
View file

@ -0,0 +1,646 @@
"""
Components/Bottom Navigation
============================
.. seealso::
`Material Design spec, Bottom navigation <https://material.io/components/bottom-navigation>`_
.. rubric:: Bottom navigation bars allow movement between primary destinations in an app:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation.png
:align: center
Usage
-----
.. code-block:: kv
<Root>>:
MDBottomNavigation:
MDBottomNavigationItem:
name: "screen 1"
YourContent:
MDBottomNavigationItem:
name: "screen 2"
YourContent:
MDBottomNavigationItem:
name: "screen 3"
YourContent:
For ease of understanding, this code works like this:
.. code-block:: kv
<Root>>:
ScreenManager:
Screen:
name: "screen 1"
YourContent:
Screen:
name: "screen 2"
YourContent:
Screen:
name: "screen 3"
YourContent:
Example
-------
.. code-block:: python
from kivymd.app import MDApp
from kivy.lang import Builder
class Test(MDApp):
def build(self):
self.theme_cls.primary_palette = "Gray"
return Builder.load_string(
'''
BoxLayout:
orientation:'vertical'
MDToolbar:
title: 'Bottom navigation'
md_bg_color: .2, .2, .2, 1
specific_text_color: 1, 1, 1, 1
MDBottomNavigation:
panel_color: .2, .2, .2, 1
MDBottomNavigationItem:
name: 'screen 1'
text: 'Python'
icon: 'language-python'
MDLabel:
text: 'Python'
halign: 'center'
MDBottomNavigationItem:
name: 'screen 2'
text: 'C++'
icon: 'language-cpp'
MDLabel:
text: 'I programming of C++'
halign: 'center'
MDBottomNavigationItem:
name: 'screen 3'
text: 'JS'
icon: 'language-javascript'
MDLabel:
text: 'JS'
halign: 'center'
'''
)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation.gif
:align: center
.. rubric:: :class:`~MDBottomNavigationItem` provides the following events for use:
.. code-block:: python
__events__ = (
"on_tab_touch_down",
"on_tab_touch_move",
"on_tab_touch_up",
"on_tab_press",
"on_tab_release",
)
.. seealso::
See :class:`~MDTab.__events__`
.. code-block:: kv
Root:
MDBottomNavigation:
MDBottomNavigationItem:
on_tab_touch_down: print("on_tab_touch_down")
on_tab_touch_move: print("on_tab_touch_move")
on_tab_touch_up: print("on_tab_touch_up")
on_tab_press: print("on_tab_press")
on_tab_release: print("on_tab_release")
YourContent:
How to automatically switch a tab?
----------------------------------
Use method :attr:`~MDBottomNavigation.switch_tab` which takes as argument
the name of the tab you want to switch to.
How to change icon color?
-------------------------
.. code-block:: kv
MDBottomNavigation:
text_color_active: 1, 0, 1, 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-text_color_active.png
.. code-block:: kv
MDBottomNavigation:
text_color_normal: 1, 0, 1, 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-text_color_normal.png
.. seealso::
`See Tab auto switch example <https://github.com/kivymd/KivyMD/wiki/Components-Tabs-Auto-Switch>`_
`See full example <https://github.com/kivymd/KivyMD/wiki/Components-Bottom-Navigation>`_
"""
__all__ = (
"TabbedPanelBase",
"MDBottomNavigationItem",
"MDBottomNavigation",
"MDTab",
)
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.metrics import sp
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
StringProperty,
)
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import Screen, ScreenManagerException
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import RectangularElevationBehavior
from kivymd.uix.behaviors.backgroundcolorbehavior import (
BackgroundColorBehavior,
SpecificBackgroundColorBehavior,
)
from kivymd.uix.button import BaseFlatButton, BasePressedButton
Builder.load_string(
"""
#:import sm kivy.uix.screenmanager
#:import Window kivy.core.window.Window
<MDBottomNavigation>
id: panel
orientation: 'vertical'
height: dp(56) # Spec
ScreenManager:
id: tab_manager
transition: sm.FadeTransition(duration=.2)
current: root.current
screens: root.tabs
MDBottomNavigationBar:
id: bottom_panel
size_hint_y: None
height: dp(56)
md_bg_color: root.theme_cls.bg_dark if not root.panel_color else root.panel_color
BoxLayout:
id: tab_bar
pos_hint: {'center_x': .5, 'center_y': .5}
height: dp(56)
size_hint: None, None
<MDBottomNavigationHeader>
canvas:
Color:
rgba: root.panel_color
#rgba: self.panel.theme_cls.bg_dark if not root.panel_color else root.panel_color
Rectangle:
size: self.size
pos: self.pos
width:
root.panel.width / len(root.panel.ids.tab_manager.screens)\
if len(root.panel.ids.tab_manager.screens) != 0 else root.panel.width
padding: (dp(12), dp(12))
on_press:
self.tab.dispatch('on_tab_press')
on_release: self.tab.dispatch('on_tab_release')
on_touch_down: self.tab.dispatch('on_tab_touch_down',*args)
on_touch_move: self.tab.dispatch('on_tab_touch_move',*args)
on_touch_up: self.tab.dispatch('on_tab_touch_up',*args)
FloatLayout:
id: item_container
MDIcon:
id: _label_icon
icon: root.tab.icon
size_hint_x: None
text_size: (None, root.height)
height: self.texture_size[1]
theme_text_color: 'Custom'
text_color: root._text_color_normal
valign: 'middle'
halign: 'center'
opposite_colors: root.opposite_colors
pos: [self.pos[0], self.pos[1]]
font_size: dp(24)
pos_hint: {'center_x': .5, 'center_y': .7}
MDLabel:
id: _label
text: root.tab.text
font_style: 'Button'
size_hint_x: None
text_size: (None, root.height)
height: self.texture_size[1]
theme_text_color: 'Custom'
text_color: root._text_color_normal
valign: 'bottom'
halign: 'center'
opposite_colors: root.opposite_colors
font_size: root._label_font_size
pos_hint: {'center_x': .5, 'center_y': .6}
<MDTab>
canvas:
Color:
rgba: root.theme_cls.bg_normal
Rectangle:
size: root.size
"""
)
class MDBottomNavigationHeader(BaseFlatButton, BasePressedButton):
panel_color = ListProperty([1, 1, 1, 0])
"""Panel color of bottom navigation.
:attr:`panel_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[1, 1, 1, 0]`.
"""
tab = ObjectProperty()
"""
:attr:`tab` is an :class:`~MDBottomNavigationItem`
and defaults to `None`.
"""
panel = ObjectProperty()
"""
:attr:`panel` is an :class:`~MDBottomNavigation`
and defaults to `None`.
"""
active = BooleanProperty(False)
text = StringProperty()
"""
:attr:`text` is an :class:`~MDTab.text`
and defaults to `''`.
"""
text_color_normal = ListProperty([1, 1, 1, 1])
"""
Text color of the label when it is not selected.
:attr:`text_color_normal` is an :class:`~kivy.properties.ListProperty`
and defaults to `[1, 1, 1, 1]`.
"""
text_color_active = ListProperty([1, 1, 1, 1])
"""
Text color of the label when it is selected.
:attr:`text_color_active` is an :class:`~kivy.properties.ListProperty`
and defaults to `[1, 1, 1, 1]`.
"""
_label = ObjectProperty()
_label_font_size = NumericProperty("12sp")
_text_color_normal = ListProperty([1, 1, 1, 1])
_text_color_active = ListProperty([1, 1, 1, 1])
def __init__(self, panel, height, tab):
self.panel = panel
self.height = height
self.tab = tab
super().__init__()
self._text_color_normal = (
self.theme_cls.disabled_hint_text_color
if self.text_color_normal == [1, 1, 1, 1]
else self.text_color_normal
)
self._label = self.ids._label
self._label_font_size = sp(12)
self.theme_cls.bind(disabled_hint_text_color=self._update_theme_style)
self.active = False
def on_press(self):
Animation(_label_font_size=sp(14), d=0.1).start(self)
Animation(
_text_color_normal=self.theme_cls.primary_color
if self.text_color_active == [1, 1, 1, 1]
else self.text_color_active,
d=0.1,
).start(self)
def _update_theme_style(self, instance, color):
"""Called when the application theme style changes (White/Black)."""
if not self.active:
self._text_color_normal = (
color
if self.text_color_normal == [1, 1, 1, 1]
else self.text_color_normal
)
class MDTab(Screen, ThemableBehavior):
"""A tab is simply a screen with meta information
that defines the content that goes in the tab header.
"""
__events__ = (
"on_tab_touch_down",
"on_tab_touch_move",
"on_tab_touch_up",
"on_tab_press",
"on_tab_release",
)
"""Events provided."""
text = StringProperty()
"""Tab header text.
:attr:`text` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
icon = StringProperty("checkbox-blank-circle")
"""Tab header icon.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `'checkbox-blank-circle'`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.index = 0
self.parent_widget = None
self.register_event_type("on_tab_touch_down")
self.register_event_type("on_tab_touch_move")
self.register_event_type("on_tab_touch_up")
self.register_event_type("on_tab_press")
self.register_event_type("on_tab_release")
def on_tab_touch_down(self, *args):
pass
def on_tab_touch_move(self, *args):
pass
def on_tab_touch_up(self, *args):
pass
def on_tab_press(self, *args):
par = self.parent_widget
if par.previous_tab is not self:
if par.previous_tab.index > self.index:
par.ids.tab_manager.transition.direction = "right"
elif par.previous_tab.index < self.index:
par.ids.tab_manager.transition.direction = "left"
par.ids.tab_manager.current = self.name
par.previous_tab = self
def on_tab_release(self, *args):
pass
def __repr__(self):
return f"<MDTab name='{self.name}', text='{self.text}'>"
class MDBottomNavigationItem(MDTab):
header = ObjectProperty()
"""
:attr:`header` is an :class:`~MDBottomNavigationHeader`
and defaults to `None`.
"""
def on_tab_press(self, *args):
par = self.parent_widget
par.ids.tab_manager.current = self.name
if par.previous_tab is not self:
Animation(_label_font_size=sp(12), d=0.1).start(
par.previous_tab.header
)
Animation(
_text_color_normal=par.previous_tab.header.text_color_normal
if par.previous_tab.header.text_color_normal != [1, 1, 1, 1]
else self.theme_cls.disabled_hint_text_color,
d=0.1,
).start(par.previous_tab.header)
par.previous_tab.header.active = False
self.header.active = True
par.previous_tab = self
def on_leave(self, *args):
pass
class TabbedPanelBase(
ThemableBehavior, SpecificBackgroundColorBehavior, BoxLayout
):
"""A class that contains all variables a :class:`~kivy.properties.TabPannel`
must have. It is here so I (zingballyhoo) don't get mad about
the :class:`~kivy.properties.TabbedPannels` not being DRY.
"""
current = StringProperty(None)
"""Current tab name.
:attr:`current` is an :class:`~kivy.properties.StringProperty`
and defaults to `None`.
"""
previous_tab = ObjectProperty()
"""
:attr:`previous_tab` is an :class:`~MDTab` and defaults to `None`.
"""
panel_color = ListProperty()
"""Panel color of bottom navigation.
:attr:`panel_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
tabs = ListProperty()
class MDBottomNavigation(TabbedPanelBase):
"""A bottom navigation that is implemented by delegating
all items to a ScreenManager."""
first_widget = ObjectProperty()
"""
:attr:`first_widget` is an :class:`~MDBottomNavigationItem`
and defaults to `None`.
"""
tab_header = ObjectProperty()
"""
:attr:`tab_header` is an :class:`~MDBottomNavigationHeader`
and defaults to `None`.
"""
text_color_normal = ListProperty([1, 1, 1, 1])
"""
Text color of the label when it is not selected.
:attr:`text_color_normal` is an :class:`~kivy.properties.ListProperty`
and defaults to `[1, 1, 1, 1]`.
"""
text_color_active = ListProperty([1, 1, 1, 1])
"""
Text color of the label when it is selected.
:attr:`text_color_active` is an :class:`~kivy.properties.ListProperty`
and defaults to `[1, 1, 1, 1]`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.previous_tab = None
self.widget_index = 0
Window.bind(on_resize=self.on_resize)
Clock.schedule_once(lambda x: self.on_resize(), 0)
def on_panel_color(self, instance, value):
self.tab_header.panel_color = value
def on_text_color_normal(self, instance, value):
for tab in self.ids.tab_bar.children:
if not tab.active:
tab._text_color_normal = value
def on_text_color_active(self, instance, value):
for tab in self.ids.tab_bar.children:
tab.text_color_active = value
if tab.active:
tab._text_color_normal = value
def switch_tab(self, name_tab):
"""Switching the tab by name."""
if not self.ids.tab_manager.has_screen(name_tab):
raise ScreenManagerException(f"No Screen with name '{name_tab}'.")
self.ids.tab_manager.get_screen(name_tab).dispatch("on_tab_press")
count_index_screen = [
self.ids.tab_manager.screens.index(screen)
for screen in self.ids.tab_manager.screens
if screen.name == name_tab
][0]
numbers_screens = list(range(len(self.ids.tab_manager.screens)))
numbers_screens.reverse()
self.ids.tab_bar.children[
numbers_screens.index(count_index_screen)
].dispatch("on_press")
def refresh_tabs(self):
"""Refresh all tabs."""
if not self.ids:
return
tab_bar = self.ids.tab_bar
tab_bar.clear_widgets()
tab_manager = self.ids.tab_manager
for tab in tab_manager.screens:
self.tab_header = MDBottomNavigationHeader(
tab=tab, panel=self, height=tab_bar.height
)
tab.header = self.tab_header
tab_bar.add_widget(self.tab_header)
if tab is self.first_widget:
self.tab_header._text_color_normal = (
self.theme_cls.primary_color
)
self.tab_header._label_font_size = sp(14)
self.tab_header.active = True
else:
self.tab_header._label_font_size = sp(12)
def on_resize(self, instance=None, width=None, do_again=True):
"""Called when the application window is resized."""
full_width = 0
for tab in self.ids.tab_manager.screens:
full_width += tab.header.width
tab.header.text_color_normal = self.text_color_normal
self.ids.tab_bar.width = full_width
if do_again:
Clock.schedule_once(lambda x: self.on_resize(do_again=False), 0.1)
def add_widget(self, widget, **kwargs):
if isinstance(widget, MDBottomNavigationItem):
self.widget_index += 1
widget.index = self.widget_index
widget.parent_widget = self
self.ids.tab_manager.add_widget(widget)
if self.widget_index == 1:
self.previous_tab = widget
self.first_widget = widget
self.refresh_tabs()
else:
super().add_widget(widget)
def remove_widget(self, widget):
if isinstance(widget, MDBottomNavigationItem):
self.ids.tab_manager.remove_widget(widget)
self.refresh_tabs()
else:
super().remove_widget(widget)
class MDBottomNavigationBar(
ThemableBehavior,
BackgroundColorBehavior,
FloatLayout,
RectangularElevationBehavior,
):
pass

564
kivymd/uix/bottomsheet.py Executable file
View file

@ -0,0 +1,564 @@
"""
Components/Bottom Sheet
=======================
.. seealso::
`Material Design spec, Sheets: bottom <https://material.io/components/sheets-bottom>`_
.. rubric:: Bottom sheets are surfaces containing supplementary content that are anchored to the bottom of the screen.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet.png
:align: center
Two classes are available to you :class:`~MDListBottomSheet` and :class:`~MDGridBottomSheet`
for standard bottom sheets dialogs:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/grid-list-bottomsheets.png
:align: center
Usage :class:`~MDListBottomSheet`
=================================
.. code-block:: python
from kivy.lang import Builder
from kivymd.toast import toast
from kivymd.uix.bottomsheet import MDListBottomSheet
from kivymd.app import MDApp
KV = '''
Screen:
MDToolbar:
title: "Example BottomSheet"
pos_hint: {"top": 1}
elevation: 10
MDRaisedButton:
text: "Open list bottom sheet"
on_release: app.show_example_list_bottom_sheet()
pos_hint: {"center_x": .5, "center_y": .5}
'''
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
def callback_for_menu_items(self, *args):
toast(args[0])
def show_example_list_bottom_sheet(self):
bottom_sheet_menu = MDListBottomSheet()
for i in range(1, 11):
bottom_sheet_menu.add_item(
f"Standart Item {i}",
lambda x, y=i: self.callback_for_menu_items(
f"Standart Item {y}"
),
)
bottom_sheet_menu.open()
Example().run()
The :attr:`~MDListBottomSheet.add_item` method of the :class:`~MDListBottomSheet`
class takes the following arguments:
``text`` - element text;
``callback`` - function that will be called when clicking on an item;
There is also an optional argument ``icon``,
which will be used as an icon to the left of the item:
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-list-bottomsheets.png
:align: center
.. rubric:: Using the :class:`~MDGridBottomSheet` class is similar
to using the :class:`~MDListBottomSheet` class:
.. code-block:: python
from kivy.lang import Builder
from kivymd.toast import toast
from kivymd.uix.bottomsheet import MDGridBottomSheet
from kivymd.app import MDApp
KV = '''
Screen:
MDToolbar:
title: 'Example BottomSheet'
pos_hint: {"top": 1}
elevation: 10
MDRaisedButton:
text: "Open grid bottom sheet"
on_release: app.show_example_grid_bottom_sheet()
pos_hint: {"center_x": .5, "center_y": .5}
'''
class Example(MDApp):
def build(self):
return Builder.load_string(KV)
def callback_for_menu_items(self, *args):
toast(args[0])
def show_example_grid_bottom_sheet(self):
bottom_sheet_menu = MDGridBottomSheet()
data = {
"Facebook": "facebook-box",
"YouTube": "youtube",
"Twitter": "twitter-box",
"Da Cloud": "cloud-upload",
"Camera": "camera",
}
for item in data.items():
bottom_sheet_menu.add_item(
item[0],
lambda x, y=item[0]: self.callback_for_menu_items(y),
icon_src=item[1],
)
bottom_sheet_menu.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/grid-bottomsheet.png
:align: center
.. rubric:: You can use custom content for bottom sheet dialogs:
.. code-block:: python
from kivy.lang import Builder
from kivy.factory import Factory
from kivymd.uix.bottomsheet import MDCustomBottomSheet
from kivymd.app import MDApp
KV = '''
<ItemForCustomBottomSheet@OneLineIconListItem>
on_press: app.custom_sheet.dismiss()
icon: ""
IconLeftWidget:
icon: root.icon
<ContentCustomSheet@BoxLayout>:
orientation: "vertical"
size_hint_y: None
height: "400dp"
MDToolbar:
title: 'Custom bottom sheet:'
ScrollView:
MDGridLayout:
cols: 1
adaptive_height: True
ItemForCustomBottomSheet:
icon: "page-previous"
text: "Preview"
ItemForCustomBottomSheet:
icon: "exit-to-app"
text: "Exit"
Screen:
MDToolbar:
title: 'Example BottomSheet'
pos_hint: {"top": 1}
elevation: 10
MDRaisedButton:
text: "Open custom bottom sheet"
on_release: app.show_example_custom_bottom_sheet()
pos_hint: {"center_x": .5, "center_y": .5}
'''
class Example(MDApp):
custom_sheet = None
def build(self):
return Builder.load_string(KV)
def show_example_custom_bottom_sheet(self):
self.custom_sheet = MDCustomBottomSheet(screen=Factory.ContentCustomSheet())
self.custom_sheet.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-bottomsheet.png
:align: center
.. note:: When you use the :attr:`~MDCustomBottomSheet` class, you must specify
the height of the user-defined content exactly, otherwise ``dp(100)``
heights will be used for your ``ContentCustomSheet`` class:
.. code-block:: kv
<ContentCustomSheet@BoxLayout>:
orientation: "vertical"
size_hint_y: None
height: "400dp"
.. note:: The height of the bottom sheet dialog will never exceed half
the height of the screen!
"""
__all__ = (
"MDGridBottomSheet",
"GridBottomSheetItem",
"MDListBottomSheet",
"MDCustomBottomSheet",
"MDBottomSheet",
)
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.modalview import ModalView
from kivy.uix.scrollview import ScrollView
from kivymd import images_path
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import BackgroundColorBehavior
from kivymd.uix.label import MDIcon
from kivymd.uix.list import ILeftBody, OneLineIconListItem, OneLineListItem
Builder.load_string(
"""
#:import Window kivy.core.window.Window
<SheetList>:
MDGridLayout:
id: box_sheet_list
cols: 1
adaptive_height: True
padding: 0, 0, 0, "96dp"
<MDBottomSheet>
md_bg_color: root.value_transparent
_upper_padding: _upper_padding
_gl_content: _gl_content
_position_content: Window.height
MDBoxLayout:
orientation: "vertical"
padding: 0, 1, 0, 0
BsPadding:
id: _upper_padding
size_hint_y: None
height: root.height - min(root.width * 9 / 16, root._gl_content.height)
on_release: root.dismiss()
BottomSheetContent:
id: _gl_content
size_hint_y: None
cols: 1
md_bg_color: 0, 0, 0, 0
canvas:
Color:
rgba: root.theme_cls.bg_normal if not root.bg_color else root.bg_color
RoundedRectangle:
pos: self.pos
size: self.size
radius:
[
(root.radius, root.radius) if root.radius_from == "top_left" or root.radius_from == "top" else (0, 0),
(root.radius, root.radius) if root.radius_from == "top_right" or root.radius_from == "top" else (0, 0),
(root.radius, root.radius) if root.radius_from == "bottom_right" or root.radius_from == "bottom" else (0, 0),
(root.radius, root.radius) if root.radius_from == "bottom_left" or root.radius_from == "bottom" else (0, 0)
]
"""
)
class SheetList(ScrollView):
pass
class BsPadding(ButtonBehavior, FloatLayout):
pass
class BottomSheetContent(BackgroundColorBehavior, GridLayout):
pass
class MDBottomSheet(ThemableBehavior, ModalView):
background = f"{images_path}transparent.png"
"""Private attribute."""
duration_opening = NumericProperty(0.15)
"""The duration of the bottom sheet dialog opening animation.
:attr:`duration_opening` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.15`.
"""
radius = NumericProperty(25)
"""The value of the rounding of the corners of the dialog.
:attr:`radius` is an :class:`~kivy.properties.NumericProperty`
and defaults to `25`.
"""
radius_from = OptionProperty(
None,
options=[
"top_left",
"top_right",
"top",
"bottom_right",
"bottom_left",
"bottom",
],
allownone=True,
)
"""Sets which corners to cut from the dialog. Available options are:
(`"top_left"`, `"top_right"`, `"top"`, `"bottom_right"`, `"bottom_left"`, `"bottom"`).
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-radius-from.png
:align: center
:attr:`radius_from` is an :class:`~kivy.properties.OptionProperty`
and defaults to `None`.
"""
animation = BooleanProperty(False)
"""To use animation of opening of dialogue of the bottom sheet or not.
:attr:`animation` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
bg_color = ListProperty()
"""Dialog background color in ``rgba`` format.
:attr:`bg_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
value_transparent = ListProperty([0, 0, 0, 0.8])
"""Background transparency value when opening a dialog.
:attr:`value_transparent` is an :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0.8]`.
"""
_upper_padding = ObjectProperty()
_gl_content = ObjectProperty()
_position_content = NumericProperty()
def open(self, *largs):
super().open(*largs)
def add_widget(self, widget, index=0, canvas=None):
super().add_widget(widget, index, canvas)
def on_dismiss(self):
self._gl_content.clear_widgets()
def resize_content_layout(self, content, layout, interval=0):
if not layout.ids.get("box_sheet_list"):
_layout = layout
else:
_layout = layout.ids.box_sheet_list
if _layout.height > Window.height / 2:
height = Window.height / 2
else:
height = _layout.height
if self.animation:
Animation(height=height, d=self.duration_opening).start(_layout)
Animation(height=height, d=self.duration_opening).start(content)
else:
layout.height = height
content.height = height
Builder.load_string(
"""
<ListBottomSheetIconLeft>
halign: "center"
theme_text_color: "Primary"
valign: "middle"
"""
)
class ListBottomSheetIconLeft(ILeftBody, MDIcon):
pass
class MDCustomBottomSheet(MDBottomSheet):
screen = ObjectProperty()
"""
Custom content.
:attr:`screen` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._gl_content.add_widget(self.screen)
Clock.schedule_once(
lambda x: self.resize_content_layout(self._gl_content, self.screen),
0,
)
class MDListBottomSheet(MDBottomSheet):
sheet_list = ObjectProperty()
"""
:attr:`sheet_list` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sheet_list = SheetList(size_hint_y=None)
self._gl_content.add_widget(self.sheet_list)
Clock.schedule_once(
lambda x: self.resize_content_layout(
self._gl_content, self.sheet_list
),
0,
)
def add_item(self, text, callback, icon=None):
"""
:arg text: element text;
:arg callback: function that will be called when clicking on an item;
:arg icon: which will be used as an icon to the left of the item;
"""
if icon:
item = OneLineIconListItem(text=text, on_release=callback)
item.add_widget(ListBottomSheetIconLeft(icon=icon))
else:
item = OneLineListItem(text=text, on_release=callback)
item.bind(on_release=lambda x: self.dismiss())
self.sheet_list.ids.box_sheet_list.add_widget(item)
Builder.load_string(
"""
<GridBottomSheetItem>
orientation: "vertical"
padding: 0, dp(24), 0, 0
size_hint_y: None
size: dp(64), dp(96)
AnchorLayout:
anchoor_x: "center"
MDIconButton:
icon: root.source
user_font_size: root.icon_size
on_release: root.dispatch("on_release")
MDLabel:
font_style: "Caption"
theme_text_color: "Secondary"
text: root.caption
halign: "center"
"""
)
class GridBottomSheetItem(ButtonBehavior, BoxLayout):
source = StringProperty()
"""
Icon path if you use a local image or icon name
if you use icon names from a file ``kivymd/icon_definitions.py``.
:attr:`source` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
caption = StringProperty()
"""
Item text.
:attr:`caption` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
icon_size = StringProperty("32sp")
"""
Icon size.
:attr:`caption` is an :class:`~kivy.properties.StringProperty`
and defaults to `'32sp'`.
"""
class MDGridBottomSheet(MDBottomSheet):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.sheet_list = SheetList(size_hint_y=None)
self.sheet_list.ids.box_sheet_list.cols = 3
self.sheet_list.ids.box_sheet_list.padding = (dp(16), 0, dp(16), dp(96))
self._gl_content.add_widget(self.sheet_list)
Clock.schedule_once(
lambda x: self.resize_content_layout(
self._gl_content, self.sheet_list
),
0,
)
def add_item(self, text, callback, icon_src):
"""
:arg text: element text;
:arg callback: function that will be called when clicking on an item;
:arg icon_src: icon item;
"""
def tap_on_item(instance):
callback(instance)
self.dismiss()
item = GridBottomSheetItem(
caption=text, on_release=tap_on_item, source=icon_src
)
if len(self._gl_content.children) % 3 == 0:
self._gl_content.height += dp(96)
self.sheet_list.ids.box_sheet_list.add_widget(item)

92
kivymd/uix/boxlayout.py Normal file
View file

@ -0,0 +1,92 @@
"""
Components/BoxLayout
====================
:class:`~kivy.uix.boxlayout.BoxLayout` class equivalent. Simplifies working
with some widget properties. For example:
BoxLayout
---------
.. code-block::
BoxLayout:
size_hint_y: None
height: self.minimum_height
canvas:
Color:
rgba: app.theme_cls.primary_color
Rectangle:
pos: self.pos
size: self.size
MDBoxLayout
-----------
.. code-block::
MDBoxLayout:
adaptive_height: True
md_bg_color: app.theme_cls.primary_color
Available options are:
---------------------
- adaptive_height_
- adaptive_width_
- adaptive_size_
.. adaptive_height:
adaptive_height
---------------
.. code-block:: kv
adaptive_height: True
Equivalent
.. code-block:: kv
size_hint_y: None
height: self.minimum_height
.. adaptive_width:
adaptive_width
--------------
.. code-block:: kv
adaptive_width: True
Equivalent
.. code-block:: kv
size_hint_x: None
height: self.minimum_width
.. adaptive_size:
adaptive_size
-------------
.. code-block:: kv
adaptive_size: True
Equivalent
.. code-block:: kv
size_hint: None, None
size: self.minimum_size
"""
from kivy.uix.boxlayout import BoxLayout
from kivymd.uix import MDAdaptiveWidget
class MDBoxLayout(BoxLayout, MDAdaptiveWidget):
pass

1842
kivymd/uix/button.py Executable file

File diff suppressed because it is too large Load diff

896
kivymd/uix/card.py Executable file
View file

@ -0,0 +1,896 @@
"""
Components/Card
===============
.. seealso::
`Material Design spec, Cards <https://material.io/components/cards>`_
.. rubric:: Cards contain content and actions about a single subject.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/cards.gif
:align: center
`KivyMD` provides the following card classes for use:
- MDCard_
- MDCardSwipe_
.. Note:: :class:`~MDCard` inherited from
:class:`~kivy.uix.boxlayout.BoxLayout`. You can use all parameters and
attributes of the :class:`~kivy.uix.boxlayout.BoxLayout` class in the
:class:`~MDCard` class.
.. MDCard:
MDCard
------
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
KV = '''
Screen:
MDCard:
size_hint: None, None
size: "280dp", "180dp"
pos_hint: {"center_x": .5, "center_y": .5}
'''
class TestCard(MDApp):
def build(self):
return Builder.load_string(KV)
TestCard().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card.png
:align: center
Add content to card:
--------------------
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
KV = '''
Screen:
MDCard:
orientation: "vertical"
padding: "8dp"
size_hint: None, None
size: "280dp", "180dp"
pos_hint: {"center_x": .5, "center_y": .5}
MDLabel:
text: "Title"
theme_text_color: "Secondary"
size_hint_y: None
height: self.texture_size[1]
MDSeparator:
height: "1dp"
MDLabel:
text: "Body"
'''
class TestCard(MDApp):
def build(self):
return Builder.load_string(KV)
TestCard().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-content.png
:align: center
.. MDCardSwipe:
MDCardSwipe
-----------
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDCardSwipe.gif
:align: center
To create a card with `swipe-to-delete` behavior, you must create a new class
that inherits from the :class:`~MDCardSwipe` class:
.. code-block:: kv
<SwipeToDeleteItem>:
size_hint_y: None
height: content.height
MDCardSwipeLayerBox:
MDCardSwipeFrontBox:
OneLineListItem:
id: content
text: root.text
_no_ripple_effect: True
.. code-block:: python
class SwipeToDeleteItem(MDCardSwipe):
text = StringProperty()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/map-mdcard-swipr.png
:align: center
End full code
-------------
.. code-block:: python
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivymd.app import MDApp
from kivymd.uix.card import MDCardSwipe
KV = '''
<SwipeToDeleteItem>:
size_hint_y: None
height: content.height
MDCardSwipeLayerBox:
# Content under the card.
MDCardSwipeFrontBox:
# Content of card.
OneLineListItem:
id: content
text: root.text
_no_ripple_effect: True
Screen:
BoxLayout:
orientation: "vertical"
spacing: "10dp"
MDToolbar:
elevation: 10
title: "MDCardSwipe"
ScrollView:
scroll_timeout : 100
MDList:
id: md_list
padding: 0
'''
class SwipeToDeleteItem(MDCardSwipe):
'''Card with `swipe-to-delete` behavior.'''
text = StringProperty()
class TestCard(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
def build(self):
return self.screen
def on_start(self):
'''Creates a list of cards.'''
for i in range(20):
self.screen.ids.md_list.add_widget(
SwipeToDeleteItem(text=f"One-line item {i}")
)
TestCard().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/list-mdcard-swipe.gif
:align: center
Binding a swipe to one of the sides of the screen
-------------------------------------------------
.. code-block:: kv
<SwipeToDeleteItem>:
# By default, the parameter is "left"
anchor: "right"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mdcard-swipe-anchor-right.gif
:align: center
.. None:: You cannot use the left and right swipe at the same time.
Swipe behavior
--------------
.. code-block:: kv
<SwipeToDeleteItem>:
# By default, the parameter is "hand"
type_swipe: "hand"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hand-mdcard-swipe.gif
:align: center
.. code-block:: kv
<SwipeToDeleteItem>:
type_swipe: "auto"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/auto-mdcard-swipe.gif
:align: center
Removing an item using the ``type_swipe = "auto"`` parameter
------------------------------------------------------------
The map provides the :attr:`MDCardSwipe.on_swipe_complete` event.
You can use this event to remove items from a list:
.. code-block:: kv
<SwipeToDeleteItem>:
on_swipe_complete: app.on_swipe_complete(root)
.. code-block:: python
def on_swipe_complete(self, instance):
self.screen.ids.md_list.remove_widget(instance)
End full code
-------------
.. code-block:: python
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivymd.app import MDApp
from kivymd.uix.card import MDCardSwipe
KV = '''
<SwipeToDeleteItem>:
size_hint_y: None
height: content.height
type_swipe: "auto"
on_swipe_complete: app.on_swipe_complete(root)
MDCardSwipeLayerBox:
MDCardSwipeFrontBox:
OneLineListItem:
id: content
text: root.text
_no_ripple_effect: True
Screen:
BoxLayout:
orientation: "vertical"
spacing: "10dp"
MDToolbar:
elevation: 10
title: "MDCardSwipe"
ScrollView:
MDList:
id: md_list
padding: 0
'''
class SwipeToDeleteItem(MDCardSwipe):
text = StringProperty()
class TestCard(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
def build(self):
return self.screen
def on_swipe_complete(self, instance):
self.screen.ids.md_list.remove_widget(instance)
def on_start(self):
for i in range(20):
self.screen.ids.md_list.add_widget(
SwipeToDeleteItem(text=f"One-line item {i}")
)
TestCard().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/autodelete-mdcard-swipe.gif
:align: center
Add content to the bottom layer of the card
-------------------------------------------
To add content to the bottom layer of the card,
use the :class:`~MDCardSwipeLayerBox` class.
.. code-block:: kv
<SwipeToDeleteItem>:
MDCardSwipeLayerBox:
padding: "8dp"
MDIconButton:
icon: "trash-can"
pos_hint: {"center_y": .5}
on_release: app.remove_item(root)
End full code
-------------
.. code-block:: python
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivymd.app import MDApp
from kivymd.uix.card import MDCardSwipe
KV = '''
<SwipeToDeleteItem>:
size_hint_y: None
height: content.height
MDCardSwipeLayerBox:
padding: "8dp"
MDIconButton:
icon: "trash-can"
pos_hint: {"center_y": .5}
on_release: app.remove_item(root)
MDCardSwipeFrontBox:
OneLineListItem:
id: content
text: root.text
_no_ripple_effect: True
Screen:
BoxLayout:
orientation: "vertical"
spacing: "10dp"
MDToolbar:
elevation: 10
title: "MDCardSwipe"
ScrollView:
MDList:
id: md_list
padding: 0
'''
class SwipeToDeleteItem(MDCardSwipe):
text = StringProperty()
class TestCard(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
def build(self):
return self.screen
def remove_item(self, instance):
self.screen.ids.md_list.remove_widget(instance)
def on_start(self):
for i in range(20):
self.screen.ids.md_list.add_widget(
SwipeToDeleteItem(text=f"One-line item {i}")
)
TestCard().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/handdelete-mdcard-swipe.gif
:align: center
Focus behavior
-------------
.. code-block:: kv
MDCard:
focus_behavior: True
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-focus.gif
:align: center
Ripple behavior
---------------
.. code-block:: kv
MDCard:
ripple_behavior: True
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/card-behavior.gif
:align: center
End full code
-------------
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
KV = '''
<StarButton@MDIconButton>
icon: "star"
on_release: self.icon = "star-outline" if self.icon == "star" else "star"
Screen:
MDCard:
orientation: "vertical"
size_hint: .5, None
height: box_top.height + box_bottom.height
focus_behavior: True
ripple_behavior: True
pos_hint: {"center_x": .5, "center_y": .5}
MDBoxLayout:
id: box_top
spacing: "20dp"
adaptive_height: True
FitImage:
source: "/Users/macbookair/album.jpeg"
size_hint: .3, None
height: text_box.height
MDBoxLayout:
id: text_box
orientation: "vertical"
adaptive_height: True
spacing: "10dp"
padding: 0, "10dp", "10dp", "10dp"
MDLabel:
text: "Ride the Lightning"
theme_text_color: "Primary"
font_style: "H5"
bold: True
size_hint_y: None
height: self.texture_size[1]
MDLabel:
text: "July 27, 1984"
size_hint_y: None
height: self.texture_size[1]
theme_text_color: "Primary"
MDSeparator:
MDBoxLayout:
id: box_bottom
adaptive_height: True
padding: "10dp", 0, 0, 0
MDLabel:
text: "Rate this album"
size_hint_y: None
height: self.texture_size[1]
pos_hint: {"center_y": .5}
theme_text_color: "Primary"
StarButton:
StarButton:
StarButton:
StarButton:
StarButton:
'''
class Test(MDApp):
def build(self):
self.theme_cls.theme_style = "Dark"
return Builder.load_string(KV)
Test().run()
"""
__all__ = (
"MDCard",
"MDCardSwipe",
"MDCardSwipeFrontBox",
"MDCardSwipeLayerBox",
"MDSeparator",
)
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.relativelayout import RelativeLayout
from kivy.utils import get_color_from_hex
from kivymd import images_path
from kivymd.color_definitions import colors
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import (
BackgroundColorBehavior,
FocusBehavior,
RectangularElevationBehavior,
RectangularRippleBehavior,
)
Builder.load_string(
"""
<MDCardSwipeLayerBox>:
canvas:
Color:
rgba: app.theme_cls.divider_color
Rectangle:
size: self.size
pos: self.pos
<MDCard>
canvas:
RoundedRectangle:
size: self.size
pos: self.pos
source: root.background
<MDSeparator>
canvas:
Color:
rgba:
self.theme_cls.divider_color if not root.color else root.color
Rectangle:
size: self.size
pos: self.pos
"""
)
class MDSeparator(ThemableBehavior, BoxLayout):
"""A separator line."""
color = ListProperty()
"""Separator color in ``rgba`` format.
:attr:`color` is a :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.on_orientation()
def on_orientation(self, *args):
self.size_hint = (
(1, None) if self.orientation == "horizontal" else (None, 1)
)
if self.orientation == "horizontal":
self.height = dp(1)
else:
self.width = dp(1)
class MDCard(
ThemableBehavior,
BackgroundColorBehavior,
RectangularElevationBehavior,
FocusBehavior,
BoxLayout,
RectangularRippleBehavior,
):
background = StringProperty()
"""
Background image path.
:attr:`background` is a :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
focus_behavior = BooleanProperty(False)
"""
Using focus when hovering over a card.
:attr:`focus_behavior` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
ripple_behavior = BooleanProperty(False)
"""
Use ripple effect for card.
:attr:`ripple_behavior` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
elevation = NumericProperty(None, allownone=True)
"""
Elevation value.
:attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
and defaults to 1.
"""
_bg_color_map = (
get_color_from_hex(colors["Light"]["CardsDialogs"]),
get_color_from_hex(colors["Dark"]["CardsDialogs"]),
[1.0, 1.0, 1.0, 0.0],
)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.theme_cls.bind(theme_style=self.update_md_bg_color)
Clock.schedule_once(lambda x: self._on_elevation(self.elevation))
Clock.schedule_once(
lambda x: self._on_ripple_behavior(self.ripple_behavior)
)
self.update_md_bg_color(self, self.theme_cls.theme_style)
def update_md_bg_color(self, instance, value):
if self.md_bg_color in self._bg_color_map:
self.md_bg_color = get_color_from_hex(colors[value]["CardsDialogs"])
def on_radius(self, instance, value):
if self.radius != [0, 0, 0, 0]:
self.background = f"{images_path}/transparent.png"
def _on_elevation(self, value):
if value is None:
self.elevation = 6
else:
self.elevation = value
def _on_ripple_behavior(self, value):
self._no_ripple_effect = False if value else True
class MDCardSwipe(RelativeLayout):
"""
:Events:
:attr:`on_swipe_complete`
Called when a swipe of card is completed.
"""
open_progress = NumericProperty(0.0)
"""
Percent of visible part of side panel. The percent is specified as a
floating point number in the range 0-1. 0.0 if panel is closed and 1.0 if
panel is opened.
:attr:`open_progress` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.0`.
"""
opening_transition = StringProperty("out_cubic")
"""
The name of the animation transition type to use when animating to
the :attr:`state` `'opened'`.
:attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_cubic'`.
"""
closing_transition = StringProperty("out_sine")
"""
The name of the animation transition type to use when animating to
the :attr:`state` 'closed'.
:attr:`closing_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_sine'`.
"""
anchor = OptionProperty("left", options=("left", "right"))
"""
Anchoring screen edge for card. Available options are: `'left'`, `'right'`.
:attr:`anchor` is a :class:`~kivy.properties.OptionProperty`
and defaults to `left`.
"""
swipe_distance = NumericProperty(50)
"""
The distance of the swipe with which the movement of navigation drawer
begins.
:attr:`swipe_distance` is a :class:`~kivy.properties.NumericProperty`
and defaults to `50`.
"""
opening_time = NumericProperty(0.2)
"""
The time taken for the card to slide to the :attr:`state` `'open'`.
:attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
state = OptionProperty("closed", options=("closed", "opened"))
"""
Detailed state. Sets before :attr:`state`. Bind to :attr:`state` instead
of :attr:`status`. Available options are: `'closed'`, `'opened'`.
:attr:`status` is a :class:`~kivy.properties.OptionProperty`
and defaults to `'closed'`.
"""
max_swipe_x = NumericProperty(0.3)
"""
If, after the events of :attr:`~on_touch_up` card position exceeds this
value - will automatically execute the method :attr:`~open_card`,
and if not - will automatically be :attr:`~close_card` method.
:attr:`max_swipe_x` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.3`.
"""
max_opened_x = NumericProperty("100dp")
"""
The value of the position the card shifts to when :attr:`~type_swipe`
s set to `'hand'`.
:attr:`max_opened_x` is a :class:`~kivy.properties.NumericProperty`
and defaults to `100dp`.
"""
type_swipe = OptionProperty("hand", options=("auto", "hand"))
"""
Type of card opening when swipe. Shift the card to the edge or to
a set position :attr:`~max_opened_x`. Available options are:
`'auto'`, `'hand'`.
:attr:`type_swipe` is a :class:`~kivy.properties.OptionProperty`
and defaults to `auto`.
"""
_opens_process = False
_to_closed = True
def __init__(self, **kw):
self.register_event_type("on_swipe_complete")
super().__init__(**kw)
def _on_swipe_complete(self, *args):
self.dispatch("on_swipe_complete")
def add_widget(self, widget, index=0, canvas=None):
if isinstance(widget, (MDCardSwipeFrontBox, MDCardSwipeLayerBox)):
return super().add_widget(widget)
def on_swipe_complete(self, *args):
"""Called when a swipe of card is completed."""
def on_anchor(self, instance, value):
if value == "right":
self.open_progress = 1.0
else:
self.open_progress = 0.0
def on_open_progress(self, instance, value):
if self.anchor == "left":
self.children[0].x = self.width * value
else:
self.children[0].x = self.width * value - self.width
def on_touch_move(self, touch):
if self.collide_point(touch.x, touch.y):
expr = (
touch.x < self.swipe_distance
if self.anchor == "left"
else touch.x > self.width - self.swipe_distance
)
if expr and not self._opens_process:
self._opens_process = True
self._to_closed = False
if self._opens_process:
self.open_progress = max(
min(self.open_progress + touch.dx / self.width, 2.5), 0
)
return super().on_touch_move(touch)
def on_touch_up(self, touch):
if self.collide_point(touch.x, touch.y):
if not self._to_closed:
self._opens_process = False
self.complete_swipe()
return super().on_touch_up(touch)
def on_touch_down(self, touch):
if self.collide_point(touch.x, touch.y):
if self.state == "opened":
self._to_closed = True
self.close_card()
return super().on_touch_down(touch)
def complete_swipe(self):
expr = (
self.open_progress <= self.max_swipe_x
if self.anchor == "left"
else self.open_progress >= self.max_swipe_x
)
if expr:
self.close_card()
else:
self.open_card()
def open_card(self):
if self.type_swipe == "hand":
swipe_x = (
self.max_opened_x
if self.anchor == "left"
else -self.max_opened_x
)
else:
swipe_x = self.width if self.anchor == "left" else 0
anim = Animation(
x=swipe_x, t=self.opening_transition, d=self.opening_time
)
anim.bind(on_complete=self._on_swipe_complete)
anim.start(self.children[0])
self.state = "opened"
def close_card(self):
anim = Animation(x=0, t=self.closing_transition, d=self.opening_time)
anim.bind(on_complete=self._reset_open_progress)
anim.start(self.children[0])
self.state = "closed"
def _reset_open_progress(self, *args):
self.open_progress = 0.0 if self.anchor == "left" else 1.0
self._to_closed = False
self.dispatch("on_swipe_complete")
class MDCardSwipeFrontBox(MDCard):
pass
class MDCardSwipeLayerBox(BoxLayout):
pass

130
kivymd/uix/carousel.py Normal file
View file

@ -0,0 +1,130 @@
# TODO: Add documentation.
from kivy.animation import Animation
from kivy.uix.carousel import Carousel
class MDCarousel(Carousel):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.register_event_type("on_slide_progress")
self.register_event_type("on_slide_complete")
def on_slide_progress(self, *args):
pass
def on_slide_complete(self, *args):
pass
def _position_visible_slides(self, *args):
slides, index = self.slides, self.index
no_of_slides = len(slides) - 1
if not slides:
return
x, y, width, height = self.x, self.y, self.width, self.height
_offset, direction = self._offset, self.direction
_prev, _next, _current = self._prev, self._next, self._current
get_slide_container = self.get_slide_container
last_slide = get_slide_container(slides[-1])
first_slide = get_slide_container(slides[0])
skip_next = False
_loop = self.loop
if direction[0] in ["r", "l"]:
xoff = x + _offset
x_prev = {"l": xoff + width, "r": xoff - width}
x_next = {"l": xoff - width, "r": xoff + width}
if _prev:
_prev.pos = (x_prev[direction[0]], y)
elif _loop and _next and index == 0:
if (_offset > 0 and direction[0] == "r") or (
_offset < 0 and direction[0] == "l"
):
last_slide.pos = (x_prev[direction[0]], y)
skip_next = True
if _current:
_current.pos = (xoff, y)
self.dispatch("on_slide_progress", (xoff, y))
if skip_next:
return
if _next:
_next.pos = (x_next[direction[0]], y)
elif _loop and _prev and index == no_of_slides:
if (_offset < 0 and direction[0] == "r") or (
_offset > 0 and direction[0] == "l"
):
first_slide.pos = (x_next[direction[0]], y)
if direction[0] in ["t", "b"]:
yoff = y + _offset
y_prev = {"t": yoff - height, "b": yoff + height}
y_next = {"t": yoff + height, "b": yoff - height}
if _prev:
_prev.pos = (x, y_prev[direction[0]])
elif _loop and _next and index == 0:
if (_offset > 0 and direction[0] == "t") or (
_offset < 0 and direction[0] == "b"
):
last_slide.pos = (x, y_prev[direction[0]])
skip_next = True
if _current:
_current.pos = (x, yoff)
if skip_next:
return
if _next:
_next.pos = (x, y_next[direction[0]])
elif _loop and _prev and index == no_of_slides:
if (_offset < 0 and direction[0] == "t") or (
_offset > 0 and direction[0] == "b"
):
first_slide.pos = (x, y_next[direction[0]])
def _start_animation(self, *args, **kwargs):
# compute target offset for ease back, next or prev
new_offset = 0
direction = kwargs.get("direction", self.direction)[0]
is_horizontal = direction in "rl"
extent = self.width if is_horizontal else self.height
min_move = kwargs.get("min_move", self.min_move)
_offset = kwargs.get("offset", self._offset)
if _offset < min_move * -extent:
new_offset = -extent
elif _offset > min_move * extent:
new_offset = extent
# if new_offset is 0, it wasnt enough to go next/prev
dur = self.anim_move_duration
if new_offset == 0:
dur = self.anim_cancel_duration
# detect edge cases if not looping
len_slides = len(self.slides)
index = self.index
if not self.loop or len_slides == 1:
is_first = index == 0
is_last = index == len_slides - 1
if direction in "rt":
towards_prev = new_offset > 0
towards_next = new_offset < 0
else:
towards_prev = new_offset < 0
towards_next = new_offset > 0
if (is_first and towards_prev) or (is_last and towards_next):
new_offset = 0
anim = Animation(_offset=new_offset, d=dur, t=self.anim_type)
anim.cancel_all(self)
def _cmp(*args):
self.dispatch(
"on_slide_complete",
self.previous_slide,
self.current_slide,
self.next_slide,
)
if self._skip_slide is not None:
self.index = self._skip_slide
self._skip_slide = None
anim.bind(on_complete=_cmp)
anim.start(self)

275
kivymd/uix/chip.py Executable file
View file

@ -0,0 +1,275 @@
"""
Components/Chip
===============
.. seealso::
`Material Design spec, Chips <https://material.io/components/chips>`_
.. rubric:: Chips are compact elements that represent an input, attribute, or action.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chips.png
:align: center
Usage
-----
.. code-block:: kv
MDChip:
label: 'Coffee'
color: .4470588235118, .1960787254902, 0, 1
icon: 'coffee'
callback: app.callback_for_menu_items
The user function takes two arguments - the object and the text of the chip:
.. code-block:: python
def callback_for_menu_items(self, instance, value):
print(instance, value)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ordinary-chip.png
:align: center
Use custom icon
---------------
.. code-block:: kv
MDChip:
label: 'Kivy'
icon: 'data/logo/kivy-icon-256.png'
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-custom-icon.png
:align: center
Use without icon
----------------
.. code-block:: kv
MDChip:
label: 'Without icon'
icon: ''
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-without-icon.png
:align: center
Chips with check
----------------
.. code-block:: kv
MDChip:
label: 'Check with icon'
icon: 'city'
check: True
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-check-icon.gif
:align: center
Choose chip
-----------
.. code-block:: kv
MDChooseChip:
MDChip:
label: 'Earth'
icon: 'earth'
selected_chip_color: .21176470535294, .098039627451, 1, 1
MDChip:
label: 'Face'
icon: 'face'
selected_chip_color: .21176470535294, .098039627451, 1, 1
MDChip:
label: 'Facebook'
icon: 'facebook'
selected_chip_color: .21176470535294, .098039627451, 1, 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-shoose-icon.gif
:align: center
.. Note:: `See full example <https://github.com/kivymd/KivyMD/wiki/Components-Chip>`_
"""
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
StringProperty,
)
from kivy.uix.boxlayout import BoxLayout
from kivymd.theming import ThemableBehavior
from kivymd.uix.button import MDIconButton
from kivymd.uix.stacklayout import MDStackLayout
Builder.load_string(
"""
#:import DEVICE_TYPE kivymd.material_resources.DEVICE_TYPE
<MDChooseChip>
adaptive_height: True
spacing: "5dp"
<MDChip>
size_hint: None, None
height: "26dp"
padding: 0, 0, "5dp", 0
width:
self.minimum_width - (dp(10) if DEVICE_TYPE == "desktop" else dp(20)) \
if root.icon != 'checkbox-blank-circle' else self.minimum_width
canvas:
Color:
rgba: root.color
RoundedRectangle:
pos: self.pos
size: self.size
radius: [root.radius]
MDBoxLayout:
id: box_check
adaptive_size: True
pos_hint: {'center_y': .5}
MDBoxLayout:
adaptive_width: True
padding: dp(10)
Label:
id: label
text: root.label
size_hint_x: None
width: self.texture_size[0]
color: root.text_color if root.text_color else (root.theme_cls.text_color)
MDIconButton:
id: icon
icon: root.icon
size_hint_y: None
height: "20dp"
pos_hint: {"center_y": .5}
user_font_size: "20dp"
disabled: True
md_bg_color_disabled: 0, 0, 0, 0
"""
)
class MDChip(BoxLayout, ThemableBehavior):
label = StringProperty()
"""Chip text.
:attr:`label` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
icon = StringProperty("checkbox-blank-circle")
"""Chip icon.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `'checkbox-blank-circle'`.
"""
color = ListProperty()
"""Chip color in ``rgba`` format.
:attr:`color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
text_color = ListProperty()
"""Chip's text color in ``rgba`` format.
:attr:`text_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
check = BooleanProperty(False)
"""
If True, a checkmark is added to the left when touch to the chip.
:attr:`check` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
callback = ObjectProperty()
"""Custom method.
:attr:`callback` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
radius = NumericProperty("12dp")
"""Corner radius values.
:attr:`radius` is an :class:`~kivy.properties.NumericProperty`
and defaults to `'12dp'`.
"""
selected_chip_color = ListProperty()
"""The color of the chip that is currently selected in ``rgba`` format.
:attr:`selected_chip_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.color:
self.color = self.theme_cls.primary_color
def on_icon(self, instance, value):
if value == "":
self.icon = "checkbox-blank-circle"
self.remove_widget(self.ids.icon)
def on_touch_down(self, touch):
if self.collide_point(*touch.pos):
md_choose_chip = self.parent
if self.selected_chip_color:
Animation(
color=self.theme_cls.primary_dark
if not self.selected_chip_color
else self.selected_chip_color,
d=0.3,
).start(self)
if issubclass(md_choose_chip.__class__, MDChooseChip):
for chip in md_choose_chip.children:
if chip is not self:
chip.color = self.theme_cls.primary_color
if self.check:
if not len(self.ids.box_check.children):
self.ids.box_check.add_widget(
MDIconButton(
icon="check",
size_hint_y=None,
height=dp(20),
disabled=True,
user_font_size=dp(20),
pos_hint={"center_y": 0.5},
)
)
else:
check = self.ids.box_check.children[0]
self.ids.box_check.remove_widget(check)
if self.callback:
self.callback(self, self.label)
class MDChooseChip(MDStackLayout):
def add_widget(self, widget, index=0, canvas=None):
if isinstance(widget, MDChip):
return super().add_widget(widget)

971
kivymd/uix/datatables.py Normal file
View file

@ -0,0 +1,971 @@
"""
Components/DataTables
=====================
.. seealso::
`Material Design spec, DataTables <https://material.io/components/data-tables>`_
.. rubric:: Data tables display sets of data across rows and columns.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-previous.png
:align: center
.. warning::
Data tables are still far from perfect. Errors are possible and we hope
you inform us about them.
"""
# Special thanks for the info -
# https://stackoverflow.com/questions/50219281/python-how-to-add-vertical-scroll-in-recycleview
__all__ = ("MDDataTable",)
from kivy import Logger
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
DictProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.behaviors import ButtonBehavior, FocusBehavior
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.recyclegridlayout import RecycleGridLayout
from kivy.uix.recycleview import RecycleView
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.scrollview import ScrollView
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import HoverBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.dialog import BaseDialog
from kivymd.uix.menu import MDDropdownMenu
from kivymd.uix.tooltip import MDTooltip
Builder.load_string(
"""
#:import DEVICE_TYPE kivymd.material_resources.DEVICE_TYPE
#:import StiffScrollEffect kivymd.stiffscroll.StiffScrollEffect
<CellRow>
orientation: "vertical"
canvas.before:
Color:
rgba:
(root.theme_cls.bg_darkest if root.theme_cls.theme_style == "Light" else root.theme_cls.bg_light) \
if self.selected else root.theme_cls.bg_normal
Rectangle:
pos: self.pos
size: self.size
on_press: if DEVICE_TYPE != "desktop": root.table.on_mouse_select(self)
on_enter: if DEVICE_TYPE == "desktop": root.table.on_mouse_select(self)
MDBoxLayout:
id: box
padding: "8dp", "8dp", 0, "8dp"
spacing: "16dp"
MDCheckbox:
id: check
size_hint: None, None
size: 0, 0
opacity: 0
on_active: root.select_check(self.active)
MDLabel:
id: label
text: " " + root.text
color: (1, 1, 1, 1) if root.theme_cls.theme_style == "Dark" else (0, 0, 0, 1)
MDSeparator:
<CellHeader>
orientation: "vertical"
size_hint_y: None
height: self.minimum_height
spacing: "4dp"
tooltip_text: root.text
MDLabel:
text: " " + root.text
size_hint_y: None
height: self.texture_size[1]
bold: True
color: (1, 1, 1, 1) if root.theme_cls.theme_style == "Dark" else (0, 0, 0, 1)
MDSeparator:
id: separator
<TableHeader>
bar_width: 0
do_scroll: False
size_hint: 1, None
height: header.height
MDGridLayout:
id: header
rows: 1
cols_minimum: root.cols_minimum
adaptive_size: True
padding: 0, "8dp", 0, 0
MDBoxLayout:
orientation: "vertical"
MDBoxLayout:
id: box
padding: "8dp", "8dp", "4dp", 0
spacing: "16dp"
MDCheckbox:
id: check
size_hint: None, None
size: 0, 0
opacity: 0
on_active: root.table_data.select_all(self.state)
disabled: True
#MDIconButton:
# id: sort_button
# icon: "menu-up"
# pos_hint: {"center_y": 1}
# ripple_scale: .65
# on_release: root.table_data.sort_by_name()
CellHeader:
id: first_cell
MDSeparator:
<TableData>
data: root.recycle_data
data_first_cells: root.data_first_cells
key_viewclass: "viewclass"
effect_cls: StiffScrollEffect
TableRecycleGridLayout:
id: row_controller
key_selection: "selectable"
cols: root.total_col_headings
cols_minimum: root.cols_minimum
default_size: None, dp(52)
default_size_hint: 1, None
size_hint: None, None
height: self.minimum_height
width: self.minimum_width
orientation: "vertical"
multiselect: True
touch_multiselect: True
<TablePagination>
adaptive_height: True
spacing: "8dp"
Widget:
MDLabel:
text: "Rows per page"
size_hint: None, 1
width: self.texture_size[0]
text_size: None, None
font_style: "Caption"
color: (1, 1, 1, 1) if root.theme_cls.theme_style == "Dark" else (0, 0, 0, 1)
MDDropDownItem:
id: drop_item
pos_hint: {'center_y': .5}
text: str(root.table_data.rows_num)
font_size: "14sp"
on_release: root.table_data.open_pagination_menu()
Widget:
size_hint_x: None
width: "32dp"
MDLabel:
id: label_rows_per_page
text: f"1-{root.table_data.rows_num} of {len(root.table_data.row_data)}"
size_hint: None, 1
#width: self.texture_size[0]
text_size: None, None
font_style: "Caption"
color: (1, 1, 1, 1) if root.theme_cls.theme_style == "Dark" else (0, 0, 0, 1)
MDIconButton:
id: button_back
icon: "chevron-left"
user_font_size: "20sp"
pos_hint: {'center_y': .5}
disabled: True
on_release: root.table_data.set_next_row_data_parts("back")
MDIconButton:
id: button_forward
icon: "chevron-right"
user_font_size: "20sp"
pos_hint: {'center_y': .5}
on_release: root.table_data.set_next_row_data_parts("forward")
<MDDataTable>
MDCard:
id: container
orientation: "vertical"
elevation: 14
md_bg_color: 0, 0, 0, 0
padding: "24dp", "24dp", "8dp", "8dp"
canvas:
Color:
rgba: root.theme_cls.bg_normal
RoundedRectangle:
pos: self.pos
size: self.size
"""
)
class TableRecycleGridLayout(
FocusBehavior, LayoutSelectionBehavior, RecycleGridLayout
):
selected_row = NumericProperty(0)
table_data = ObjectProperty(None)
def get_nodes(self):
nodes = self.get_selectable_nodes()
if self.nodes_order_reversed:
nodes = nodes[::-1]
if not nodes:
return None, None
selected = self.selected_nodes
if not selected: # nothing selected, select the first
self.selected_row = 0
self.select_row(nodes)
return None, None
if len(nodes) == 1: # the only selectable node is selected already
return None, None
index = selected[-1]
if index > len(nodes):
last = len(nodes)
else:
last = nodes.index(index)
self.clear_selection()
return last, nodes
def select_next(self, instance):
"""Select next row."""
self.table_data = instance
last, nodes = self.get_nodes()
if not nodes:
return
if last == len(nodes) - 1:
self.selected_row = nodes[0]
else:
self.selected_row = nodes[last + 1]
self.selected_row += self.table_data.total_col_headings
self.select_row(nodes)
def select_current(self, instance):
"""Select current row."""
self.table_data = instance
last, nodes = self.get_nodes()
if not nodes:
return
self.select_row(nodes)
def select_row(self, nodes):
col = self.table_data.recycle_data[self.selected_row]["range"]
for x in range(col[0], col[1] + 1):
self.select_node(nodes[x])
class CellRow(
ThemableBehavior,
RecycleDataViewBehavior,
HoverBehavior,
ButtonBehavior,
BoxLayout,
):
text = StringProperty() # row text
table = ObjectProperty() # <TableData object>
index = None
selected = BooleanProperty(False)
selectable = BooleanProperty(True)
def on_table(self, instance, table):
"""Sets padding/spacing to zero if no checkboxes are used for rows."""
if not table.check:
self.ids.box.padding = 0
self.ids.box.spacing = 0
def refresh_view_attrs(self, table_data, index, data):
"""
Called by the :class:`RecycleAdapter` when the view is initially
populated with the values from the `data` dictionary for this item.
Any pos or size info should be removed because they are set
subsequently with :attr:`refresh_view_layout`.
:Parameters:
`table_data`: :class:`TableData` instance
The :class:`TableData` that caused the update.
`data`: dict
The data dict used to populate this view.
"""
self.index = index
return super().refresh_view_attrs(table_data, index, data)
def on_touch_down(self, touch):
if super().on_touch_down(touch):
if self.table._parent:
self.table._parent.dispatch("on_row_press", self)
return True
def apply_selection(self, table_data, index, is_selected):
"""Called when list items of table appear on the screen."""
self.selected = is_selected
# Set checkboxes.
if table_data.check:
if self.text in table_data.data_first_cells:
self.ids.check.size = (dp(32), dp(32))
self.ids.check.opacity = 1
self.ids.box.spacing = dp(16)
self.ids.box.padding[0] = dp(8)
else:
self.ids.check.size = (0, 0)
self.ids.check.opacity = 0
self.ids.box.spacing = 0
self.ids.box.padding[0] = 0
# Set checkboxes state.
if table_data._rows_number in table_data.current_selection_check:
for index in table_data.current_selection_check[
table_data._rows_number
]:
if (
self.index
in table_data.current_selection_check[
table_data._rows_number
]
):
self.ids.check.state = "down"
else:
self.ids.check.state = "normal"
else:
self.ids.check.state = "normal"
def select_check(self, active):
"""Called upon activation/deactivation of the checkbox."""
if active and self.index not in self.table.current_selection_check:
if (
self.table._rows_number
not in self.table.current_selection_check
):
self.table.current_selection_check[self.table._rows_number] = []
if (
self.index
not in self.table.current_selection_check[
self.table._rows_number
]
):
self.table.current_selection_check[
self.table._rows_number
].append(self.index)
else:
if self.table._rows_number in self.table.current_selection_check:
if (
self.index
in self.table.current_selection_check[
self.table._rows_number
]
and not active
):
self.table.current_selection_check[
self.table._rows_number
].remove(self.index)
self.table.get_select_row(self.index)
class CellHeader(MDTooltip, BoxLayout):
text = StringProperty() # column text
class TableHeader(ScrollView):
table_data = ObjectProperty() # <TableData object>
column_data = ListProperty() # MDDataTable.column_data
col_headings = ListProperty() # column names list
sort = BooleanProperty(False) # MDDataTable.sort
# kivy.uix.gridlayout.GridLayout.cols_minimum
cols_minimum = DictProperty()
def __init__(self, **kwargs):
super().__init__(**kwargs)
# Create cells.
for i, col_heading in enumerate(self.column_data):
self.cols_minimum[i] = col_heading[1] * 5
self.col_headings.append(col_heading[0])
if i:
self.ids.header.add_widget(
CellHeader(text=col_heading[0], width=self.cols_minimum[i])
)
else:
# Sets the text in the first cell.
self.ids.first_cell.text = col_heading[0]
self.ids.first_cell.ids.separator.height = 0
self.ids.first_cell.width = self.cols_minimum[i]
def on_table_data(self, instance, value):
"""Sets the checkbox in the first cell."""
if self.table_data.check:
self.ids.check.size = (dp(32), dp(32))
self.ids.check.opacity = 1
else:
self.ids.box.padding[0] = 0
self.ids.box.spacing = 0
def on_sort(self, instance, value):
"""Rows sorting method."""
Logger.info("TableData: Sorting table items is not implemented")
# if not self.sort:
# self.ids.sort_button.size = (0, 0)
# self.ids.sort_button.opacity = 0
class TableData(RecycleView):
recycle_data = ListProperty() # kivy.uix.recycleview.RecycleView.data
data_first_cells = ListProperty() # list of first row cells
row_data = ListProperty() # MDDataTable.row_data
total_col_headings = NumericProperty(0) # TableHeader.col_headings
cols_minimum = DictProperty() # TableHeader.cols_minimum
table_header = ObjectProperty() # <TableHeader object>
pagination_menu = ObjectProperty() # <MDDropdownMenu object>
pagination = ObjectProperty() # <TablePagination object>
check = ObjectProperty() # MDDataTable.check
rows_num = NumericProperty() # number of rows displayed on the table page
# Open or close the menu for selecting the number of rows displayed
# on the table page.
pagination_menu_open = BooleanProperty(False)
# List of indexes of marked checkboxes.
current_selection_check = DictProperty()
sort = BooleanProperty()
_parent = ObjectProperty()
_rows_number = NumericProperty(0)
_rows_num = NumericProperty()
_current_value = NumericProperty(1)
_to_value = NumericProperty()
_row_data_parts = ListProperty()
def __init__(self, table_header, **kwargs):
super().__init__(**kwargs)
self.table_header = table_header
self.total_col_headings = len(table_header.col_headings)
self.cols_minimum = table_header.cols_minimum
self.set_row_data()
Clock.schedule_once(self.set_default_first_row, 0)
def get_select_row(self, index):
"""Returns the current row with all elements."""
row = []
for data in self.recycle_data:
if index in data["range"]:
row.append(data["text"])
self._parent.dispatch("on_check_press", row)
def set_default_first_row(self, dt):
"""Set default first row as selected."""
self.ids.row_controller.select_next(self)
def sort_by_name(self):
"""Sorts table data."""
# TODO: implement a rows sorting method.
def set_row_data(self):
data = []
low = 0
high = self.total_col_headings - 1
self.recycle_data = []
self.data_first_cells = []
if self._row_data_parts:
# for row in self.row_data:
for row in self._row_data_parts[self._rows_number]:
for i in range(len(row)):
data.append([row[i], row[0], [low, high]])
low += self.total_col_headings
high += self.total_col_headings
for j, x in enumerate(data):
if x[0] == x[1]:
self.data_first_cells.append(x[0])
self.recycle_data.append(
{
"text": str(x[0]),
"Index": str(x[1]),
"range": x[2],
"selectable": True,
"viewclass": "CellRow",
"table": self,
}
)
if not self.table_header.column_data:
raise ValueError("Set value for column_data in class TableData")
self.data_first_cells.append(self.table_header.column_data[0][0])
def set_text_from_of(self, direction):
"""Sets the text of the numbers of displayed pages in table."""
if self.pagination:
if direction == "forward":
if (
len(self._row_data_parts[self._rows_number])
< self._to_value
):
self._current_value = self._current_value + self.rows_num
else:
self._current_value = self._current_value + len(
self._row_data_parts[self._rows_number]
)
self._to_value = self._to_value + len(
self._row_data_parts[self._rows_number]
)
if direction == "back":
self._current_value = self._current_value - len(
self._row_data_parts[self._rows_number]
)
self._to_value = self._to_value - len(
self._row_data_parts[self._rows_number + 1]
)
if direction == "increment":
self._current_value = 1
self._to_value = self.rows_num + self._current_value - 1
self.pagination.ids.label_rows_per_page.text = f"{self._current_value}-{self._to_value} of {len(self.row_data)}"
def select_all(self, state):
"""Sets the checkboxes of all rows to the active/inactive position."""
# FIXME: fix the work of selecting all cells..
for i, data in enumerate(self.recycle_data):
opts = self.layout_manager.view_opts
cell_row_obj = self.view_adapter.get_view(
i, self.data[i], opts[i]["viewclass"]
)
cell_row_obj.ids.check.state = state
self.on_mouse_select(cell_row_obj)
cell_row_obj.select_check(True if state == "down" else False)
def close_pagination_menu(self, *args):
"""Called when the pagination menu window is closed."""
self.pagination_menu_open = False
def open_pagination_menu(self):
"""Open pagination menu window."""
if self.pagination_menu.items:
self.pagination_menu_open = True
self.pagination_menu.open()
def set_number_displayed_lines(self, instance_menu, instance_menu_item):
"""
Called when the user sets the number of pages displayed
in the table.
"""
self.rows_num = int(instance_menu_item.text)
self.set_row_data()
self.set_text_from_of("increment")
def set_next_row_data_parts(self, direction):
"""Called when switching the pages of the table."""
if direction == "forward":
self._rows_number += 1
self.pagination.ids.button_back.disabled = False
elif direction == "back":
self._rows_number -= 1
self.pagination.ids.button_forward.disabled = False
self.set_row_data()
self.set_text_from_of(direction)
if self._to_value == len(self.row_data):
self.pagination.ids.button_forward.disabled = True
if self._current_value == 1:
self.pagination.ids.button_back.disabled = True
def _split_list_into_equal_parts(self, lst, parts):
for i in range(0, len(lst), parts):
yield lst[i : i + parts]
def on_mouse_select(self, instance):
"""Called on the ``on_enter`` event of the :class:`~CellRow` class"""
if not self.pagination_menu_open:
if self.ids.row_controller.selected_row != instance.index:
self.ids.row_controller.selected_row = instance.index
self.ids.row_controller.select_current(self)
def on_rows_num(self, instance, value):
if not self._to_value:
self._to_value = value
self._rows_number = 0
self._row_data_parts = list(
self._split_list_into_equal_parts(self.row_data, value)
)
# def on_pagination(self, instance_table, instance_pagination):
# if len(self._row_data_parts) <= self._to_value:
# instance_pagination.ids.button_forward.disabled = True
class TablePagination(ThemableBehavior, MDBoxLayout):
"""Pagination Container."""
table_data = ObjectProperty() # <TableData object>
class MDDataTable(BaseDialog):
"""
:Events:
:attr:`on_row_press`
Called when a table row is clicked.
:attr:`on_check_press`
Called when the check box in the table row is checked.
.. rubric:: Use events as follows
.. code-block:: python
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.datatables import MDDataTable
class Example(MDApp):
def build(self):
self.data_tables = MDDataTable(
size_hint=(0.9, 0.6),
use_pagination=True,
check=True,
column_data=[
("No.", dp(30)),
("Column 1", dp(30)),
("Column 2", dp(30)),
("Column 3", dp(30)),
("Column 4", dp(30)),
("Column 5", dp(30)),
],
row_data=[
(f"{i + 1}", "2.23", "3.65", "44.1", "0.45", "62.5")
for i in range(50)
],
)
self.data_tables.bind(on_row_press=self.on_row_press)
self.data_tables.bind(on_check_press=self.on_check_press)
def on_start(self):
self.data_tables.open()
def on_row_press(self, instance_table, instance_row):
'''Called when a table row is clicked.'''
print(instance_table, instance_row)
def on_check_press(self, instance_table, current_row):
'''Called when the check box in the table row is checked.'''
print(instance_table, current_row)
Example().run()
"""
column_data = ListProperty()
"""
Data for header columns.
.. code-block:: python
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.datatables import MDDataTable
class Example(MDApp):
def build(self):
self.data_tables = MDDataTable(
size_hint=(0.9, 0.6),
# name column, width column
column_data=[
("Column 1", dp(30)),
("Column 2", dp(30)),
("Column 3", dp(30)),
("Column 4", dp(30)),
("Column 5", dp(30)),
("Column 6", dp(30)),
],
)
def on_start(self):
self.data_tables.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-column-data.png
:align: center
:attr:`column_data` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
row_data = ListProperty()
"""
Data for rows.
.. code-block:: python
from kivy.metrics import dp
from kivymd.app import MDApp
from kivymd.uix.datatables import MDDataTable
class Example(MDApp):
def build(self):
self.data_tables = MDDataTable(
size_hint=(0.9, 0.6),
column_data=[
("Column 1", dp(30)),
("Column 2", dp(30)),
("Column 3", dp(30)),
("Column 4", dp(30)),
("Column 5", dp(30)),
("Column 6", dp(30)),
],
row_data=[
# The number of elements must match the length
# of the `column_data` list.
("1", "2", "3", "4", "5", "6"),
("1", "2", "3", "4", "5", "6"),
],
)
def on_start(self):
self.data_tables.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-row-data.png
:align: center
:attr:`row_data` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
sort = BooleanProperty(False)
"""
Whether to display buttons for sorting table items.
:attr:`sort` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
check = BooleanProperty(False)
"""
Use or not use checkboxes for rows.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-check.gif
:align: center
:attr:`check` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
use_pagination = BooleanProperty(False)
"""
Use page pagination for table or not.
.. code-block:: python
from kivymd.app import MDApp
from kivymd.uix.datatables import MDDataTable
class Example(MDApp):
def build(self):
self.data_tables = MDDataTable(
size_hint=(0.9, 0.6),
use_pagination=True,
column_data=[
("No.", dp(30)),
("Column 1", dp(30)),
("Column 2", dp(30)),
("Column 3", dp(30)),
("Column 4", dp(30)),
("Column 5", dp(30)),
],
row_data=[
(f"{i + 1}", "1", "2", "3", "4", "5") for i in range(50)
],
)
def on_start(self):
self.data_tables.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-use-pagination.png
:align: center
:attr:`use_pagination` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
rows_num = NumericProperty(5)
"""
The number of rows displayed on one page of the table.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-use-pagination.gif
:align: center
:attr:`rows_num` is an :class:`~kivy.properties.NumericProperty`
and defaults to `10`.
"""
pagination_menu_pos = OptionProperty("center", options=["center", "auto"])
"""
Menu position for selecting the number of displayed rows.
Available options are `'center'`, `'auto'`.
.. rubric:: Center
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-menu-pos-center.png
:align: center
.. rubric:: Auto
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-menu-pos-auto.png
:align: center
:attr:`pagination_menu_pos` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'center'`.
"""
pagination_menu_height = NumericProperty("140dp")
"""
Menu height for selecting the number of displayed rows.
.. rubric:: 140dp
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-menu-height-140.png
:align: center
.. rubric:: 240dp
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-menu-height-240.png
:align: center
:attr:`pagination_menu_height` is an :class:`~kivy.properties.NumericProperty`
and defaults to `'140dp'`.
"""
background_color = ListProperty([0, 0, 0, 0])
"""
Background color in the format (r, g, b, a).
See :attr:`~kivy.uix.modalview.ModalView.background_color`.
:attr:`background_color` is a :class:`~kivy.properties.ListProperty` and
defaults to [0, 0, 0, .7].
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.register_event_type("on_row_press")
self.register_event_type("on_check_press")
self.header = TableHeader(column_data=self.column_data, sort=self.sort)
self.table_data = TableData(
self.header,
row_data=self.row_data,
check=self.check,
rows_num=self.rows_num,
_parent=self,
)
self.pagination = TablePagination(table_data=self.table_data)
self.table_data.pagination = self.pagination
self.header.table_data = self.table_data
self.table_data.fbind("scroll_x", self._scroll_with_header)
self.ids.container.add_widget(self.header)
self.ids.container.add_widget(self.table_data)
if self.use_pagination:
self.ids.container.add_widget(self.pagination)
Clock.schedule_once(self.create_pagination_menu, 0.5)
def on_row_press(self, *args):
"""Called when a table row is clicked."""
def on_check_press(self, *args):
"""Called when the check box in the table row is checked."""
def _scroll_with_header(self, instance, value):
self.header.scroll_x = value
def create_pagination_menu(self, interval):
menu_items = [
{"text": f"{i}"}
for i in range(
self.rows_num, len(self.row_data) // 2, self.rows_num
)
]
pagination_menu = MDDropdownMenu(
caller=self.pagination.ids.drop_item,
items=menu_items,
position=self.pagination_menu_pos,
max_height=self.pagination_menu_height,
width_mult=2,
)
pagination_menu.bind(
on_release=self.table_data.set_number_displayed_lines,
on_dismiss=self.table_data.close_pagination_menu,
)
self.table_data.pagination_menu = pagination_menu

603
kivymd/uix/dialog.py Executable file
View file

@ -0,0 +1,603 @@
"""
Components/Dialog
=================
.. seealso::
`Material Design spec, Dialogs <https://material.io/components/dialogs>`_
.. rubric:: Dialogs inform users about a task and can contain critical
information, require decisions, or involve multiple tasks.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialogs.png
:align: center
Usage
-----
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
KV = '''
FloatLayout:
MDFlatButton:
text: "ALERT DIALOG"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: app.show_alert_dialog()
'''
class Example(MDApp):
dialog = None
def build(self):
return Builder.load_string(KV)
def show_alert_dialog(self):
if not self.dialog:
self.dialog = MDDialog(
text="Discard draft?",
buttons=[
MDFlatButton(
text="CANCEL", text_color=self.theme_cls.primary_color
),
MDFlatButton(
text="DISCARD", text_color=self.theme_cls.primary_color
),
],
)
self.dialog.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/alert-dialog.png
:align: center
"""
__all__ = ("MDDialog",)
from kivy.clock import Clock
from kivy.core.window import Window
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.modalview import ModalView
from kivymd.material_resources import DEVICE_TYPE
from kivymd.theming import ThemableBehavior
from kivymd.uix.button import BaseButton
from kivymd.uix.card import MDSeparator
from kivymd.uix.list import BaseListItem
Builder.load_string(
"""
#:import images_path kivymd.images_path
<BaseDialog>
background: '{}/transparent.png'.format(images_path)
canvas.before:
PushMatrix
RoundedRectangle:
pos: self.pos
size: self.size
radius: [5]
Scale:
origin: self.center
x: root._scale_x
y: root._scale_y
canvas.after:
PopMatrix
<MDDialog>
MDCard:
id: container
orientation: "vertical"
size_hint_y: None
height: self.minimum_height
elevation: 4
md_bg_color: 0, 0, 0, 0
padding: "24dp", "24dp", "8dp", "8dp"
canvas:
Color:
rgba: root.md_bg_color
RoundedRectangle:
pos: self.pos
size: self.size
radius: root.radius
MDLabel:
id: title
text: root.title
font_style: "H6"
bold: True
markup: True
size_hint_y: None
height: self.texture_size[1]
valign: "top"
BoxLayout:
id: spacer_top_box
size_hint_y: None
height: root._spacer_top
MDLabel:
id: text
text: root.text
font_style: "Body1"
theme_text_color: "Custom"
text_color: root.theme_cls.disabled_hint_text_color
size_hint_y: None
height: self.texture_size[1]
markup: True
ScrollView:
id: scroll
size_hint_y: None
height: root._scroll_height
MDGridLayout:
id: box_items
adaptive_height: True
cols: 1
BoxLayout:
id: spacer_bottom_box
size_hint_y: None
height: self.minimum_height
AnchorLayout:
id: root_button_box
size_hint_y: None
height: "52dp"
anchor_x: "right"
MDBoxLayout:
id: button_box
adaptive_size: True
spacing: "8dp"
"""
)
class BaseDialog(ThemableBehavior, ModalView):
_scale_x = NumericProperty(1)
_scale_y = NumericProperty(1)
class MDDialog(BaseDialog):
title = StringProperty()
"""
Title dialog.
.. code-block:: python
self.dialog = MDDialog(
title="Reset settings?",
buttons=[
MDFlatButton(
text="CANCEL", text_color=self.theme_cls.primary_color
),
MDFlatButton(
text="ACCEPT", text_color=self.theme_cls.primary_color
),
],
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-title.png
:align: center
:attr:`title` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
text = StringProperty()
"""
Text dialog.
.. code-block:: python
self.dialog = MDDialog(
title="Reset settings?",
text="This will reset your device to its default factory settings.",
buttons=[
MDFlatButton(
text="CANCEL", text_color=self.theme_cls.primary_color
),
MDFlatButton(
text="ACCEPT", text_color=self.theme_cls.primary_color
),
],
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-text.png
:align: center
:attr:`text` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
radius = ListProperty([7, 7, 7, 7])
"""
Dialog corners rounding value.
.. code-block:: python
self.dialog = MDDialog(
text="Oops! Something seems to have gone wrong!",
radius=[20, 7, 20, 7],
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-radius.png
:align: center
:attr:`radius` is an :class:`~kivy.properties.ListProperty`
and defaults to `[7, 7, 7, 7]`.
"""
buttons = ListProperty()
"""
List of button objects for dialog.
Objects must be inherited from :class:`~kivymd.uix.button.BaseButton` class.
.. code-block:: python
self.dialog = MDDialog(
text="Discard draft?",
buttons=[
MDFlatButton(text="CANCEL"), MDRaisedButton(text="DISCARD"),
],
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-buttons.png
:align: center
:attr:`buttons` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
items = ListProperty()
"""
List of items objects for dialog.
Objects must be inherited from :class:`~kivymd.uix.list.BaseListItem` class.
With type 'simple'
-----------------
.. code-block:: python
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivymd.app import MDApp
from kivymd.uix.dialog import MDDialog
from kivymd.uix.list import OneLineAvatarListItem
KV = '''
<Item>
ImageLeftWidget:
source: root.source
FloatLayout:
MDFlatButton:
text: "ALERT DIALOG"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: app.show_simple_dialog()
'''
class Item(OneLineAvatarListItem):
divider = None
source = StringProperty()
class Example(MDApp):
dialog = None
def build(self):
return Builder.load_string(KV)
def show_simple_dialog(self):
if not self.dialog:
self.dialog = MDDialog(
title="Set backup account",
type="simple",
items=[
Item(text="user01@gmail.com", source="user-1.png"),
Item(text="user02@gmail.com", source="user-2.png"),
Item(text="Add account", source="add-icon.png"),
],
)
self.dialog.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-items.png
:align: center
With type 'confirmation'
-----------------------
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
from kivymd.uix.list import OneLineAvatarIconListItem
KV = '''
<ItemConfirm>
on_release: root.set_icon(check)
CheckboxLeftWidget:
id: check
group: "check"
FloatLayout:
MDFlatButton:
text: "ALERT DIALOG"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: app.show_confirmation_dialog()
'''
class ItemConfirm(OneLineAvatarIconListItem):
divider = None
def set_icon(self, instance_check):
instance_check.active = True
check_list = instance_check.get_widgets(instance_check.group)
for check in check_list:
if check != instance_check:
check.active = False
class Example(MDApp):
dialog = None
def build(self):
return Builder.load_string(KV)
def show_confirmation_dialog(self):
if not self.dialog:
self.dialog = MDDialog(
title="Phone ringtone",
type="confirmation",
items=[
ItemConfirm(text="Callisto"),
ItemConfirm(text="Luna"),
ItemConfirm(text="Night"),
ItemConfirm(text="Solo"),
ItemConfirm(text="Phobos"),
ItemConfirm(text="Diamond"),
ItemConfirm(text="Sirena"),
ItemConfirm(text="Red music"),
ItemConfirm(text="Allergio"),
ItemConfirm(text="Magic"),
ItemConfirm(text="Tic-tac"),
],
buttons=[
MDFlatButton(
text="CANCEL", text_color=self.theme_cls.primary_color
),
MDFlatButton(
text="OK", text_color=self.theme_cls.primary_color
),
],
)
self.dialog.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-confirmation.png
:align: center
:attr:`items` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
type = OptionProperty(
"alert", options=["alert", "simple", "confirmation", "custom"]
)
"""
Dialog type.
Available option are `'alert'`, `'simple'`, `'confirmation'`, `'custom'`.
:attr:`type` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'alert'`.
"""
content_cls = ObjectProperty()
"""
Custom content class.
.. code-block::
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivymd.app import MDApp
from kivymd.uix.button import MDFlatButton
from kivymd.uix.dialog import MDDialog
KV = '''
<Content>
orientation: "vertical"
spacing: "12dp"
size_hint_y: None
height: "120dp"
MDTextField:
hint_text: "City"
MDTextField:
hint_text: "Street"
FloatLayout:
MDFlatButton:
text: "ALERT DIALOG"
pos_hint: {'center_x': .5, 'center_y': .5}
on_release: app.show_confirmation_dialog()
'''
class Content(BoxLayout):
pass
class Example(MDApp):
dialog = None
def build(self):
return Builder.load_string(KV)
def show_confirmation_dialog(self):
if not self.dialog:
self.dialog = MDDialog(
title="Address:",
type="custom",
content_cls=Content(),
buttons=[
MDFlatButton(
text="CANCEL", text_color=self.theme_cls.primary_color
),
MDFlatButton(
text="OK", text_color=self.theme_cls.primary_color
),
],
)
self.dialog.open()
Example().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dialog-custom.png
:align: center
:attr:`content_cls` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `'None'`.
"""
md_bg_color = ListProperty()
"""
Background color in the format (r, g, b, a).
:attr:`md_bg_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
_scroll_height = NumericProperty("28dp")
_spacer_top = NumericProperty("24dp")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.md_bg_color = (
self.theme_cls.bg_dark if not self.md_bg_color else self.md_bg_color
)
if self.size_hint == [1, 1] and DEVICE_TYPE == "mobile":
self.size_hint = (None, None)
self.width = dp(280)
elif self.size_hint == [1, 1] and DEVICE_TYPE == "desktop":
self.size_hint = (None, None)
self.width = dp(560)
if not self.title:
self._spacer_top = 0
if not self.buttons:
self.ids.root_button_box.height = 0
else:
self.create_buttons()
update_height = False
if self.type in ("simple", "confirmation"):
if self.type == "confirmation":
self.ids.spacer_top_box.add_widget(MDSeparator())
self.ids.spacer_bottom_box.add_widget(MDSeparator())
self.create_items()
if self.type == "custom":
if self.content_cls:
self.ids.container.remove_widget(self.ids.scroll)
self.ids.container.remove_widget(self.ids.text)
self.ids.spacer_top_box.add_widget(self.content_cls)
self.ids.spacer_top_box.padding = (0, "24dp", "16dp", 0)
update_height = True
if self.type == "alert":
self.ids.scroll.bar_width = 0
if update_height:
Clock.schedule_once(self.update_height)
def update_height(self, *_):
self._spacer_top = self.content_cls.height + dp(24)
def on_open(self):
# TODO: Add scrolling text.
self.height = self.ids.container.height
def set_normal_height(self):
self.size_hint_y = 0.8
def get_normal_height(self):
return (
(Window.height * 80 / 100)
- self._spacer_top
- dp(52)
- self.ids.container.padding[1]
- self.ids.container.padding[-1]
- 100
)
def edit_padding_for_item(self, instance_item):
instance_item.ids._left_container.x = 0
instance_item._txt_left_pad = "56dp"
def create_items(self):
self.ids.container.remove_widget(self.ids.text)
height = 0
for item in self.items:
if issubclass(item.__class__, BaseListItem):
height += item.height # calculate height contents
self.edit_padding_for_item(item)
self.ids.box_items.add_widget(item)
if height > Window.height:
self.set_normal_height()
self.ids.scroll.height = self.get_normal_height()
else:
self.ids.scroll.height = height
def create_buttons(self):
for button in self.buttons:
if issubclass(button.__class__, BaseButton):
self.ids.button_box.add_widget(button)

713
kivymd/uix/dialog2.py Normal file
View file

@ -0,0 +1,713 @@
"""
Dialog
======
Copyright (c) 2015 Andrés Rodríguez and KivyMD contributors -
KivyMD library up to version 0.1.2
Copyright (c) 2019 Ivanov Yuri and KivyMD contributors -
KivyMD library version 0.1.3 and higher
For suggestions and questions:
<kivydevelopment@gmail.com>
This file is distributed under the terms of the same license,
as the Kivy framework.
`Material Design spec, Dialogs <https://material.io/design/components/dialogs.html>`_
Example
-------
from kivymd.app import MDApp
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.utils import get_hex_from_color
from kivymd.uix.dialog import MDInputDialog, MDDialog
from kivymd.theming import ThemeManager
Builder.load_string('''
<ExampleDialogs@BoxLayout>
orientation: 'vertical'
spacing: dp(5)
MDToolbar:
id: toolbar
title: app.title
left_action_items: [['menu', lambda x: None]]
elevation: 10
md_bg_color: app.theme_cls.primary_color
FloatLayout:
MDRectangleFlatButton:
text: "Open input dialog"
pos_hint: {'center_x': .5, 'center_y': .7}
opposite_colors: True
on_release: app.show_example_input_dialog()
MDRectangleFlatButton:
text: "Open Ok Cancel dialog"
pos_hint: {'center_x': .5, 'center_y': .5}
opposite_colors: True
on_release: app.show_example_okcancel_dialog()
''')
class Example(MDApp):
title = "Dialogs"
def build(self):
return Factory.ExampleDialogs()
def callback_for_menu_items(self, *args):
from kivymd.toast.kivytoast import toast
toast(args[0])
def show_example_input_dialog(self):
dialog = MDInputDialog(
title='Title', hint_text='Hint text', size_hint=(.8, .4),
text_button_ok='Yes',
events_callback=self.callback_for_menu_items)
dialog.open()
def show_example_okcancel_dialog(self):
dialog = MDDialog(
title='Title', size_hint=(.8, .3), text_button_ok='Yes',
text="Your [color=%s][b]text[/b][/color] dialog" % get_hex_from_color(
self.theme_cls.primary_color),
text_button_cancel='Cancel',
events_callback=self.callback_for_menu_items)
dialog.open()
Example().run()
"""
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import StringProperty, ObjectProperty, BooleanProperty
from kivy.metrics import dp
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.modalview import ModalView
from kivymd.uix.card import MDCard
from kivymd.uix.button import MDFlatButton, MDRaisedButton, MDTextButton
from kivymd.uix.textfield import MDTextField, MDTextFieldRect
from kivymd.theming import ThemableBehavior
from kivymd import images_path
from kivymd.material_resources import DEVICE_IOS
Builder.load_string(
"""
#:import images_path kivymd.images_path
<ContentInputDialog>
orientation: 'vertical'
padding: dp(15)
spacing: dp(10)
MDLabel:
font_style: 'H6'
text: root.title
halign: 'left' if not root.device_ios else 'center'
BoxLayout:
id: box_input
size_hint: 1, None
Widget:
Widget:
MDSeparator:
id: sep
BoxLayout:
id: box_buttons
size_hint_y: None
height: dp(20)
padding: dp(20), 0, dp(20), 0
#:import webbrowser webbrowser
#:import parse urllib.parse
<ThinLabel@MDLabel>:
size_hint: 1, None
valign: 'middle'
height: self.texture_size[1]
<ThinLabelButton@ThinLabel+MDTextButton>:
size_hint_y: None
valign: 'middle'
height: self.texture_size[1]
<ThinBox@BoxLayout>:
size_hint_y: None
height: self.minimum_height
padding: dp(0), dp(0), dp(10), dp(0)
<ListMDDialog>
title: ""
BoxLayout:
orientation: 'vertical'
padding: dp(15)
spacing: dp(10)
MDLabel:
id: title
text: root.title
font_style: 'H6'
halign: 'left' if not root.device_ios else 'center'
valign: 'top'
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
ScrollView:
id: scroll
size_hint_y: None
height:
root.height - (title.height + dp(48)\
+ sep.height)
canvas:
Rectangle:
pos: self.pos
size: self.size
#source: '{}dialog_in_fade.png'.format(images_path)
source: '{}transparent.png'.format(images_path)
MDList:
id: list_layout
size_hint_y: None
height: self.minimum_height
spacing: dp(15)
canvas.before:
Rectangle:
pos: self.pos
size: self.size
Color:
rgba: [1,0,0,.5]
ThinBox:
ThinLabel:
text: "Address: "
ThinLabelButton:
text: root.address
on_release:
webbrowser.open("http://maps.apple.com/?address="+parse.quote(self.text))
ThinBox:
ThinLabel:
text: "Website: "
ThinLabelButton:
text: root.Website
on_release:
webbrowser.open(self.text)
ThinBox:
ThinLabel:
text: "Facebook: "
ThinLabelButton:
text: root.Facebook
on_release:
webbrowser.open(self.text)
ThinBox:
ThinLabel:
text: "Twitter: "
ThinLabelButton:
text: root.Twitter
on_release:
webbrowser.open(self.text)
ThinBox:
ThinLabel:
text: "Season1 Date: "
ThinLabel:
text: root.Season1_date
ThinBox:
ThinLabel:
text: "Season1 Hours: "
ThinLabel:
text: root.Season1_hours
ThinBox:
ThinLabel:
text: "Season2 Date: "
ThinLabel:
text: root.Season2_date
ThinBox:
ThinLabel:
text: "Season2 Hours: "
ThinLabel:
text: root.Season2_hours
ThinBox:
ThinLabel:
text: "Season3 Date: "
ThinLabel:
text: root.Season3_date
ThinBox:
ThinLabel:
text: "Season3 Hours: "
ThinLabel:
text: root.Season3_hours
ThinBox:
ThinLabel:
text: "Season4 Date: "
ThinLabel:
text: root.Season4_date
ThinBox:
ThinLabel:
text: "Season4 Hours: "
ThinLabel:
text: root.Season4_hours
ThinBox:
ThinLabel:
text: "Credit: "
ThinLabel:
text: root.Credit
ThinBox:
ThinLabel:
text: "WIC: "
ThinLabel:
text: root.WIC
ThinBox:
ThinLabel:
text: "WICcash: "
ThinLabel:
text: root.WICcash
ThinBox:
ThinLabel:
text: "SFMNP: "
ThinLabel:
text: root.SFMNP
ThinBox:
ThinLabel:
text: "SNAP: "
ThinLabel:
text: root.SNAP
ThinBox:
ThinLabel:
text: "Organic: "
ThinLabel:
text: root.Organic
ThinBox:
ThinLabel:
text: "Baked Goods: "
ThinLabel:
text: root.Bakedgoods
ThinBox:
ThinLabel:
text: "Cheese: "
ThinLabel:
text: root.Cheese
ThinBox:
ThinLabel:
text: "Crafts: "
ThinLabel:
text: root.Crafts
ThinBox:
ThinLabel:
text: "Flowers: "
ThinLabel:
text: root.Flowers
ThinBox:
ThinLabel:
text: "Eggs: "
ThinLabel:
text: root.Eggs
ThinBox:
ThinLabel:
text: "Seafood: "
ThinLabel:
text: root.Seafood
ThinBox:
ThinLabel:
text: "Herbs: "
ThinLabel:
text: root.Herbs
ThinBox:
ThinLabel:
text: "Vegetables: "
ThinLabel:
text: root.Vegetables
ThinBox:
ThinLabel:
text: "Honey: "
ThinLabel:
text: root.Honey
ThinBox:
ThinLabel:
text: "Jams: "
ThinLabel:
text: root.Jams
ThinBox:
ThinLabel:
text: "Maple: "
ThinLabel:
text: root.Maple
ThinBox:
ThinLabel:
text: "Meat: "
ThinLabel:
text: root.Meat
ThinBox:
ThinLabel:
text: "Nursery: "
ThinLabel:
text: root.Nursery
ThinBox:
ThinLabel:
text: "Nuts: "
ThinLabel:
text: root.Nuts
ThinBox:
ThinLabel:
text: "Plants: "
ThinLabel:
text: root.Plants
ThinBox:
ThinLabel:
text: "Poultry: "
ThinLabel:
text: root.Poultry
ThinBox:
ThinLabel:
text: "Prepared: "
ThinLabel:
text: root.Prepared
ThinBox:
ThinLabel:
text: "Soap: "
ThinLabel:
text: root.Soap
ThinBox:
ThinLabel:
text: "Trees: "
ThinLabel:
text: root.Trees
ThinBox:
ThinLabel:
text: "Wine: "
ThinLabel:
text: root.Wine
ThinBox:
ThinLabel:
text: "Coffee: "
ThinLabel:
text: root.Coffee
ThinBox:
ThinLabel:
text: "Beans: "
ThinLabel:
text: root.Beans
ThinBox:
ThinLabel:
text: "Fruits: "
ThinLabel:
text: root.Fruits
ThinBox:
ThinLabel:
text: "Grains: "
ThinLabel:
text: root.Grains
ThinBox:
spacing: dp(10)
ThinLabel:
id: juices
text: "Juices: "
ThinLabel:
text: root.Juices
ThinBox:
spacing: dp(10)
ThinLabel:
text: "Mushrooms: "
ThinLabel:
text: root.Mushrooms
ThinBox:
ThinLabel:
text: "Pet Food: "
ThinLabel:
text: root.PetFood
ThinBox:
ThinLabel:
text: "Tofu: "
ThinLabel:
text: root.Tofu
ThinBox:
ThinLabel:
text: "Wild Harvested: "
ThinLabel:
text: root.WildHarvested
MDSeparator:
id: sep
<ContentMDDialog>
orientation: 'vertical'
padding: dp(15)
spacing: dp(10)
text_button_ok: ''
text_button_cancel: ''
MDLabel:
id: title
text: root.title
font_style: 'H6'
halign: 'left' if not root.device_ios else 'center'
valign: 'top'
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
ScrollView:
id: scroll
size_hint_y: None
height:
root.height - (box_buttons.height + title.height + dp(48)\
+ sep.height)
canvas:
Rectangle:
pos: self.pos
size: self.size
#source: f'{images_path}dialog_in_fade.png'
source: f'{images_path}transparent.png'
MDLabel:
text: '\\n' + root.text + '\\n'
size_hint_y: None
height: self.texture_size[1]
valign: 'top'
halign: 'left' if not root.device_ios else 'center'
markup: True
MDSeparator:
id: sep
BoxLayout:
id: box_buttons
size_hint_y: None
height: dp(20)
padding: dp(20), 0, dp(20), 0
"""
)
if DEVICE_IOS:
Heir = BoxLayout
else:
Heir = MDCard
# FIXME: Not work themes for iOS.
class BaseDialog(ThemableBehavior, ModalView):
def set_content(self, instance_content_dialog):
def _events_callback(result_press):
self.dismiss()
if result_press and self.events_callback:
self.events_callback(result_press, self)
if self.device_ios: # create buttons for iOS
self.background = self._background
if isinstance(instance_content_dialog, ContentInputDialog):
self.text_field = MDTextFieldRect(
pos_hint={"center_x": 0.5},
size_hint=(1, None),
multiline=False,
height=dp(33),
cursor_color=self.theme_cls.primary_color,
hint_text=instance_content_dialog.hint_text,
)
instance_content_dialog.ids.box_input.height = dp(33)
instance_content_dialog.ids.box_input.add_widget(
self.text_field
)
if self.text_button_cancel != "":
anchor = "left"
else:
anchor = "center"
box_button_ok = AnchorLayout(anchor_x=anchor)
box_button_ok.add_widget(
MDTextButton(
text=self.text_button_ok,
font_size="18sp",
on_release=lambda x: _events_callback(self.text_button_ok),
)
)
instance_content_dialog.ids.box_buttons.add_widget(box_button_ok)
if self.text_button_cancel != "":
box_button_ok.anchor_x = "left"
box_button_cancel = AnchorLayout(anchor_x="right")
box_button_cancel.add_widget(
MDTextButton(
text=self.text_button_cancel,
font_size="18sp",
on_release=lambda x: _events_callback(
self.text_button_cancel
),
)
)
instance_content_dialog.ids.box_buttons.add_widget(
box_button_cancel
)
else: # create buttons for Android
if isinstance(instance_content_dialog, ContentInputDialog):
self.text_field = MDTextField(
size_hint=(1, None),
height=dp(48),
hint_text=instance_content_dialog.hint_text,
)
instance_content_dialog.ids.box_input.height = dp(48)
instance_content_dialog.ids.box_input.add_widget(
self.text_field
)
instance_content_dialog.ids.box_buttons.remove_widget(
instance_content_dialog.ids.sep
)
box_buttons = AnchorLayout(
anchor_x="right", size_hint_y=None, height=dp(30)
)
box = BoxLayout(size_hint_x=None, spacing=dp(5))
box.bind(minimum_width=box.setter("width"))
button_ok = MDRaisedButton(
text=self.text_button_ok,
on_release=lambda x: _events_callback(self.text_button_ok),
)
box.add_widget(button_ok)
if self.text_button_cancel != "":
button_cancel = MDFlatButton(
text=self.text_button_cancel,
theme_text_color="Custom",
text_color=self.theme_cls.primary_color,
on_release=lambda x: _events_callback(
self.text_button_cancel
),
)
box.add_widget(button_cancel)
box_buttons.add_widget(box)
instance_content_dialog.ids.box_buttons.add_widget(box_buttons)
instance_content_dialog.ids.box_buttons.height = button_ok.height
instance_content_dialog.remove_widget(
instance_content_dialog.ids.sep
)
class ListMDDialog(BaseDialog):
name = StringProperty("Missing data")
address = StringProperty("Missing data")
Website = StringProperty("Missing data")
Facebook = StringProperty("Missing data")
Twitter = StringProperty("Missing data")
Season1_date = StringProperty("Missing data")
Season1_hours = StringProperty("Missing data")
Season2_date = StringProperty("Missing data")
Season2_hours = StringProperty("Missing data")
Season3_date = StringProperty("Missing data")
Season3_hours = StringProperty("Missing data")
Season4_date = StringProperty("Missing data")
Season4_hours = StringProperty("Missing data")
Credit = StringProperty("Missing data")
WIC = StringProperty("Missing data")
WICcash = StringProperty("Missing data")
SFMNP = StringProperty("Missing data")
SNAP = StringProperty("Missing data")
Organic = StringProperty("Missing data")
Bakedgoods = StringProperty("Missing data")
Cheese = StringProperty("Missing data")
Crafts = StringProperty("Missing data")
Flowers = StringProperty("Missing data")
Eggs = StringProperty("Missing data")
Seafood = StringProperty("Missing data")
Herbs = StringProperty("Missing data")
Vegetables = StringProperty("Missing data")
Honey = StringProperty("Missing data")
Jams = StringProperty("Missing data")
Maple = StringProperty("Missing data")
Meat = StringProperty("Missing data")
Nursery = StringProperty("Missing data")
Nuts = StringProperty("Missing data")
Plants = StringProperty("Missing data")
Poultry = StringProperty("Missing data")
Prepared = StringProperty("Missing data")
Soap = StringProperty("Missing data")
Trees = StringProperty("Missing data")
Wine = StringProperty("Missing data")
Coffee = StringProperty("Missing data")
Beans = StringProperty("Missing data")
Fruits = StringProperty("Missing data")
Grains = StringProperty("Missing data")
Juices = StringProperty("Missing data")
Mushrooms = StringProperty("Missing data")
PetFood = StringProperty("Missing data")
Tofu = StringProperty("Missing data")
WildHarvested = StringProperty("Missing data")
background = StringProperty('{}ios_bg_mod.png'.format(images_path))
class MDInputDialog(BaseDialog):
title = StringProperty("Title")
hint_text = StringProperty()
text_button_ok = StringProperty("Ok")
text_button_cancel = StringProperty()
events_callback = ObjectProperty()
_background = StringProperty(f"{images_path}ios_bg_mod.png")
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.content_dialog = ContentInputDialog(
title=self.title,
hint_text=self.hint_text,
text_button_ok=self.text_button_ok,
text_button_cancel=self.text_button_cancel,
device_ios=self.device_ios,
)
self.add_widget(self.content_dialog)
self.set_content(self.content_dialog)
Clock.schedule_once(self.set_field_focus, 0.5)
def set_field_focus(self, interval):
self.text_field.focus = True
class MDDialog(BaseDialog):
title = StringProperty("Title")
text = StringProperty("Text dialog")
text_button_cancel = StringProperty()
text_button_ok = StringProperty("Ok")
events_callback = ObjectProperty()
_background = StringProperty(f"{images_path}ios_bg_mod.png")
def __init__(self, **kwargs):
super().__init__(**kwargs)
content_dialog = ContentMDDialog(
title=self.title,
text=self.text,
text_button_ok=self.text_button_ok,
text_button_cancel=self.text_button_cancel,
device_ios=self.device_ios,
)
self.add_widget(content_dialog)
self.set_content(content_dialog)
class ContentInputDialog(Heir):
title = StringProperty()
hint_text = StringProperty()
text_button_ok = StringProperty()
text_button_cancel = StringProperty()
device_ios = BooleanProperty()
class ContentMDDialog(Heir):
title = StringProperty()
text = StringProperty()
text_button_cancel = StringProperty()
text_button_ok = StringProperty()
device_ios = BooleanProperty()

135
kivymd/uix/dropdownitem.py Normal file
View file

@ -0,0 +1,135 @@
"""
Components/Dropdown Item
========================
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/dropdown-item.png
:align: center
Usage
-----
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
KV = '''
Screen
MDDropDownItem:
id: drop_item
pos_hint: {'center_x': .5, 'center_y': .5}
text: 'Item'
on_release: self.set_item("New Item")
'''
class Test(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.screen = Builder.load_string(KV)
def build(self):
return self.screen
Test().run()
.. seealso::
`Work with the class MDDropdownMenu see here <https://kivymd.readthedocs.io/en/latest/components/menu/index.html#center-position>`_
"""
__all__ = ("MDDropDownItem",)
from kivy.lang import Builder
from kivy.properties import NumericProperty, StringProperty
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.widget import Widget
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import RectangularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
Builder.load_string(
"""
<_Triangle>:
canvas:
Color:
rgba: root.theme_cls.text_color
Triangle:
points:
[ \
self.right-14, self.y+7, \
self.right-7, self.y+7, \
self.right-7, self.y+14 \
]
<MDDropDownItem>
orientation: "vertical"
adaptive_size: True
spacing: "5dp"
padding: "5dp", "5dp", "5dp", 0
MDBoxLayout:
adaptive_size: True
spacing: "10dp"
Label:
id: label_item
size_hint: None, None
size: self.texture_size
color: root.theme_cls.text_color
font_size: root.font_size
_Triangle:
size_hint: None, None
size: "20dp", "20dp"
MDSeparator:
"""
)
class _Triangle(ThemableBehavior, Widget):
pass
class MDDropDownItem(
ThemableBehavior, RectangularRippleBehavior, ButtonBehavior, MDBoxLayout
):
text = StringProperty()
"""
Text item.
:attr:`text` is a :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
current_item = StringProperty()
"""
Current name item.
:attr:`current_item` is a :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
font_size = NumericProperty("16sp")
"""
Item font size.
:attr:`font_size` is a :class:`~kivy.properties.NumericProperty`
and defaults to `'16sp'`.
"""
def on_text(self, instance, value):
self.ids.label_item.text = value
def set_item(self, name_item):
"""Sets new text for an item."""
self.ids.label_item.text = name_item
self.current_item = name_item

395
kivymd/uix/expansionpanel.py Executable file
View file

@ -0,0 +1,395 @@
"""
Components/Expansion Panel
==========================
.. seealso::
`Material Design spec, Expansion panel <https://material.io/archive/guidelines/components/expansion-panels.html#>`_
.. rubric:: Expansion panels contain creation flows and allow lightweight editing of an element.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/expansion-panel.png
:align: center
Usage
-----
.. code-block:: python
self.add_widget(
MDExpansionPanel(
icon="logo.png", # panel icon
content=Content(), # panel content
panel_cls=MDExpansionPanelOneLine(text="Secondary text"), # panel class
)
)
To use :class:`~MDExpansionPanel` you must pass one of the following classes
to the :attr:`~MDExpansionPanel.panel_cls` parameter:
- :class:`~MDExpansionPanelOneLine`
- :class:`~MDExpansionPanelTwoLine`
- :class:`~MDExpansionPanelThreeLine`
These classes are inherited from the following classes:
- :class:`~kivymd.uix.list.OneLineAvatarIconListItem`
- :class:`~kivymd.uix.list.TwoLineAvatarIconListItem`
- :class:`~kivymd.uix.list.ThreeLineAvatarIconListItem`
.. code-block:: python
self.root.ids.box.add_widget(
MDExpansionPanel(
icon="logo.png",
content=Content(),
panel_cls=MDExpansionPanelThreeLine(
text="Text",
secondary_text="Secondary text",
tertiary_text="Tertiary text",
)
)
)
Example
-------
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.expansionpanel import MDExpansionPanel, MDExpansionPanelThreeLine
from kivymd import images_path
KV = '''
<Content>
adaptive_height: True
TwoLineIconListItem:
text: "(050)-123-45-67"
secondary_text: "Mobile"
IconLeftWidget:
icon: 'phone'
ScrollView:
MDGridLayout:
id: box
cols: 1
adaptive_height: True
'''
class Content(MDBoxLayout):
'''Custom content.'''
class Test(MDApp):
def build(self):
return Builder.load_string(KV)
def on_start(self):
for i in range(10):
self.root.ids.box.add_widget(
MDExpansionPanel(
icon=f"{images_path}kivymd.png",
content=Content(),
panel_cls=MDExpansionPanelThreeLine(
text="Text",
secondary_text="Secondary text",
tertiary_text="Tertiary text",
)
)
)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/expansion-panel.gif
:align: center
Two events are available for :class:`~MDExpansionPanel`
------------------------------------------------------
- :attr:`~MDExpansionPanel.on_open`
- :attr:`~MDExpansionPanel.on_close`
.. code-block:: kv
MDExpansionPanel:
on_open: app.on_panel_open(args)
on_close: app.on_panel_close(args)
The user function takes one argument - the object of the panel:
.. code-block:: python
def on_panel_open(self, instance_panel):
print(instance_panel)
.. seealso:: `See Expansion panel example <https://github.com/kivymd/KivyMD/wiki/Components-Expansion-Panel>`_
`Expansion panel and MDCard <https://github.com/kivymd/KivyMD/wiki/Components-Expansion-Panel-and-MDCard>`_
"""
__all__ = (
"MDExpansionPanel",
"MDExpansionPanelOneLine",
"MDExpansionPanelTwoLine",
"MDExpansionPanelThreeLine",
)
from kivy.animation import Animation
from kivy.lang import Builder
from kivy.properties import NumericProperty, ObjectProperty, StringProperty
from kivy.uix.relativelayout import RelativeLayout
from kivy.uix.widget import WidgetException
import kivymd.material_resources as m_res
from kivymd.icon_definitions import md_icons
from kivymd.uix.button import MDIconButton
from kivymd.uix.list import (
IconLeftWidget,
ImageLeftWidget,
IRightBodyTouch,
OneLineAvatarIconListItem,
ThreeLineAvatarIconListItem,
TwoLineAvatarIconListItem,
)
Builder.load_string(
"""
<MDExpansionChevronRight>:
icon: 'chevron-right'
disabled: True
canvas.before:
PushMatrix
Rotate:
angle: self._angle
axis: (0, 0, 1)
origin: self.center
canvas.after:
PopMatrix
<MDExpansionPanel>
size_hint_y: None
#height: dp(68)
"""
)
class MDExpansionChevronRight(IRightBodyTouch, MDIconButton):
"""Chevron icon on the right panel."""
_angle = NumericProperty(0)
class MDExpansionPanelOneLine(OneLineAvatarIconListItem):
"""Single line panel."""
class MDExpansionPanelTwoLine(TwoLineAvatarIconListItem):
"""Two-line panel."""
class MDExpansionPanelThreeLine(ThreeLineAvatarIconListItem):
"""Three-line panel."""
class MDExpansionPanel(RelativeLayout):
"""
:Events:
:attr:`on_open`
Called when a panel is opened.
:attr:`on_close`
Called when a panel is closed.
"""
content = ObjectProperty()
"""Content of panel. Must be `Kivy` widget.
:attr:`content` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
icon = StringProperty()
"""Icon of panel.
Icon Should be either be a path to an image or
a logo name in :class:`~kivymd.icon_definitions.md_icons`
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
opening_transition = StringProperty("out_cubic")
"""
The name of the animation transition type to use when animating to
the :attr:`state` `'open'`.
:attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_cubic'`.
"""
opening_time = NumericProperty(0.2)
"""
The time taken for the panel to slide to the :attr:`state` `'open'`.
:attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
closing_transition = StringProperty("out_sine")
"""The name of the animation transition type to use when animating to
the :attr:`state` 'close'.
:attr:`closing_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_sine'`.
"""
closing_time = NumericProperty(0.2)
"""
The time taken for the panel to slide to the :attr:`state` `'close'`.
:attr:`closing_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
panel_cls = ObjectProperty()
"""
Panel object. The object must be one of the classes
:class:`~MDExpansionPanelOneLine`, :class:`~MDExpansionPanelTwoLine` or
:class:`~MDExpansionPanelThreeLine`.
:attr:`panel_cls` is a :class:`~kivy.properties.ObjectProperty`
and defaults to `None`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.register_event_type("on_open")
self.register_event_type("on_close")
if self.panel_cls and isinstance(
self.panel_cls,
(
MDExpansionPanelOneLine,
MDExpansionPanelTwoLine,
MDExpansionPanelThreeLine,
),
):
self.panel_cls.pos_hint = {"top": 1}
self.panel_cls._no_ripple_effect = True
self.panel_cls.bind(
on_release=lambda x: self.check_open_panel(self.panel_cls)
)
self.chevron = MDExpansionChevronRight()
self.panel_cls.add_widget(self.chevron)
if self.icon:
if self.icon in md_icons.keys():
self.panel_cls.add_widget(
IconLeftWidget(
icon=self.icon, pos_hint={"center_y": 0.5}
)
)
else:
self.panel_cls.add_widget(
ImageLeftWidget(
source=self.icon, pos_hint={"center_y": 0.5}
)
)
else:
# if no icon
self.panel_cls._txt_left_pad = m_res.HORIZ_MARGINS
self.add_widget(self.panel_cls)
else:
raise ValueError(
"KivyMD: `panel_cls` object must be must be one of the "
"objects from the list\n"
"[MDExpansionPanelOneLine, MDExpansionPanelTwoLine, "
"MDExpansionPanelThreeLine]"
)
def on_open(self, *args):
"""Called when a panel is opened."""
def on_close(self, *args):
"""Called when a panel is closed."""
def check_open_panel(self, instance):
"""
Called when you click on the panel. Called methods to open or close
a panel.
"""
press_current_panel = False
for panel in self.parent.children:
if isinstance(panel, MDExpansionPanel):
if len(panel.children) == 2:
if instance is panel.children[1]:
press_current_panel = True
panel.remove_widget(panel.children[0])
chevron = panel.children[0].children[0].children[0]
self.set_chevron_up(chevron)
self.close_panel(panel)
self.dispatch("on_close")
break
if not press_current_panel:
self.set_chevron_down()
def set_chevron_down(self):
"""Sets the chevron down."""
Animation(_angle=-90, d=self.opening_time).start(self.chevron)
self.open_panel()
self.dispatch("on_open")
def set_chevron_up(self, instance_chevron):
"""Sets the chevron up."""
Animation(_angle=0, d=self.closing_time).start(instance_chevron)
def close_panel(self, instance_panel):
"""Method closes the panel."""
Animation(
height=self.panel_cls.height,
d=self.closing_time,
t=self.closing_transition,
).start(instance_panel)
def open_panel(self, *args):
"""Method opens a panel."""
anim = Animation(
height=self.content.height + self.height,
d=self.opening_time,
t=self.opening_transition,
)
anim.bind(on_complete=self._add_content)
anim.start(self)
def add_widget(self, widget, index=0, canvas=None):
if isinstance(
widget,
(
MDExpansionPanelOneLine,
MDExpansionPanelTwoLine,
MDExpansionPanelThreeLine,
),
):
self.height = widget.height
return super().add_widget(widget)
def _add_content(self, *args):
if self.content:
try:
self.add_widget(self.content)
except WidgetException:
pass

658
kivymd/uix/filemanager.py Executable file
View file

@ -0,0 +1,658 @@
"""
Components/File Manager
=======================
A simple manager for selecting directories and files.
Usage
-----
.. code-block:: python
path = '/' # path to the directory that will be opened in the file manager
file_manager = MDFileManager(
exit_manager=self.exit_manager, # function called when the user reaches directory tree root
select_path=self.select_path, # function called when selecting a file/directory
)
file_manager.show(path)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager.png
:align: center
Or with ``preview`` mode:
.. code-block:: python
file_manager = MDFileManager(
exit_manager=self.exit_manager,
select_path=self.select_path,
preview=True,
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/file-manager-previous.png
:align: center
.. warning:: The `preview` mode is intended only for viewing images and will
not display other types of files.
Example
-------
.. code-block:: python
from kivy.core.window import Window
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.filemanager import MDFileManager
from kivymd.toast import toast
KV = '''
BoxLayout:
orientation: 'vertical'
MDToolbar:
title: "MDFileManager"
left_action_items: [['menu', lambda x: None]]
elevation: 10
FloatLayout:
MDRoundFlatIconButton:
text: "Open manager"
icon: "folder"
pos_hint: {'center_x': .5, 'center_y': .6}
on_release: app.file_manager_open()
'''
class Example(MDApp):
def __init__(self, **kwargs):
super().__init__(**kwargs)
Window.bind(on_keyboard=self.events)
self.manager_open = False
self.file_manager = MDFileManager(
exit_manager=self.exit_manager,
select_path=self.select_path,
preview=True,
)
def build(self):
return Builder.load_string(KV)
def file_manager_open(self):
self.file_manager.show('/') # output manager to the screen
self.manager_open = True
def select_path(self, path):
'''It will be called when you click on the file name
or the catalog selection button.
:type path: str;
:param path: path to the selected directory or file;
'''
self.exit_manager()
toast(path)
def exit_manager(self, *args):
'''Called when the user reaches the root of the directory tree.'''
self.manager_open = False
self.file_manager.close()
def events(self, instance, keyboard, keycode, text, modifiers):
'''Called when buttons are pressed on the mobile device.'''
if keyboard in (1001, 27):
if self.manager_open:
self.file_manager.back()
return True
Example().run()
"""
__all__ = ("MDFileManager",)
import locale
import os
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.modalview import ModalView
from kivymd import images_path
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import CircularRippleBehavior
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.floatlayout import MDFloatLayout
from kivymd.uix.list import BaseListItem, ContainerSupport
from kivymd.utils.fitimage import FitImage
ACTIVITY_MANAGER = """
#:import os os
<BodyManager@BoxLayout>
icon: "folder"
path: ""
background_normal: ""
background_down: ""
dir_or_file_name: ""
_selected: False
events_callback: lambda x: None
orientation: "vertical"
ModifiedOneLineIconListItem:
text: root.dir_or_file_name
bg_color: self.theme_cls.bg_darkest if root._selected else self.theme_cls.bg_normal
on_release: root.events_callback(root.path, root)
IconLeftWidget:
icon: root.icon
theme_text_color: "Custom"
text_color: self.theme_cls.primary_color
MDSeparator:
<LabelContent@MDLabel>
size_hint_y: None
height: self.texture_size[1]
shorten: True
shorten_from: "center"
halign: "center"
text_size: self.width, None
<BodyManagerWithPreview>
name: ""
path: ""
realpath: ""
type: "folder"
events_callback: lambda x: None
_selected: False
orientation: "vertical"
size_hint_y: None
hright: root.height
padding: dp(20)
IconButton:
mipmap: True
source: root.path
bg_color: app.theme_cls.bg_darkest if root._selected else app.theme_cls.bg_normal
on_release:
root.events_callback(\
os.path.join(root.path if root.type != "folder" else root.realpath, \
root.name), root)
LabelContent:
text: root.name
<FloatButton>
anchor_x: "right"
anchor_y: "bottom"
size_hint_y: None
height: dp(56)
padding: dp(10)
MDFloatingActionButton:
size_hint: None, None
size:dp(56), dp(56)
icon: root.icon
opposite_colors: True
elevation: 8
on_release: root.callback()
md_bg_color: root.md_bg_color
<MDFileManager>
md_bg_color: root.theme_cls.bg_normal
BoxLayout:
orientation: "vertical"
spacing: dp(5)
MDToolbar:
id: toolbar
title: root.current_path
right_action_items: [["close-box", lambda x: root.exit_manager(1)]]
left_action_items: [["chevron-left", lambda x: root.back()]]
elevation: 10
RecycleView:
id: rv
key_viewclass: "viewclass"
key_size: "height"
bar_width: dp(4)
bar_color: root.theme_cls.primary_color
#on_scroll_stop: root._update_list_images()
RecycleGridLayout:
padding: dp(10)
cols: 3 if root.preview else 1
default_size: None, dp(48)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: "vertical"
<ModifiedOneLineIconListItem>
BoxLayout:
id: _left_container
size_hint: None, None
x: root.x + dp(16)
y: root.y + root.height / 2 - self.height / 2
size: dp(48), dp(48)
"""
class BodyManagerWithPreview(MDBoxLayout):
"""Base class for folder icons and thumbnails images in ``preview`` mode."""
class IconButton(CircularRippleBehavior, ButtonBehavior, FitImage):
"""Folder icons/thumbnails images in ``preview`` mode."""
class FloatButton(AnchorLayout):
callback = ObjectProperty()
md_bg_color = ListProperty([1, 1, 1, 1])
icon = StringProperty()
class ModifiedOneLineIconListItem(ContainerSupport, BaseListItem):
_txt_left_pad = NumericProperty("72dp")
_txt_top_pad = NumericProperty("16dp")
_txt_bot_pad = NumericProperty("15dp")
_num_lines = 1
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.height = dp(48)
class MDFileManager(ThemableBehavior, MDFloatLayout):
icon = StringProperty("check")
"""
The icon that will be used on the directory selection button.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `check`.
"""
icon_folder = StringProperty(f"{images_path}folder.png")
"""
The icon that will be used for folder icons when using ``preview = True``.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `check`.
"""
exit_manager = ObjectProperty(lambda x: None)
"""
Function called when the user reaches directory tree root.
:attr:`exit_manager` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `lambda x: None`.
"""
select_path = ObjectProperty(lambda x: None)
"""
Function, called when selecting a file/directory.
:attr:`select_path` is an :class:`~kivy.properties.ObjectProperty`
and defaults to `lambda x: None`.
"""
ext = ListProperty()
"""
List of file extensions to be displayed in the manager.
For example, `['.py', '.kv']` - will filter out all files,
except python scripts and Kv Language.
:attr:`ext` is an :class:`~kivy.properties.ListProperty`
and defaults to `[]`.
"""
search = OptionProperty("all", options=["all", "dirs", "files"])
"""
It can take the values 'all' 'dirs' 'files' - display only directories
or only files or both them. By default, it displays folders, and files.
Available options are: `'all'`, `'dirs'`, `'files'`.
:attr:`search` is an :class:`~kivy.properties.OptionProperty`
and defaults to `all`.
"""
current_path = StringProperty(os.getcwd())
"""
Current directory.
:attr:`current_path` is an :class:`~kivy.properties.StringProperty`
and defaults to `/`.
"""
use_access = BooleanProperty(True)
"""
Show access to files and directories.
:attr:`use_access` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
preview = BooleanProperty(False)
"""
Shows only image previews.
:attr:`preview` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
show_hidden_files = BooleanProperty(False)
"""
Shows hidden files.
:attr:`show_hidden_files` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
sort_by = OptionProperty(
"name", options=["nothing", "name", "date", "size", "type"]
)
"""
It can take the values 'nothing' 'name' 'date' 'size' 'type' - sorts files by option
By default, sort by name.
Available options are: `'nothing'`, `'name'`, `'date'`, `'size'`, `'type'`.
:attr:`sort_by` is an :class:`~kivy.properties.OptionProperty`
and defaults to `name`.
"""
sort_by_desc = BooleanProperty(False)
"""
Sort by descending.
:attr:`sort_by_desc` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
multiselect = BooleanProperty(False)
"""
Determines whether the user is able to select multiple files or not.
:attr:`multiselect` is a :class:`~kivy.properties.BooleanProperty` and defaults to
False.
"""
selection = ListProperty([])
"""
Contains the list of files that are currently selected.
:attr:`selection` is a read-only :class:`~kivy.properties.ListProperty` and
defaults to [].
"""
_window_manager = None
_window_manager_open = False
def __init__(self, **kwargs):
super().__init__(**kwargs)
toolbar_label = self.ids.toolbar.children[1].children[0]
toolbar_label.font_style = "Subtitle1"
self.add_widget(
FloatButton(
callback=self.select_directory_on_press_button,
md_bg_color=self.theme_cls.primary_color,
icon=self.icon,
)
)
if self.preview:
self.ext = [".png", ".jpg", ".jpeg"]
def __sort_files(self, files):
def sort_by_name(files):
files.sort(key=locale.strxfrm)
files.sort(key=str.casefold)
return files
if self.sort_by == "name":
sorted_files = sort_by_name(files)
elif self.sort_by == "date":
_files = sort_by_name(files)
_sorted_files = [os.path.join(self.current_path, f) for f in _files]
_sorted_files.sort(key=os.path.getmtime, reverse=True)
sorted_files = [os.path.basename(f) for f in _sorted_files]
elif self.sort_by == "size":
_files = sort_by_name(files)
_sorted_files = [os.path.join(self.current_path, f) for f in _files]
_sorted_files.sort(key=os.path.getsize, reverse=True)
sorted_files = [os.path.basename(f) for f in _sorted_files]
elif self.sort_by == "type":
_files = sort_by_name(files)
sorted_files = sorted(
_files,
key=lambda f: (os.path.splitext(f)[1], os.path.splitext(f)[0]),
)
else:
sorted_files = files
if self.sort_by_desc:
sorted_files.reverse()
return sorted_files
def show(self, path):
"""Forms the body of a directory tree.
:param path:
The path to the directory that will be opened in the file manager.
"""
self.current_path = path
self.selection = []
dirs, files = self.get_content()
manager_list = []
if dirs == [] and files == []: # selected directory
pass
elif not dirs and not files: # directory is unavailable
return
if self.preview:
for name_dir in self.__sort_files(dirs):
manager_list.append(
{
"viewclass": "BodyManagerWithPreview",
"path": self.icon_folder,
"realpath": os.path.join(path),
"type": "folder",
"name": name_dir,
"events_callback": self.select_dir_or_file,
"height": dp(150),
"_selected": False,
}
)
for name_file in self.__sort_files(files):
if (
os.path.splitext(os.path.join(path, name_file))[1]
in self.ext
):
manager_list.append(
{
"viewclass": "BodyManagerWithPreview",
"path": os.path.join(path, name_file),
"name": name_file,
"type": "files",
"events_callback": self.select_dir_or_file,
"height": dp(150),
"_selected": False,
}
)
else:
for name in self.__sort_files(dirs):
_path = os.path.join(path, name)
access_string = self.get_access_string(_path)
if "r" not in access_string:
icon = "folder-lock"
else:
icon = "folder"
manager_list.append(
{
"viewclass": "BodyManager",
"path": _path,
"icon": icon,
"dir_or_file_name": name,
"events_callback": self.select_dir_or_file,
"_selected": False,
}
)
for name in self.__sort_files(files):
if self.ext and os.path.splitext(name)[1] not in self.ext:
continue
manager_list.append(
{
"viewclass": "BodyManager",
"path": name,
"icon": "file-outline",
"dir_or_file_name": os.path.split(name)[1],
"events_callback": self.select_dir_or_file,
"_selected": False,
}
)
self.ids.rv.data = manager_list
if not self._window_manager:
self._window_manager = ModalView(
size_hint=(1, 1), auto_dismiss=False
)
self._window_manager.add_widget(self)
if not self._window_manager_open:
self._window_manager.open()
self._window_manager_open = True
def get_access_string(self, path):
access_string = ""
if self.use_access:
access_data = {"r": os.R_OK, "w": os.W_OK, "x": os.X_OK}
for access in access_data.keys():
access_string += (
access if os.access(path, access_data[access]) else "-"
)
return access_string
def get_content(self):
"""Returns a list of the type [[Folder List], [file list]]."""
try:
files = []
dirs = []
for content in os.listdir(self.current_path):
if os.path.isdir(os.path.join(self.current_path, content)):
if self.search == "all" or self.search == "dirs":
if (not self.show_hidden_files) and (
content.startswith(".")
):
continue
else:
dirs.append(content)
else:
if self.search == "all" or self.search == "files":
if len(self.ext) != 0:
try:
files.append(
os.path.join(self.current_path, content)
)
except IndexError:
pass
else:
if (
not self.show_hidden_files
and content.startswith(".")
):
continue
else:
files.append(content)
return dirs, files
except OSError:
return None, None
def close(self):
"""Closes the file manager window."""
self._window_manager.dismiss()
self._window_manager_open = False
def select_dir_or_file(self, path, widget):
"""Called by tap on the name of the directory or file."""
if os.path.isfile(os.path.join(self.current_path, path)):
if self.multiselect:
file_path = os.path.join(self.current_path, path)
if file_path in self.selection:
widget._selected = False
self.selection.remove(file_path)
else:
widget._selected = True
self.selection.append(file_path)
else:
self.select_path(os.path.join(self.current_path, path))
else:
self.current_path = path
self.show(path)
def back(self):
"""Returning to the branch down in the directory tree."""
path, end = os.path.split(self.current_path)
if not end:
self.close()
self.exit_manager(1)
else:
self.show(path)
def select_directory_on_press_button(self, *args):
"""Called when a click on a floating button."""
if len(self.selection) > 0:
self.select_path(self.selection)
else:
self.select_path(self.current_path)
Builder.load_string(ACTIVITY_MANAGER)

42
kivymd/uix/floatlayout.py Normal file
View file

@ -0,0 +1,42 @@
"""
Components/FloatLayout
======================
:class:`~kivy.uix.floatlayout.FloatLayout` class equivalent. Simplifies working
with some widget properties. For example:
FloatLayout
-----------
.. code-block::
FloatLayout:
canvas:
Color:
rgba: app.theme_cls.primary_color
RoundedRectangle:
pos: self.pos
size: self.size
radius: [25, 0, 0, 0]
MDFloatLayout
-------------
.. code-block::
MDFloatLayout:
radius: [25, 0, 0, 0]
md_bg_color: app.theme_cls.primary_color
.. Warning:: For a :class:`~kivy.uix.floatlayout.FloatLayout`, the
``minimum_size`` attributes are always 0, so you cannot use
``adaptive_size`` and related options.
"""
from kivy.uix.floatlayout import FloatLayout
from kivymd.uix import MDAdaptiveWidget
class MDFloatLayout(FloatLayout, MDAdaptiveWidget):
pass

92
kivymd/uix/gridlayout.py Normal file
View file

@ -0,0 +1,92 @@
"""
Components/GridLayout
====================
:class:`~kivy.uix.gridlayout.GridLayout` class equivalent. Simplifies working
with some widget properties. For example:
GridLayout
---------
.. code-block::
GridLayout:
size_hint_y: None
height: self.minimum_height
canvas:
Color:
rgba: app.theme_cls.primary_color
Rectangle:
pos: self.pos
size: self.size
MDGridLayout
-----------
.. code-block::
MDGridLayout:
adaptive_height: True
md_bg_color: app.theme_cls.primary_color
Available options are:
---------------------
- adaptive_height_
- adaptive_width_
- adaptive_size_
.. adaptive_height:
adaptive_height
---------------
.. code-block:: kv
adaptive_height: True
Equivalent
.. code-block:: kv
size_hint_y: None
height: self.minimum_height
.. adaptive_width:
adaptive_width
--------------
.. code-block:: kv
adaptive_width: True
Equivalent
.. code-block:: kv
size_hint_x: None
height: self.minimum_width
.. adaptive_size:
adaptive_size
-------------
.. code-block:: kv
adaptive_size: True
Equivalent
.. code-block:: kv
size_hint: None, None
size: self.minimum_size
"""
from kivy.uix.gridlayout import GridLayout
from kivymd.uix import MDAdaptiveWidget
class MDGridLayout(GridLayout, MDAdaptiveWidget):
pass

311
kivymd/uix/imagelist.py Executable file
View file

@ -0,0 +1,311 @@
"""
Components/Image List
=====================
.. seealso::
`Material Design spec, Image lists <https://material.io/components/image-lists>`_
.. rubric:: Image lists display a collection of images in an organized grid.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/image-list.png
:align: center
`KivyMD` provides the following tile classes for use:
- SmartTileWithStar_
- SmartTileWithLabel_
.. SmartTileWithStar:
SmartTileWithStar
-----------------
.. code-block::
from kivymd.app import MDApp
from kivy.lang import Builder
KV = '''
<MyTile@SmartTileWithStar>
size_hint_y: None
height: "240dp"
ScrollView:
MDGridLayout:
cols: 3
adaptive_height: True
padding: dp(4), dp(4)
spacing: dp(4)
MyTile:
stars: 5
source: "cat-1.jpg"
MyTile:
stars: 5
source: "cat-2.jpg"
MyTile:
stars: 5
source: "cat-3.jpg"
'''
class MyApp(MDApp):
def build(self):
return Builder.load_string(KV)
MyApp().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/SmartTileWithStar.gif
:align: center
.. SmartTileWithLabel:
SmartTileWithLabel
------------------
.. code-block:: python
from kivymd.app import MDApp
from kivy.lang import Builder
KV = '''
<MyTile@SmartTileWithLabel>
size_hint_y: None
height: "240dp"
ScrollView:
MDGridLayout:
cols: 3
adaptive_height: True
padding: dp(4), dp(4)
spacing: dp(4)
MyTile:
source: "cat-1.jpg"
text: "[size=26]Cat 1[/size]\\n[size=14]cat-1.jpg[/size]"
MyTile:
source: "cat-2.jpg"
text: "[size=26]Cat 2[/size]\\n[size=14]cat-2.jpg[/size]"
tile_text_color: app.theme_cls.accent_color
MyTile:
source: "cat-3.jpg"
text: "[size=26][color=#ffffff]Cat 3[/color][/size]\\n[size=14]cat-3.jpg[/size]"
tile_text_color: app.theme_cls.accent_color
'''
class MyApp(MDApp):
def build(self):
return Builder.load_string(KV)
MyApp().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/SmartTileWithLabel.png
:align: center
"""
__all__ = ("SmartTile", "SmartTileWithLabel", "SmartTileWithStar")
from kivy.lang import Builder
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.behaviors import ButtonBehavior
from kivymd.theming import ThemableBehavior
from kivymd.uix.behaviors import RectangularRippleBehavior
from kivymd.uix.button import MDIconButton
from kivymd.uix.floatlayout import MDFloatLayout
Builder.load_string(
"""
<SmartTile>
_img_widget: img
_img_overlay: img_overlay
_box_overlay: box
FitImage:
id: img
source: root.source
x: root.x
y: root.y if root.overlap or root.box_position == 'header' else box.top
BoxLayout:
id: img_overlay
size_hint: img.size_hint
size: img.size
pos: img.pos
MDBoxLayout:
id: box
md_bg_color: root.box_color
size_hint_y: None
height: "68dp" if root.lines == 2 else "48dp"
x: root.x
y: root.y if root.box_position == 'footer' else root.y + root.height - self.height
<SmartTileWithLabel>
_img_widget: img
_img_overlay: img_overlay
_box_overlay: box
_box_label: boxlabel
FitImage:
id: img
source: root.source
x: root.x
y: root.y if root.overlap or root.box_position == 'header' else box.top
BoxLayout:
id: img_overlay
size_hint: img.size_hint
size: img.size
pos: img.pos
MDBoxLayout:
id: box
padding: "5dp", 0, 0, 0
md_bg_color: root.box_color
adaptive_height: True
x: root.x
y: root.y if root.box_position == 'footer' else root.y + root.height - self.height
MDLabel:
id: boxlabel
font_style: root.font_style
size_hint_y: None
height: self.texture_size[1]
text: root.text
color: root.tile_text_color
markup: True
"""
)
class SmartTile(
ThemableBehavior, RectangularRippleBehavior, ButtonBehavior, MDFloatLayout
):
"""
A tile for more complex needs.
Includes an image, a container to place overlays and a box that can act
as a header or a footer, as described in the Material Design specs.
"""
box_color = ListProperty((0, 0, 0, 0.5))
"""
Sets the color and opacity for the information box.
:attr:`box_color` is a :class:`~kivy.properties.ListProperty`
and defaults to `(0, 0, 0, 0.5)`.
"""
box_position = OptionProperty("footer", options=["footer", "header"])
"""
Determines wether the information box acts as a header or footer to the
image. Available are options: `'footer'`, `'header'`.
:attr:`box_position` is a :class:`~kivy.properties.OptionProperty`
and defaults to `'footer'`.
"""
lines = OptionProperty(1, options=[1, 2])
"""
Number of lines in the `header/footer`. As per `Material Design specs`,
only 1 and 2 are valid values. Available are options: ``1``, ``2``.
:attr:`lines` is a :class:`~kivy.properties.OptionProperty`
and defaults to `1`.
"""
overlap = BooleanProperty(True)
"""
Determines if the `header/footer` overlaps on top of the image or not.
:attr:`overlap` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
source = StringProperty()
"""
Path to tile image. See :attr:`~kivy.uix.image.Image.source`.
:attr:`source` is a :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
_img_widget = ObjectProperty()
_img_overlay = ObjectProperty()
_box_overlay = ObjectProperty()
_box_label = ObjectProperty()
def reload(self):
self._img_widget.reload()
class SmartTileWithLabel(SmartTile):
font_style = StringProperty("Caption")
"""
Tile font style.
:attr:`font_style` is a :class:`~kivy.properties.StringProperty`
and defaults to `'Caption'`.
"""
tile_text_color = ListProperty((1, 1, 1, 1))
"""
Tile text color in ``rgba`` format.
:attr:`tile_text_color` is a :class:`~kivy.properties.StringProperty`
and defaults to `(1, 1, 1, 1)`.
"""
text = StringProperty()
"""
Determines the text for the box `footer/header`.
:attr:`text` is a :class:`~kivy.properties.StringProperty`
and defaults to `''`.
"""
_box_label = ObjectProperty()
class SmartTileWithStar(SmartTileWithLabel):
stars = NumericProperty(1)
"""
Tile stars.
:attr:`stars` is a :class:`~kivy.properties.NumericProperty`
and defaults to `1`.
"""
def on_stars(self, *args):
for star in range(self.stars):
self.ids.box.add_widget(
_Star(
icon="star-outline",
theme_text_color="Custom",
text_color=(1, 1, 1, 1),
)
)
class _Star(MDIconButton):
def on_touch_down(self, touch):
return True

400
kivymd/uix/label.py Executable file
View file

@ -0,0 +1,400 @@
"""
Components/Label
================
.. rubric:: The :class:`MDLabel` widget is for rendering text.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/label.png
:align: center
- MDLabel_
- MDIcon_
.. MDLabel:
MDLabel
-------
Class :class:`MDLabel` inherited from the :class:`~kivy.uix.label.Label` class
but for :class:`MDLabel` the ``text_size`` parameter is ``(self.width, None)``
and default is positioned on the left:
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
KV = '''
Screen:
BoxLayout:
orientation: "vertical"
MDToolbar:
title: "MDLabel"
MDLabel:
text: "MDLabel"
'''
class Test(MDApp):
def build(self):
return Builder.load_string(KV)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-to-left.png
:align: center
.. Note:: See :attr:`~kivy.uix.label.Label.halign`
and :attr:`~kivy.uix.label.Label.valign` attributes
of the :class:`~kivy.uix.label.Label` class
.. code-block:: kv
MDLabel:
text: "MDLabel"
halign: "center"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-to-center.png
:align: center
:class:`~MDLabel` color:
------------------------
:class:`~MDLabel` provides standard color themes for label color management:
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.label import MDLabel
KV = '''
Screen:
BoxLayout:
id: box
orientation: "vertical"
MDToolbar:
title: "MDLabel"
'''
class Test(MDApp):
def build(self):
screen = Builder.load_string(KV)
# Names of standard color themes.
for name_theme in [
"Primary",
"Secondary",
"Hint",
"Error",
"ContrastParentBackground",
]:
screen.ids.box.add_widget(
MDLabel(
text=name_theme,
halign="center",
theme_text_color=name_theme,
)
)
return screen
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-theme-text-color.png
:align: center
To use a custom color for :class:`~MDLabel`, use a theme `'Custom'`.
After that, you can specify the desired color in the ``rgba`` format
in the ``text_color`` parameter:
.. code-block:: kv
MDLabel:
text: "Custom color"
halign: "center"
theme_text_color: "Custom"
text_color: 0, 0, 1, 1
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-custom-color.png
:align: center
:class:`~MDLabel` provides standard font styles for labels. To do this,
specify the name of the desired style in the :attr:`~MDLabel.font_style`
parameter:
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
from kivymd.uix.label import MDLabel
from kivymd.font_definitions import theme_font_styles
KV = '''
Screen:
BoxLayout:
orientation: "vertical"
MDToolbar:
title: "MDLabel"
ScrollView:
MDList:
id: box
'''
class Test(MDApp):
def build(self):
screen = Builder.load_string(KV)
# Names of standard font styles.
for name_style in theme_font_styles[:-1]:
screen.ids.box.add_widget(
MDLabel(
text=f"{name_style} style",
halign="center",
font_style=name_style,
)
)
return screen
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-font-style.gif
:align: center
.. MDIcon:
MDIcon
-------
You can use labels to display material design icons using the
:class:`~MDIcon` class.
.. seealso::
`Material Design Icons <https://materialdesignicons.com/>`_
`Material Design Icon Names <https://github.com/kivymd/KivyMD/blob/master/kivymd/icon_definitions.py>`_
The :class:`~MDIcon` class is inherited from
:class:`~MDLabel` and has the same parameters.
.. Warning:: For the :class:`~MDIcon` class, you cannot use ``text``
and ``font_style`` options!
.. code-block:: kv
MDIcon:
halign: "center"
icon: "language-python"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon.png
:align: center
"""
__all__ = ("MDLabel", "MDIcon")
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import sp
from kivy.properties import (
AliasProperty,
BooleanProperty,
ListProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.label import Label
from kivymd.theming import ThemableBehavior
from kivymd.theming_dynamic_text import get_contrast_text_color
Builder.load_string(
"""
#:import md_icons kivymd.icon_definitions.md_icons
<MDLabel>
disabled_color: self.theme_cls.disabled_hint_text_color
text_size: self.width, None
<MDIcon>:
font_style: "Icon"
text: u"{}".format(md_icons[self.icon]) if self.icon in md_icons else ""
source: None if self.icon in md_icons else self.icon
canvas:
Color:
rgba: (1, 1, 1, 1) if self.source else (0, 0, 0, 0)
Rectangle:
source: self.source if self.source else None
pos: self.pos
size: self.size
"""
)
class MDLabel(ThemableBehavior, Label):
font_style = StringProperty("Body1")
"""
Label font style.
Available vanilla font_style are: `'H1'`, `'H2'`, `'H3'`, `'H4'`, `'H5'`, `'H6'`,
`'Subtitle1'`, `'Subtitle2'`, `'Body1'`, `'Body2'`, `'Button'`,
`'Caption'`, `'Overline'`, `'Icon'`.
:attr:`font_style` is an :class:`~kivy.properties.StringProperty`
and defaults to `'Body1'`.
"""
_capitalizing = BooleanProperty(False)
def _get_text(self):
if self._capitalizing:
return self._text.upper()
return self._text
def _set_text(self, value):
self._text = value
_text = StringProperty()
text = AliasProperty(_get_text, _set_text, bind=["_text", "_capitalizing"])
"""Text of the label."""
theme_text_color = OptionProperty(
"Primary",
allownone=True,
options=[
"Primary",
"Secondary",
"Hint",
"Error",
"Custom",
"ContrastParentBackground",
],
)
"""
Label color scheme name.
Available options are: `'Primary'`, `'Secondary'`, `'Hint'`, `'Error'`,
`'Custom'`, `'ContrastParentBackground'`.
:attr:`theme_text_color` is an :class:`~kivy.properties.OptionProperty`
and defaults to `None`.
"""
text_color = ListProperty(None, allownone=True)
"""Label text color in ``rgba`` format.
:attr:`text_color` is an :class:`~kivy.properties.ListProperty`
and defaults to `None`.
"""
parent_background = ListProperty(None, allownone=True)
_currently_bound_property = {}
can_capitalize = BooleanProperty(True)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(
font_style=self.update_font_style,
can_capitalize=self.update_font_style,
)
self.on_theme_text_color(None, self.theme_text_color)
self.update_font_style()
self.on_opposite_colors(None, self.opposite_colors)
Clock.schedule_once(self.check_font_styles)
def check_font_styles(self, *dt):
if self.font_style not in list(self.theme_cls.font_styles.keys()):
raise ValueError(
f"MDLabel.font_style is set to an invalid option '{self.font_style}'."
f"Must be one of: {list(self.theme_cls.font_styles)}"
)
else:
return True
def update_font_style(self, *args):
if self.check_font_styles() is True:
font_info = self.theme_cls.font_styles[self.font_style]
self.font_name = font_info[0]
self.font_size = sp(font_info[1])
if font_info[2] and self.can_capitalize:
self._capitalizing = True
else:
self._capitalizing = False
# TODO: Add letter spacing change
# self.letter_spacing = font_info[3]
def on_theme_text_color(self, instance, value):
t = self.theme_cls
op = self.opposite_colors
setter = self.setter("color")
t.unbind(**self._currently_bound_property)
attr_name = {
"Primary": "text_color" if not op else "opposite_text_color",
"Secondary": "secondary_text_color"
if not op
else "opposite_secondary_text_color",
"Hint": "disabled_hint_text_color"
if not op
else "opposite_disabled_hint_text_color",
"Error": "error_color",
}.get(value, None)
if attr_name:
c = {attr_name: setter}
t.bind(**c)
self._currently_bound_property = c
self.color = getattr(t, attr_name)
else:
# 'Custom' and 'ContrastParentBackground' lead here, as well as the
# generic None value it's not yet been set
if value == "Custom" and self.text_color:
self.color = self.text_color
elif value == "ContrastParentBackground" and self.parent_background:
self.color = get_contrast_text_color(self.parent_background)
else:
self.color = [0, 0, 0, 1]
def on_text_color(self, *args):
if self.theme_text_color == "Custom":
self.color = self.text_color
def on_opposite_colors(self, instance, value):
self.on_theme_text_color(self, self.theme_text_color)
class MDIcon(MDLabel):
icon = StringProperty("android")
"""
Label icon name.
:attr:`icon` is an :class:`~kivy.properties.StringProperty`
and defaults to `'android'`.
"""
source = StringProperty(None, allownone=True)
"""
Path to icon.
:attr:`source` is an :class:`~kivy.properties.StringProperty`
and defaults to `None`.
"""

1026
kivymd/uix/list.py Executable file

File diff suppressed because it is too large Load diff

1198
kivymd/uix/menu.py Executable file

File diff suppressed because it is too large Load diff

754
kivymd/uix/navigationdrawer.py Executable file
View file

@ -0,0 +1,754 @@
"""
Components/Navigation Drawer
============================
.. seealso::
`Material Design spec, Navigation drawer <https://material.io/components/navigation-drawer>`_
.. rubric:: Navigation drawers provide access to destinations in your app.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer.png
:align: center
When using the class :class:`~MDNavigationDrawer` skeleton of your `KV` markup
should look like this:
.. code-block:: kv
Root:
NavigationLayout:
ScreenManager:
Screen_1:
Screen_2:
MDNavigationDrawer:
# This custom rule should implement what will be appear in your MDNavigationDrawer
ContentNavigationDrawer
A simple example:
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivymd.app import MDApp
KV = '''
Screen:
NavigationLayout:
ScreenManager:
Screen:
BoxLayout:
orientation: 'vertical'
MDToolbar:
title: "Navigation Drawer"
elevation: 10
left_action_items: [['menu', lambda x: nav_drawer.toggle_nav_drawer()]]
Widget:
MDNavigationDrawer:
id: nav_drawer
ContentNavigationDrawer:
'''
class ContentNavigationDrawer(BoxLayout):
pass
class TestNavigationDrawer(MDApp):
def build(self):
return Builder.load_string(KV)
TestNavigationDrawer().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer.gif
:align: center
.. Note:: :class:`~MDNavigationDrawer` is an empty
:class:`~kivymd.uix.card.MDCard` panel.
Let's extend the ``ContentNavigationDrawer`` class from the above example and
create content for our :class:`~MDNavigationDrawer` panel:
.. code-block:: kv
# Menu item in the DrawerList list.
<ItemDrawer>:
theme_text_color: "Custom"
on_release: self.parent.set_color_item(self)
IconLeftWidget:
id: icon
icon: root.icon
theme_text_color: "Custom"
text_color: root.text_color
.. code-block:: python
class ItemDrawer(OneLineIconListItem):
icon = StringProperty()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/drawer-item.png
:align: center
Top of ``ContentNavigationDrawer`` and ``DrawerList`` for menu items:
.. code-block:: kv
<ContentNavigationDrawer>:
orientation: "vertical"
padding: "8dp"
spacing: "8dp"
AnchorLayout:
anchor_x: "left"
size_hint_y: None
height: avatar.height
Image:
id: avatar
size_hint: None, None
size: "56dp", "56dp"
source: "kivymd.png"
MDLabel:
text: "KivyMD library"
font_style: "Button"
size_hint_y: None
height: self.texture_size[1]
MDLabel:
text: "kivydevelopment@gmail.com"
font_style: "Caption"
size_hint_y: None
height: self.texture_size[1]
ScrollView:
DrawerList:
id: md_list
.. code-block:: python
class ContentNavigationDrawer(BoxLayout):
pass
class DrawerList(ThemableBehavior, MDList):
def set_color_item(self, instance_item):
'''Called when tap on a menu item.'''
# Set the color of the icon and text for the menu item.
for item in self.children:
if item.text_color == self.theme_cls.primary_color:
item.text_color = self.theme_cls.text_color
break
instance_item.text_color = self.theme_cls.primary_color
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/drawer-top.png
:align: center
Create a menu list for ``ContentNavigationDrawer``:
.. code-block:: python
def on_start(self):
icons_item = {
"folder": "My files",
"account-multiple": "Shared with me",
"star": "Starred",
"history": "Recent",
"checkbox-marked": "Shared with me",
"upload": "Upload",
}
for icon_name in icons_item.keys():
self.root.ids.content_drawer.ids.md_list.add_widget(
ItemDrawer(icon=icon_name, text=icons_item[icon_name])
)
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/drawer-work.gif
:align: center
Switching screens in the ``ScreenManager`` and using the common ``MDToolbar``
---------------------------------------------------------------------------
.. code-block:: python
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.properties import ObjectProperty
from kivymd.app import MDApp
KV = '''
<ContentNavigationDrawer>:
ScrollView:
MDList:
OneLineListItem:
text: "Screen 1"
on_press:
root.nav_drawer.set_state("close")
root.screen_manager.current = "scr 1"
OneLineListItem:
text: "Screen 2"
on_press:
root.nav_drawer.set_state("close")
root.screen_manager.current = "scr 2"
Screen:
MDToolbar:
id: toolbar
pos_hint: {"top": 1}
elevation: 10
title: "MDNavigationDrawer"
left_action_items: [["menu", lambda x: nav_drawer.set_state("open")]]
NavigationLayout:
x: toolbar.height
ScreenManager:
id: screen_manager
Screen:
name: "scr 1"
MDLabel:
text: "Screen 1"
halign: "center"
Screen:
name: "scr 2"
MDLabel:
text: "Screen 2"
halign: "center"
MDNavigationDrawer:
id: nav_drawer
ContentNavigationDrawer:
screen_manager: screen_manager
nav_drawer: nav_drawer
'''
class ContentNavigationDrawer(BoxLayout):
screen_manager = ObjectProperty()
nav_drawer = ObjectProperty()
class TestNavigationDrawer(MDApp):
def build(self):
return Builder.load_string(KV)
TestNavigationDrawer().run()
NavigationDrawer with type ``standard``
---------------------------------------
You can use the ``standard`` behavior type for the NavigationDrawer:
.. code-block:: kv
MDNavigationDrawer:
type: "standard"
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-standard.gif
:align: center
.. seealso::
`Full example of Components-Navigation-Drawer <https://github.com/kivymd/KivyMD/wiki/Components-Navigation-Drawer>`_
"""
__all__ = ("NavigationLayout", "MDNavigationDrawer")
from kivy.animation import Animation, AnimationTransition
from kivy.core.window import Window
from kivy.graphics.context_instructions import Color
from kivy.graphics.vertex_instructions import Rectangle
from kivy.lang import Builder
from kivy.logger import Logger
from kivy.properties import (
AliasProperty,
BooleanProperty,
ListProperty,
NumericProperty,
ObjectProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import ScreenManager
from kivymd.uix.card import MDCard
from kivymd.uix.toolbar import MDToolbar
Builder.load_string(
"""
#:import Window kivy.core.window.Window
<MDNavigationDrawer>:
size_hint_x: None
width: Window.width - dp(56) if Window.width <= dp(376) else dp(320)
x:
(self.width * (self.open_progress - 1)) \
if self.anchor == "left" \
else (Window.width - self.width * self.open_progress)
elevation: 10
canvas:
Clear
Color:
rgba: self.md_bg_color
RoundedRectangle:
size: self.size
pos: self.pos
source: root.background
radius: root._radius
md_bg_color: self.theme_cls.bg_light
"""
)
class NavigationDrawerContentError(Exception):
pass
class NavigationLayout(FloatLayout):
_scrim_color = ObjectProperty(None)
_scrim_rectangle = ObjectProperty(None)
_screen_manager = ObjectProperty(None)
_navigation_drawer = ObjectProperty(None)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(width=self.update_pos)
def update_pos(self, *args):
drawer = self._navigation_drawer
manager = self._screen_manager
if not drawer or not manager:
return
if drawer.type == "standard":
manager.size_hint_x = None
if drawer.anchor == "left":
manager.x = drawer.width + drawer.x
manager.width = self.width - manager.x
else:
manager.x = 0
manager.width = drawer.x
elif drawer.type == "modal":
manager.size_hint_x = None
manager.x = 0
if drawer.anchor == "left":
manager.width = self.width - manager.x
else:
manager.width = self.width
def add_scrim(self, widget):
with widget.canvas.after:
self._scrim_color = Color(rgba=[0, 0, 0, 0])
self._scrim_rectangle = Rectangle(pos=widget.pos, size=widget.size)
widget.bind(
pos=self.update_scrim_rectangle,
size=self.update_scrim_rectangle,
)
def update_scrim_rectangle(self, *args):
self._scrim_rectangle.pos = self.pos
self._scrim_rectangle.size = self.size
def add_widget(self, widget, index=0, canvas=None):
"""
Only two layouts are allowed:
:class:`~kivy.uix.screenmanager.ScreenManager` and
:class:`~MDNavigationDrawer`.
"""
if not isinstance(
widget, (MDNavigationDrawer, ScreenManager, MDToolbar)
):
raise NavigationDrawerContentError(
"The NavigationLayout must contain "
"only `MDNavigationDrawer` and `ScreenManager`"
)
if isinstance(widget, ScreenManager):
self._screen_manager = widget
self.add_scrim(widget)
if isinstance(widget, MDNavigationDrawer):
self._navigation_drawer = widget
widget.bind(
x=self.update_pos, width=self.update_pos, anchor=self.update_pos
)
if len(self.children) > 3:
raise NavigationDrawerContentError(
"The NavigationLayout must contain "
"only `MDNavigationDrawer` and `ScreenManager`"
)
return super().add_widget(widget)
class MDNavigationDrawer(MDCard):
type = OptionProperty("modal", options=("standard", "modal"))
"""
Type of drawer. Modal type will be on top of screen. Standard type will be
at left or right of screen. Also it automatically disables
:attr:`close_on_click` and :attr:`enable_swiping` to prevent closing
drawer for standard type.
:attr:`type` is a :class:`~kivy.properties.OptionProperty`
and defaults to `modal`.
"""
anchor = OptionProperty("left", options=("left", "right"))
"""
Anchoring screen edge for drawer. Set it to `'right'` for right-to-left
languages. Available options are: `'left'`, `'right'`.
:attr:`anchor` is a :class:`~kivy.properties.OptionProperty`
and defaults to `left`.
"""
close_on_click = BooleanProperty(True)
"""
Close when click on scrim or keyboard escape. It automatically sets to
False for "standard" type.
:attr:`close_on_click` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
state = OptionProperty("close", options=("close", "open"))
"""
Indicates if panel closed or opened. Sets after :attr:`status` change.
Available options are: `'close'`, `'open'`.
:attr:`state` is a :class:`~kivy.properties.OptionProperty`
and defaults to `'close'`.
"""
status = OptionProperty(
"closed",
options=(
"closed",
"opening_with_swipe",
"opening_with_animation",
"opened",
"closing_with_swipe",
"closing_with_animation",
),
)
"""
Detailed state. Sets before :attr:`state`. Bind to :attr:`state` instead
of :attr:`status`. Available options are: `'closed'`,
`'opening_with_swipe'`, `'opening_with_animation'`, `'opened'`,
`'closing_with_swipe'`, `'closing_with_animation'`.
:attr:`status` is a :class:`~kivy.properties.OptionProperty`
and defaults to `'closed'`.
"""
open_progress = NumericProperty(0.0)
"""
Percent of visible part of side panel. The percent is specified as a
floating point number in the range 0-1. 0.0 if panel is closed and 1.0 if
panel is opened.
:attr:`open_progress` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.0`.
"""
enable_swiping = BooleanProperty(True)
"""
Allow to open or close navigation drawer with swipe. It automatically
sets to False for "standard" type.
:attr:`enable_swiping` is a :class:`~kivy.properties.BooleanProperty`
and defaults to `True`.
"""
swipe_distance = NumericProperty(10)
"""
The distance of the swipe with which the movement of navigation drawer
begins.
:attr:`swipe_distance` is a :class:`~kivy.properties.NumericProperty`
and defaults to `10`.
"""
swipe_edge_width = NumericProperty(20)
"""
The size of the area in px inside which should start swipe to drag
navigation drawer.
:attr:`swipe_edge_width` is a :class:`~kivy.properties.NumericProperty`
and defaults to `20`.
"""
scrim_color = ListProperty([0, 0, 0, 0.5])
"""
Color for scrim. Alpha channel will be multiplied with
:attr:`_scrim_alpha`. Set fourth channel to 0 if you want to disable
scrim.
:attr:`scrim_color` is a :class:`~kivy.properties.ListProperty`
and defaults to `[0, 0, 0, 0.5]`.
"""
_radius = ListProperty([0, 0, 0, 0])
def _get_scrim_alpha(self):
_scrim_alpha = 0
if self.type == "modal":
_scrim_alpha = self._scrim_alpha_transition(self.open_progress)
if (
isinstance(self.parent, NavigationLayout)
and self.parent._scrim_color
):
self.parent._scrim_color.rgba = self.scrim_color[:3] + [
self.scrim_color[3] * _scrim_alpha
]
return _scrim_alpha
_scrim_alpha = AliasProperty(
_get_scrim_alpha,
None,
bind=("_scrim_alpha_transition", "open_progress", "scrim_color"),
)
"""
Multiplier for alpha channel of :attr:`scrim_color`. For internal
usage only.
"""
scrim_alpha_transition = StringProperty("linear")
"""
The name of the animation transition type to use for changing
:attr:`scrim_alpha`.
:attr:`scrim_alpha_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'linear'`.
"""
def _get_scrim_alpha_transition(self):
return getattr(AnimationTransition, self.scrim_alpha_transition)
_scrim_alpha_transition = AliasProperty(
_get_scrim_alpha_transition,
None,
bind=("scrim_alpha_transition",),
cache=True,
)
opening_transition = StringProperty("out_cubic")
"""
The name of the animation transition type to use when animating to
the :attr:`state` `'open'`.
:attr:`opening_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_cubic'`.
"""
opening_time = NumericProperty(0.2)
"""
The time taken for the panel to slide to the :attr:`state` `'open'`.
:attr:`opening_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
closing_transition = StringProperty("out_sine")
"""The name of the animation transition type to use when animating to
the :attr:`state` 'close'.
:attr:`closing_transition` is a :class:`~kivy.properties.StringProperty`
and defaults to `'out_sine'`.
"""
closing_time = NumericProperty(0.2)
"""
The time taken for the panel to slide to the :attr:`state` `'close'`.
:attr:`closing_time` is a :class:`~kivy.properties.NumericProperty`
and defaults to `0.2`.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.bind(
open_progress=self.update_status,
status=self.update_status,
state=self.update_status,
)
Window.bind(on_keyboard=self._handle_keyboard)
def set_state(self, new_state="toggle", animation=True):
"""Change state of the side panel.
New_state can be one of `"toggle"`, `"open"` or `"close"`.
"""
if new_state == "toggle":
new_state = "close" if self.state == "open" else "open"
if new_state == "open":
Animation.cancel_all(self, "open_progress")
self.status = "opening_with_animation"
if animation:
Animation(
open_progress=1.0,
d=self.opening_time * (1 - self.open_progress),
t=self.opening_transition,
).start(self)
else:
self.open_progress = 1
else: # "close"
Animation.cancel_all(self, "open_progress")
self.status = "closing_with_animation"
if animation:
Animation(
open_progress=0.0,
d=self.closing_time * self.open_progress,
t=self.closing_transition,
).start(self)
else:
self.open_progress = 0
def toggle_nav_drawer(self):
Logger.warning(
"KivyMD: The 'toggle_nav_drawer' method is deprecated, "
"use 'set_state' instead."
)
self.set_state("toggle", animation=True)
def update_status(self, *_):
status = self.status
if status == "closed":
self.state = "close"
elif status == "opened":
self.state = "open"
elif self.open_progress == 1 and status == "opening_with_animation":
self.status = "opened"
self.state = "open"
elif self.open_progress == 0 and status == "closing_with_animation":
self.status = "closed"
self.state = "close"
elif status in (
"opening_with_swipe",
"opening_with_animation",
"closing_with_swipe",
"closing_with_animation",
):
pass
if self.status == "closed":
self._elevation = 0
self._update_shadow(self, self._elevation)
else:
self._elevation = self.elevation
self._update_shadow(self, self._elevation)
def get_dist_from_side(self, x):
if self.anchor == "left":
return 0 if x < 0 else x
return 0 if x > Window.width else Window.width - x
def on_touch_down(self, touch):
if self.status == "closed":
return False
elif self.status == "opened":
for child in self.children[:]:
if child.dispatch("on_touch_down", touch):
return True
if self.type == "standard" and not self.collide_point(
touch.ox, touch.oy
):
return False
return True
def on_touch_move(self, touch):
if self.enable_swiping:
if self.status == "closed":
if (
self.get_dist_from_side(touch.ox) <= self.swipe_edge_width
and abs(touch.x - touch.ox) > self.swipe_distance
):
self.status = "opening_with_swipe"
elif self.status == "opened":
self.status = "closing_with_swipe"
if self.status in ("opening_with_swipe", "closing_with_swipe"):
self.open_progress = max(
min(
self.open_progress
+ (touch.dx if self.anchor == "left" else -touch.dx)
/ self.width,
1,
),
0,
)
return True
return super().on_touch_move(touch)
def on_touch_up(self, touch):
if self.status == "opening_with_swipe":
if self.open_progress > 0.5:
self.set_state("open", animation=True)
else:
self.set_state("close", animation=True)
elif self.status == "closing_with_swipe":
if self.open_progress < 0.5:
self.set_state("close", animation=True)
else:
self.set_state("open", animation=True)
elif self.status == "opened":
if self.close_on_click and not self.collide_point(
touch.ox, touch.oy
):
self.set_state("close", animation=True)
elif self.type == "standard" and not self.collide_point(
touch.ox, touch.oy
):
return False
elif self.status == "closed":
return False
return True
def on_radius(self, instance, value):
self._radius = value
def on_type(self, *args):
if self.type == "standard":
self.enable_swiping = False
self.close_on_click = False
else:
self.enable_swiping = True
self.close_on_click = True
def _handle_keyboard(self, window, key, *largs):
if key == 27 and self.status == "opened" and self.close_on_click:
self.set_state("close")
return True

1114
kivymd/uix/picker.py Executable file

File diff suppressed because it is too large Load diff

313
kivymd/uix/progressbar.py Executable file
View file

@ -0,0 +1,313 @@
"""
Components/Progress Bar
=======================
.. seealso::
`Material Design spec, Progress indicators https://material.io/components/progress-indicators`_
.. rubric:: Progress indicators express an unspecified wait time or display
the length of a process.
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/progress-bar-preview.png
:align: center
`KivyMD` provides the following bars classes for use:
- MDProgressBar_
- Determinate_
- Indeterminate_
.. MDProgressBar:
MDProgressBar
-------------
.. code-block:: python
from kivy.lang import Builder
from kivymd.app import MDApp
KV = '''
BoxLayout:
padding: "10dp"
MDProgressBar:
value: 50
'''
class Test(MDApp):
def build(self):
return Builder.load_string(KV)
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/progress-bar.png
:align: center
Vertical orientation
--------------------
.. code-block:: kv
MDProgressBar:
orientation: "vertical"
value: 50
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/progress-bar-vertical.png
:align: center
With custom color
-----------------
.. code-block:: kv
MDProgressBar:
value: 50
color: app.theme_cls.accent_color
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/progress-bar-custom-color.png
:align: center
.. Indeterminate:
Indeterminate
-------------
.. code-block:: python
from kivy.lang import Builder
from kivy.properties import StringProperty
from kivymd.app import MDApp
KV = '''
Screen:
MDProgressBar:
id: progress
pos_hint: {"center_y": .6}
type: "indeterminate"
MDRaisedButton:
text: "START"
pos_hint: {"center_x": .5, "center_y": .45}
on_press: app.state = "stop" if app.state == "start" else "start"
'''
class Test(MDApp):
state = StringProperty("stop")
def build(self):
return Builder.load_string(KV)
def on_state(self, instance, value):
{
"start": self.root.ids.progress.start,
"stop": self.root.ids.progress.stop,
}.get(value)()
Test().run()
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/indeterminate-progress-bar.gif
:align: center
.. Determinate:
Determinate
-----------
.. code-block:: kv
MDProgressBar:
type: "determinate"
running_duration: 1
catching_duration: 1.5
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/determinate-progress-bar.gif
:align: center
"""
__all__ = ("MDProgressBar",)
from kivy.animation import Animation
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.properties import (
BooleanProperty,
ListProperty,
NumericProperty,
OptionProperty,
StringProperty,
)
from kivy.uix.progressbar import ProgressBar
from kivymd.theming import ThemableBehavior
Builder.load_string(
"""
<MDProgressBar>
canvas:
Clear
Color:
rgba: self.theme_cls.divider_color
Rectangle:
size:
(self.width, dp(4)) \
if self.orientation == "horizontal" \
else (dp(4), self.height)
pos:
(self.x, self.center_y - dp(4)) \
if self.orientation == "horizontal" \
else (self.center_x - dp(4),self.y)
Color:
rgba:
self.theme_cls.primary_color if not self.color else self.color
Rectangle:
size:
(self.width * self.value_normalized, sp(4)) \
if self.orientation == "horizontal" \
else (sp(4), self.height * self.value_normalized)
pos:
(self.width * (1 - self.value_normalized) + self.x \
if self.reversed else self.x + self._x, self.center_y - dp(4)) \
if self.orientation == "horizontal" \
else (self.center_x - dp(4),self.height \
* (1 - self.value_normalized) + self.y if self.reversed \
else self.y)
"""
)
class MDProgressBar(ThemableBehavior, ProgressBar):
reversed = BooleanProperty(False)
"""Reverse the direction the progressbar moves.
:attr:`reversed` is an :class:`~kivy.properties.BooleanProperty`
and defaults to `False`.
"""
orientation = OptionProperty(
"horizontal", options=["horizontal", "vertical"]
)
"""Orientation of progressbar. Available options are: `'horizontal '`,
`'vertical'`.
:attr:`orientation` is an :class:`~kivy.properties.OptionProperty`
and defaults to `'horizontal'`.
"""
color = ListProperty()
"""
Progress bar color in ``rgba`` format.
:attr:`color` is an :class:`~kivy.properties.OptionProperty`
and defaults to `[]`.
"""
running_transition = StringProperty("in_cubic")
"""Running transition.
:attr:`running_transition` is an :class:`~kivy.properties.StringProperty`
and defaults to `'in_cubic'`.
"""
catching_transition = StringProperty("out_quart")
"""Catching transition.
:attr:`catching_transition` is an :class:`~kivy.properties.StringProperty`
and defaults to `'out_quart'`.
"""
running_duration = NumericProperty(0.5)
"""Running duration.
:attr:`running_duration` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.5`.
"""
catching_duration = NumericProperty(0.8)
"""Catching duration.
:attr:`running_duration` is an :class:`~kivy.properties.NumericProperty`
and defaults to `0.8`.
"""
type = OptionProperty(
None, options=["indeterminate", "determinate"], allownone=True
)
"""Type of progressbar. Available options are: `'indeterminate '`,
`'determinate'`.
:attr:`type` is an :class:`~kivy.properties.OptionProperty`
and defaults to `None`.
"""
_x = NumericProperty(0)
def __init__(self, **kwargs):
self.catching_anim = None
self.running_anim = None
super().__init__(**kwargs)
def start(self):
"""Start animation."""
if self.type in ("indeterminate", "determinate"):
Clock.schedule_once(self._set_default_value)
if not self.catching_anim and not self.running_anim:
if self.type == "indeterminate":
self._create_indeterminate_animations()
else:
self._create_determinate_animations()
self.running_away()
def stop(self):
"""Stop animation."""
Animation.cancel_all(self)
self._set_default_value(0)
def running_away(self, *args):
self._set_default_value(0)
self.running_anim.start(self)
def catching_up(self, *args):
if self.type == "indeterminate":
self.reversed = True
self.catching_anim.start(self)
def _create_determinate_animations(self):
self.running_anim = Animation(
value=100,
opacity=1,
t=self.running_transition,
d=self.running_duration,
)
self.running_anim.bind(on_complete=self.catching_up)
self.catching_anim = Animation(
opacity=0,
t=self.catching_transition,
d=self.catching_duration,
)
self.catching_anim.bind(on_complete=self.running_away)
def _create_indeterminate_animations(self):
self.running_anim = Animation(
_x=self.width / 2,
value=50,
t=self.running_transition,
d=self.running_duration,
)
self.running_anim.bind(on_complete=self.catching_up)
self.catching_anim = Animation(
value=0, t=self.catching_transition, d=self.catching_duration
)
self.catching_anim.bind(on_complete=self.running_away)
def _set_default_value(self, interval):
self._x = 0
self.value = 0
self.reversed = False

223
kivymd/uix/refreshlayout.py Executable file
View file

@ -0,0 +1,223 @@
"""
Components/Refresh Layout
=========================
Example
-------
.. code-block:: python
from kivymd.app import MDApp
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.properties import StringProperty
from kivymd.uix.button import MDIconButton
from kivymd.icon_definitions import md_icons
from kivymd.uix.list import ILeftBodyTouch, OneLineIconListItem
from kivymd.theming import ThemeManager
from kivymd.utils import asynckivy
Builder.load_string('''
<ItemForList>
text: root.text
IconLeftSampleWidget:
icon: root.icon
<Example@FloatLayout>
BoxLayout:
orientation: 'vertical'
MDToolbar:
title: app.title
md_bg_color: app.theme_cls.primary_color
background_palette: 'Primary'
elevation: 10
left_action_items: [['menu', lambda x: x]]
MDScrollViewRefreshLayout:
id: refresh_layout
refresh_callback: app.refresh_callback
root_layout: root
MDGridLayout:
id: box
adaptive_height: True
cols: 1
''')
class IconLeftSampleWidget(ILeftBodyTouch, MDIconButton):
pass
class ItemForList(OneLineIconListItem):
icon = StringProperty()
class Example(MDApp):
title = 'Example Refresh Layout'
screen = None
x = 0
y = 15
def build(self):
self.screen = Factory.Example()
self.set_list()
return self.screen
def set_list(self):
async def set_list():
names_icons_list = list(md_icons.keys())[self.x:self.y]
for name_icon in names_icons_list:
await asynckivy.sleep(0)
self.screen.ids.box.add_widget(
ItemForList(icon=name_icon, text=name_icon))
asynckivy.start(set_list())
def refresh_callback(self, *args):
'''A method that updates the state of your application
while the spinner remains on the screen.'''
def refresh_callback(interval):
self.screen.ids.box.clear_widgets()
if self.x == 0:
self.x, self.y = 15, 30
else:
self.x, self.y = 0, 15
self.set_list()
self.screen.ids.refresh_layout.refresh_done()
self.tick = 0
Clock.schedule_once(refresh_callback, 1)
Example().run()
"""
from kivy.animation import Animation
from kivy.core.window import Window
from kivy.effects.dampedscroll import DampedScrollEffect
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import ListProperty, NumericProperty, ObjectProperty
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.scrollview import ScrollView
from kivymd.theming import ThemableBehavior
Builder.load_string(
"""
#:import Window kivy.core.window.Window
<RefreshSpinner>
AnchorLayout:
id: body_spinner
size_hint: None, None
size: dp(46), dp(46)
y: Window.height
pos_hint: {'center_x': .5}
anchor_x: 'center'
anchor_y: 'center'
canvas:
Clear
Color:
rgba: root.theme_cls.primary_dark
Ellipse:
pos: self.pos
size: self.size
MDSpinner:
id: spinner
size_hint: None, None
size: dp(30), dp(30)
color: 1, 1, 1, 1
"""
)
class _RefreshScrollEffect(DampedScrollEffect):
"""This class is simply based on DampedScrollEffect.
If you need any documentation please look at kivy.effects.dampedscrolleffect.
"""
min_scroll_to_reload = NumericProperty("-100dp")
"""Minimum overscroll value to reload."""
def on_overscroll(self, scrollview, overscroll):
if overscroll < self.min_scroll_to_reload:
scroll_view = self.target_widget.parent
scroll_view._did_overscroll = True
return True
else:
return False
class MDScrollViewRefreshLayout(ScrollView):
root_layout = ObjectProperty()
"""The spinner will be attached to this layout."""
def __init__(self, **kargs):
super().__init__(**kargs)
self.effect_cls = _RefreshScrollEffect
self._work_spinnrer = False
self._did_overscroll = False
self.refresh_spinner = None
def on_touch_up(self, *args):
if self._did_overscroll and not self._work_spinnrer:
if self.refresh_callback:
self.refresh_callback()
if not self.refresh_spinner:
self.refresh_spinner = RefreshSpinner(_refresh_layout=self)
self.root_layout.add_widget(self.refresh_spinner)
self.refresh_spinner.start_anim_spinner()
self._work_spinnrer = True
self._did_overscroll = False
return True
return super().on_touch_up(*args)
def refresh_done(self):
if self.refresh_spinner:
self.refresh_spinner.hide_anim_spinner()
class RefreshSpinner(ThemableBehavior, FloatLayout):
spinner_color = ListProperty([1, 1, 1, 1])
_refresh_layout = ObjectProperty()
"""kivymd.refreshlayout.MDScrollViewRefreshLayout object."""
def start_anim_spinner(self):
spinner = self.ids.body_spinner
Animation(
y=spinner.y - self.theme_cls.standard_increment * 2 + dp(10),
d=0.8,
t="out_elastic",
).start(spinner)
def hide_anim_spinner(self):
spinner = self.ids.body_spinner
anim = Animation(y=Window.height, d=0.8, t="out_elastic")
anim.bind(on_complete=self.set_spinner)
anim.start(spinner)
def set_spinner(self, *args):
body_spinner = self.ids.body_spinner
body_spinner.size = (dp(46), dp(46))
body_spinner.y = Window.height
body_spinner.opacity = 1
spinner = self.ids.spinner
spinner.size = (dp(30), dp(30))
spinner.opacity = 1
self._refresh_layout._work_spinnrer = False
self._refresh_layout._did_overscroll = False

Some files were not shown because too many files have changed in this diff Show more