diff --git a/assets/images/marker.png b/assets/images/marker.png new file mode 100644 index 0000000..acebcf4 Binary files /dev/null and b/assets/images/marker.png differ diff --git a/buildozer.spec b/buildozer.spec index b2f750e..e15647c 100644 --- a/buildozer.spec +++ b/buildozer.spec @@ -36,7 +36,7 @@ version = 0.1 # (list) Application requirements # 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 # Sets custom source for any requirements with recipes diff --git a/forestmapview.kv b/forestmapview.kv index 7cf0923..83bbb7a 100644 --- a/forestmapview.kv +++ b/forestmapview.kv @@ -7,7 +7,7 @@ lon: 24.747571 zoom: 15 on_zoom: - self.zoom = 15 if self.zoom < 15 else self.zoom + self.zoom = 5 if self.zoom < 5 else self.zoom on_lat: self.start_get_fov_trees() on_lon: diff --git a/forestmapview.py b/forestmapview.py index a952ada..5d5719a 100644 --- a/forestmapview.py +++ b/forestmapview.py @@ -2,8 +2,11 @@ from kivy_garden.mapview import MapView from kivy.clock import Clock from kivy.app import App +from treemarker import TreeMarker + class ForestMapView(MapView): get_trees_timer = None + tree_names = [] def start_get_fov_trees(self): # After one second get the trees in field of view @@ -17,15 +20,31 @@ class ForestMapView(MapView): def get_fov_trees(self, *args): # Get reference to main app and the db cursor 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() 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) trees = app.cursor.fetchall() - print(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): - pass - \ No newline at end of file + # 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) \ No newline at end of file diff --git a/kivymd/__init__.py b/kivymd/__init__.py new file mode 100644 index 0000000..1aad13b --- /dev/null +++ b/kivymd/__init__.py @@ -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 `_ +a framework for cross-platform, touch-enabled graphical applications. +The project's goal is to approximate Google's `Material Design spec +`_ as close as possible without +sacrificing ease of use or application performance. + +This library is a fork of the `KivyMD project +`_ 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 diff --git a/kivymd/app.py b/kivymd/app.py new file mode 100644 index 0000000..833d85a --- /dev/null +++ b/kivymd/app.py @@ -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() diff --git a/kivymd/color_definitions.py b/kivymd/color_definitions.py new file mode 100755 index 0000000..6e56f15 --- /dev/null +++ b/kivymd/color_definitions.py @@ -0,0 +1,944 @@ +""" +Themes/Color Definitions +======================== + +.. seealso:: + + `Material Design spec, The color system `_ + +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 +`_. + +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 = ''' + + 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 + + + : + size_hint_y: None + height: "42dp" + + canvas: + Color: + rgba: root.color + Rectangle: + size: self.size + pos: self.pos + + MDLabel: + text: root.text + halign: "center" + + + : + ''' + + 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.""" diff --git a/kivymd/factory_registers.py b/kivymd/factory_registers.py new file mode 100644 index 0000000..7ed4d12 --- /dev/null +++ b/kivymd/factory_registers.py @@ -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") diff --git a/kivymd/font_definitions.py b/kivymd/font_definitions.py new file mode 100755 index 0000000..e7cc8e2 --- /dev/null +++ b/kivymd/font_definitions.py @@ -0,0 +1,69 @@ +""" +Themes/Font Definitions +======================= + +.. seealso:: + + `Material Design spec, The type system `_ +""" + +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 +""" diff --git a/kivymd/fonts/Roboto-Black.ttf b/kivymd/fonts/Roboto-Black.ttf new file mode 100644 index 0000000..689fe5c Binary files /dev/null and b/kivymd/fonts/Roboto-Black.ttf differ diff --git a/kivymd/fonts/Roboto-BlackItalic.ttf b/kivymd/fonts/Roboto-BlackItalic.ttf new file mode 100644 index 0000000..0b4e0ee Binary files /dev/null and b/kivymd/fonts/Roboto-BlackItalic.ttf differ diff --git a/kivymd/fonts/Roboto-Bold.ttf b/kivymd/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..d3f01ad Binary files /dev/null and b/kivymd/fonts/Roboto-Bold.ttf differ diff --git a/kivymd/fonts/Roboto-BoldItalic.ttf b/kivymd/fonts/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..41cc1e7 Binary files /dev/null and b/kivymd/fonts/Roboto-BoldItalic.ttf differ diff --git a/kivymd/fonts/Roboto-Italic.ttf b/kivymd/fonts/Roboto-Italic.ttf new file mode 100644 index 0000000..6a1cee5 Binary files /dev/null and b/kivymd/fonts/Roboto-Italic.ttf differ diff --git a/kivymd/fonts/Roboto-Light.ttf b/kivymd/fonts/Roboto-Light.ttf new file mode 100644 index 0000000..219063a Binary files /dev/null and b/kivymd/fonts/Roboto-Light.ttf differ diff --git a/kivymd/fonts/Roboto-LightItalic.ttf b/kivymd/fonts/Roboto-LightItalic.ttf new file mode 100644 index 0000000..0e81e87 Binary files /dev/null and b/kivymd/fonts/Roboto-LightItalic.ttf differ diff --git a/kivymd/fonts/Roboto-Medium.ttf b/kivymd/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..1a7f3b0 Binary files /dev/null and b/kivymd/fonts/Roboto-Medium.ttf differ diff --git a/kivymd/fonts/Roboto-MediumItalic.ttf b/kivymd/fonts/Roboto-MediumItalic.ttf new file mode 100644 index 0000000..0030295 Binary files /dev/null and b/kivymd/fonts/Roboto-MediumItalic.ttf differ diff --git a/kivymd/fonts/Roboto-Regular.ttf b/kivymd/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..2c97eea Binary files /dev/null and b/kivymd/fonts/Roboto-Regular.ttf differ diff --git a/kivymd/fonts/Roboto-Thin.ttf b/kivymd/fonts/Roboto-Thin.ttf new file mode 100644 index 0000000..b74a4fd Binary files /dev/null and b/kivymd/fonts/Roboto-Thin.ttf differ diff --git a/kivymd/fonts/Roboto-ThinItalic.ttf b/kivymd/fonts/Roboto-ThinItalic.ttf new file mode 100644 index 0000000..dd0ddb8 Binary files /dev/null and b/kivymd/fonts/Roboto-ThinItalic.ttf differ diff --git a/kivymd/fonts/materialdesignicons-webfont.ttf b/kivymd/fonts/materialdesignicons-webfont.ttf new file mode 100644 index 0000000..17457d5 Binary files /dev/null and b/kivymd/fonts/materialdesignicons-webfont.ttf differ diff --git a/kivymd/icon_definitions.py b/kivymd/icon_definitions.py new file mode 100755 index 0000000..876dfb4 --- /dev/null +++ b/kivymd/icon_definitions.py @@ -0,0 +1,5782 @@ +""" +Themes/Icon Definitions +======================= + +.. seealso:: + + `Material Design Icons `_ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/material-icons.png + :align: center + +List of icons from materialdesignicons.com. These expanded material design +icons are maintained by Austin Andrews (Templarian on Github). + +LAST UPDATED: Version 5.6.55 + +To preview the icons and their names, you can use the following application: +---------------------------------------------------------------------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import StringProperty + from kivy.uix.screenmanager import Screen + + from kivymd.icon_definitions import md_icons + from kivymd.app import MDApp + from kivymd.uix.list import OneLineIconListItem + + + Builder.load_string( + ''' + #:import images_path kivymd.images_path + + + : + + IconLeftWidget: + icon: root.icon + + + : + + BoxLayout: + orientation: 'vertical' + spacing: dp(10) + padding: dp(20) + + BoxLayout: + size_hint_y: None + height: self.minimum_height + + MDIconButton: + icon: 'magnify' + + MDTextField: + id: search_field + hint_text: 'Search icon' + on_text: root.set_list_md_icons(self.text, True) + + RecycleView: + id: rv + key_viewclass: 'viewclass' + key_size: 'height' + + RecycleBoxLayout: + padding: dp(10) + default_size: None, dp(48) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + ''' + ) + + + class CustomOneLineIconListItem(OneLineIconListItem): + icon = StringProperty() + + + class PreviousMDIcons(Screen): + + def set_list_md_icons(self, text="", search=False): + '''Builds a list of icons for the screen MDIcons.''' + + def add_icon_item(name_icon): + self.ids.rv.data.append( + { + "viewclass": "CustomOneLineIconListItem", + "icon": name_icon, + "text": name_icon, + "callback": lambda x: x, + } + ) + + self.ids.rv.data = [] + for name_icon in md_icons.keys(): + if search: + if text in name_icon: + add_icon_item(name_icon) + else: + add_icon_item(name_icon) + + + class MainApp(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = PreviousMDIcons() + + def build(self): + return self.screen + + def on_start(self): + self.screen.set_list_md_icons() + + + MainApp().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icons-previous.gif + :align: center + +""" + +# THIS DICTIONARY WAS GENERATED BY ``kivymd/tools/update_icons.py`` +md_icons = { + "ab-testing": "\U000F01C9", + "abjad-arabic": "\U000F1328", + "abjad-hebrew": "\U000F1329", + "abugida-devanagari": "\U000F132A", + "abugida-thai": "\U000F132B", + "access-point": "\U000F0003", + "access-point-check": "\U000F1538", + "access-point-minus": "\U000F1539", + "access-point-network": "\U000F0002", + "access-point-network-off": "\U000F0BE1", + "access-point-off": "\U000F1511", + "access-point-plus": "\U000F153A", + "access-point-remove": "\U000F153B", + "account": "\U000F0004", + "account-alert": "\U000F0005", + "account-alert-outline": "\U000F0B50", + "account-arrow-left": "\U000F0B51", + "account-arrow-left-outline": "\U000F0B52", + "account-arrow-right": "\U000F0B53", + "account-arrow-right-outline": "\U000F0B54", + "account-box": "\U000F0006", + "account-box-multiple": "\U000F0934", + "account-box-multiple-outline": "\U000F100A", + "account-box-outline": "\U000F0007", + "account-cancel": "\U000F12DF", + "account-cancel-outline": "\U000F12E0", + "account-cash": "\U000F1097", + "account-cash-outline": "\U000F1098", + "account-check": "\U000F0008", + "account-check-outline": "\U000F0BE2", + "account-child": "\U000F0A89", + "account-child-circle": "\U000F0A8A", + "account-child-outline": "\U000F10C8", + "account-circle": "\U000F0009", + "account-circle-outline": "\U000F0B55", + "account-clock": "\U000F0B56", + "account-clock-outline": "\U000F0B57", + "account-cog": "\U000F1370", + "account-cog-outline": "\U000F1371", + "account-convert": "\U000F000A", + "account-convert-outline": "\U000F1301", + "account-cowboy-hat": "\U000F0E9B", + "account-details": "\U000F0631", + "account-details-outline": "\U000F1372", + "account-edit": "\U000F06BC", + "account-edit-outline": "\U000F0FFB", + "account-group": "\U000F0849", + "account-group-outline": "\U000F0B58", + "account-hard-hat": "\U000F05B5", + "account-heart": "\U000F0899", + "account-heart-outline": "\U000F0BE3", + "account-key": "\U000F000B", + "account-key-outline": "\U000F0BE4", + "account-lock": "\U000F115E", + "account-lock-outline": "\U000F115F", + "account-minus": "\U000F000D", + "account-minus-outline": "\U000F0AEC", + "account-multiple": "\U000F000E", + "account-multiple-check": "\U000F08C5", + "account-multiple-check-outline": "\U000F11FE", + "account-multiple-minus": "\U000F05D3", + "account-multiple-minus-outline": "\U000F0BE5", + "account-multiple-outline": "\U000F000F", + "account-multiple-plus": "\U000F0010", + "account-multiple-plus-outline": "\U000F0800", + "account-multiple-remove": "\U000F120A", + "account-multiple-remove-outline": "\U000F120B", + "account-music": "\U000F0803", + "account-music-outline": "\U000F0CE9", + "account-network": "\U000F0011", + "account-network-outline": "\U000F0BE6", + "account-off": "\U000F0012", + "account-off-outline": "\U000F0BE7", + "account-outline": "\U000F0013", + "account-plus": "\U000F0014", + "account-plus-outline": "\U000F0801", + "account-question": "\U000F0B59", + "account-question-outline": "\U000F0B5A", + "account-reactivate": "\U000F152B", + "account-reactivate-outline": "\U000F152C", + "account-remove": "\U000F0015", + "account-remove-outline": "\U000F0AED", + "account-search": "\U000F0016", + "account-search-outline": "\U000F0935", + "account-settings": "\U000F0630", + "account-settings-outline": "\U000F10C9", + "account-star": "\U000F0017", + "account-star-outline": "\U000F0BE8", + "account-supervisor": "\U000F0A8B", + "account-supervisor-circle": "\U000F0A8C", + "account-supervisor-circle-outline": "\U000F14EC", + "account-supervisor-outline": "\U000F112D", + "account-switch": "\U000F0019", + "account-switch-outline": "\U000F04CB", + "account-tie": "\U000F0CE3", + "account-tie-outline": "\U000F10CA", + "account-tie-voice": "\U000F1308", + "account-tie-voice-off": "\U000F130A", + "account-tie-voice-off-outline": "\U000F130B", + "account-tie-voice-outline": "\U000F1309", + "account-voice": "\U000F05CB", + "adjust": "\U000F001A", + "adobe": "\U000F0936", + "adobe-acrobat": "\U000F0F9D", + "air-conditioner": "\U000F001B", + "air-filter": "\U000F0D43", + "air-horn": "\U000F0DAC", + "air-humidifier": "\U000F1099", + "air-humidifier-off": "\U000F1466", + "air-purifier": "\U000F0D44", + "airbag": "\U000F0BE9", + "airballoon": "\U000F001C", + "airballoon-outline": "\U000F100B", + "airplane": "\U000F001D", + "airplane-landing": "\U000F05D4", + "airplane-off": "\U000F001E", + "airplane-takeoff": "\U000F05D5", + "airport": "\U000F084B", + "alarm": "\U000F0020", + "alarm-bell": "\U000F078E", + "alarm-check": "\U000F0021", + "alarm-light": "\U000F078F", + "alarm-light-outline": "\U000F0BEA", + "alarm-multiple": "\U000F0022", + "alarm-note": "\U000F0E71", + "alarm-note-off": "\U000F0E72", + "alarm-off": "\U000F0023", + "alarm-panel": "\U000F15C4", + "alarm-panel-outline": "\U000F15C5", + "alarm-plus": "\U000F0024", + "alarm-snooze": "\U000F068E", + "album": "\U000F0025", + "alert": "\U000F0026", + "alert-box": "\U000F0027", + "alert-box-outline": "\U000F0CE4", + "alert-circle": "\U000F0028", + "alert-circle-check": "\U000F11ED", + "alert-circle-check-outline": "\U000F11EE", + "alert-circle-outline": "\U000F05D6", + "alert-decagram": "\U000F06BD", + "alert-decagram-outline": "\U000F0CE5", + "alert-minus": "\U000F14BB", + "alert-minus-outline": "\U000F14BE", + "alert-octagon": "\U000F0029", + "alert-octagon-outline": "\U000F0CE6", + "alert-octagram": "\U000F0767", + "alert-octagram-outline": "\U000F0CE7", + "alert-outline": "\U000F002A", + "alert-plus": "\U000F14BA", + "alert-plus-outline": "\U000F14BD", + "alert-remove": "\U000F14BC", + "alert-remove-outline": "\U000F14BF", + "alert-rhombus": "\U000F11CE", + "alert-rhombus-outline": "\U000F11CF", + "alien": "\U000F089A", + "alien-outline": "\U000F10CB", + "align-horizontal-center": "\U000F11C3", + "align-horizontal-left": "\U000F11C2", + "align-horizontal-right": "\U000F11C4", + "align-vertical-bottom": "\U000F11C5", + "align-vertical-center": "\U000F11C6", + "align-vertical-top": "\U000F11C7", + "all-inclusive": "\U000F06BE", + "allergy": "\U000F1258", + "alpha": "\U000F002B", + "alpha-a": "\U000F0AEE", + "alpha-a-box": "\U000F0B08", + "alpha-a-box-outline": "\U000F0BEB", + "alpha-a-circle": "\U000F0BEC", + "alpha-a-circle-outline": "\U000F0BED", + "alpha-b": "\U000F0AEF", + "alpha-b-box": "\U000F0B09", + "alpha-b-box-outline": "\U000F0BEE", + "alpha-b-circle": "\U000F0BEF", + "alpha-b-circle-outline": "\U000F0BF0", + "alpha-c": "\U000F0AF0", + "alpha-c-box": "\U000F0B0A", + "alpha-c-box-outline": "\U000F0BF1", + "alpha-c-circle": "\U000F0BF2", + "alpha-c-circle-outline": "\U000F0BF3", + "alpha-d": "\U000F0AF1", + "alpha-d-box": "\U000F0B0B", + "alpha-d-box-outline": "\U000F0BF4", + "alpha-d-circle": "\U000F0BF5", + "alpha-d-circle-outline": "\U000F0BF6", + "alpha-e": "\U000F0AF2", + "alpha-e-box": "\U000F0B0C", + "alpha-e-box-outline": "\U000F0BF7", + "alpha-e-circle": "\U000F0BF8", + "alpha-e-circle-outline": "\U000F0BF9", + "alpha-f": "\U000F0AF3", + "alpha-f-box": "\U000F0B0D", + "alpha-f-box-outline": "\U000F0BFA", + "alpha-f-circle": "\U000F0BFB", + "alpha-f-circle-outline": "\U000F0BFC", + "alpha-g": "\U000F0AF4", + "alpha-g-box": "\U000F0B0E", + "alpha-g-box-outline": "\U000F0BFD", + "alpha-g-circle": "\U000F0BFE", + "alpha-g-circle-outline": "\U000F0BFF", + "alpha-h": "\U000F0AF5", + "alpha-h-box": "\U000F0B0F", + "alpha-h-box-outline": "\U000F0C00", + "alpha-h-circle": "\U000F0C01", + "alpha-h-circle-outline": "\U000F0C02", + "alpha-i": "\U000F0AF6", + "alpha-i-box": "\U000F0B10", + "alpha-i-box-outline": "\U000F0C03", + "alpha-i-circle": "\U000F0C04", + "alpha-i-circle-outline": "\U000F0C05", + "alpha-j": "\U000F0AF7", + "alpha-j-box": "\U000F0B11", + "alpha-j-box-outline": "\U000F0C06", + "alpha-j-circle": "\U000F0C07", + "alpha-j-circle-outline": "\U000F0C08", + "alpha-k": "\U000F0AF8", + "alpha-k-box": "\U000F0B12", + "alpha-k-box-outline": "\U000F0C09", + "alpha-k-circle": "\U000F0C0A", + "alpha-k-circle-outline": "\U000F0C0B", + "alpha-l": "\U000F0AF9", + "alpha-l-box": "\U000F0B13", + "alpha-l-box-outline": "\U000F0C0C", + "alpha-l-circle": "\U000F0C0D", + "alpha-l-circle-outline": "\U000F0C0E", + "alpha-m": "\U000F0AFA", + "alpha-m-box": "\U000F0B14", + "alpha-m-box-outline": "\U000F0C0F", + "alpha-m-circle": "\U000F0C10", + "alpha-m-circle-outline": "\U000F0C11", + "alpha-n": "\U000F0AFB", + "alpha-n-box": "\U000F0B15", + "alpha-n-box-outline": "\U000F0C12", + "alpha-n-circle": "\U000F0C13", + "alpha-n-circle-outline": "\U000F0C14", + "alpha-o": "\U000F0AFC", + "alpha-o-box": "\U000F0B16", + "alpha-o-box-outline": "\U000F0C15", + "alpha-o-circle": "\U000F0C16", + "alpha-o-circle-outline": "\U000F0C17", + "alpha-p": "\U000F0AFD", + "alpha-p-box": "\U000F0B17", + "alpha-p-box-outline": "\U000F0C18", + "alpha-p-circle": "\U000F0C19", + "alpha-p-circle-outline": "\U000F0C1A", + "alpha-q": "\U000F0AFE", + "alpha-q-box": "\U000F0B18", + "alpha-q-box-outline": "\U000F0C1B", + "alpha-q-circle": "\U000F0C1C", + "alpha-q-circle-outline": "\U000F0C1D", + "alpha-r": "\U000F0AFF", + "alpha-r-box": "\U000F0B19", + "alpha-r-box-outline": "\U000F0C1E", + "alpha-r-circle": "\U000F0C1F", + "alpha-r-circle-outline": "\U000F0C20", + "alpha-s": "\U000F0B00", + "alpha-s-box": "\U000F0B1A", + "alpha-s-box-outline": "\U000F0C21", + "alpha-s-circle": "\U000F0C22", + "alpha-s-circle-outline": "\U000F0C23", + "alpha-t": "\U000F0B01", + "alpha-t-box": "\U000F0B1B", + "alpha-t-box-outline": "\U000F0C24", + "alpha-t-circle": "\U000F0C25", + "alpha-t-circle-outline": "\U000F0C26", + "alpha-u": "\U000F0B02", + "alpha-u-box": "\U000F0B1C", + "alpha-u-box-outline": "\U000F0C27", + "alpha-u-circle": "\U000F0C28", + "alpha-u-circle-outline": "\U000F0C29", + "alpha-v": "\U000F0B03", + "alpha-v-box": "\U000F0B1D", + "alpha-v-box-outline": "\U000F0C2A", + "alpha-v-circle": "\U000F0C2B", + "alpha-v-circle-outline": "\U000F0C2C", + "alpha-w": "\U000F0B04", + "alpha-w-box": "\U000F0B1E", + "alpha-w-box-outline": "\U000F0C2D", + "alpha-w-circle": "\U000F0C2E", + "alpha-w-circle-outline": "\U000F0C2F", + "alpha-x": "\U000F0B05", + "alpha-x-box": "\U000F0B1F", + "alpha-x-box-outline": "\U000F0C30", + "alpha-x-circle": "\U000F0C31", + "alpha-x-circle-outline": "\U000F0C32", + "alpha-y": "\U000F0B06", + "alpha-y-box": "\U000F0B20", + "alpha-y-box-outline": "\U000F0C33", + "alpha-y-circle": "\U000F0C34", + "alpha-y-circle-outline": "\U000F0C35", + "alpha-z": "\U000F0B07", + "alpha-z-box": "\U000F0B21", + "alpha-z-box-outline": "\U000F0C36", + "alpha-z-circle": "\U000F0C37", + "alpha-z-circle-outline": "\U000F0C38", + "alphabet-aurebesh": "\U000F132C", + "alphabet-cyrillic": "\U000F132D", + "alphabet-greek": "\U000F132E", + "alphabet-latin": "\U000F132F", + "alphabet-piqad": "\U000F1330", + "alphabet-tengwar": "\U000F1337", + "alphabetical": "\U000F002C", + "alphabetical-off": "\U000F100C", + "alphabetical-variant": "\U000F100D", + "alphabetical-variant-off": "\U000F100E", + "altimeter": "\U000F05D7", + "amazon": "\U000F002D", + "amazon-alexa": "\U000F08C6", + "ambulance": "\U000F002F", + "ammunition": "\U000F0CE8", + "ampersand": "\U000F0A8D", + "amplifier": "\U000F0030", + "amplifier-off": "\U000F11B5", + "anchor": "\U000F0031", + "android": "\U000F0032", + "android-auto": "\U000F0A8E", + "android-debug-bridge": "\U000F0033", + "android-messages": "\U000F0D45", + "android-studio": "\U000F0034", + "angle-acute": "\U000F0937", + "angle-obtuse": "\U000F0938", + "angle-right": "\U000F0939", + "angular": "\U000F06B2", + "angularjs": "\U000F06BF", + "animation": "\U000F05D8", + "animation-outline": "\U000F0A8F", + "animation-play": "\U000F093A", + "animation-play-outline": "\U000F0A90", + "ansible": "\U000F109A", + "antenna": "\U000F1119", + "anvil": "\U000F089B", + "apache-kafka": "\U000F100F", + "api": "\U000F109B", + "api-off": "\U000F1257", + "apple": "\U000F0035", + "apple-airplay": "\U000F001F", + "apple-finder": "\U000F0036", + "apple-icloud": "\U000F0038", + "apple-ios": "\U000F0037", + "apple-keyboard-caps": "\U000F0632", + "apple-keyboard-command": "\U000F0633", + "apple-keyboard-control": "\U000F0634", + "apple-keyboard-option": "\U000F0635", + "apple-keyboard-shift": "\U000F0636", + "apple-safari": "\U000F0039", + "application": "\U000F0614", + "application-cog": "\U000F1577", + "application-export": "\U000F0DAD", + "application-import": "\U000F0DAE", + "application-settings": "\U000F1555", + "approximately-equal": "\U000F0F9E", + "approximately-equal-box": "\U000F0F9F", + "apps": "\U000F003B", + "apps-box": "\U000F0D46", + "arch": "\U000F08C7", + "archive": "\U000F003C", + "archive-alert": "\U000F14FD", + "archive-alert-outline": "\U000F14FE", + "archive-arrow-down": "\U000F1259", + "archive-arrow-down-outline": "\U000F125A", + "archive-arrow-up": "\U000F125B", + "archive-arrow-up-outline": "\U000F125C", + "archive-outline": "\U000F120E", + "arm-flex": "\U000F0FD7", + "arm-flex-outline": "\U000F0FD6", + "arrange-bring-forward": "\U000F003D", + "arrange-bring-to-front": "\U000F003E", + "arrange-send-backward": "\U000F003F", + "arrange-send-to-back": "\U000F0040", + "arrow-all": "\U000F0041", + "arrow-bottom-left": "\U000F0042", + "arrow-bottom-left-bold-outline": "\U000F09B7", + "arrow-bottom-left-thick": "\U000F09B8", + "arrow-bottom-left-thin-circle-outline": "\U000F1596", + "arrow-bottom-right": "\U000F0043", + "arrow-bottom-right-bold-outline": "\U000F09B9", + "arrow-bottom-right-thick": "\U000F09BA", + "arrow-bottom-right-thin-circle-outline": "\U000F1595", + "arrow-collapse": "\U000F0615", + "arrow-collapse-all": "\U000F0044", + "arrow-collapse-down": "\U000F0792", + "arrow-collapse-horizontal": "\U000F084C", + "arrow-collapse-left": "\U000F0793", + "arrow-collapse-right": "\U000F0794", + "arrow-collapse-up": "\U000F0795", + "arrow-collapse-vertical": "\U000F084D", + "arrow-decision": "\U000F09BB", + "arrow-decision-auto": "\U000F09BC", + "arrow-decision-auto-outline": "\U000F09BD", + "arrow-decision-outline": "\U000F09BE", + "arrow-down": "\U000F0045", + "arrow-down-bold": "\U000F072E", + "arrow-down-bold-box": "\U000F072F", + "arrow-down-bold-box-outline": "\U000F0730", + "arrow-down-bold-circle": "\U000F0047", + "arrow-down-bold-circle-outline": "\U000F0048", + "arrow-down-bold-hexagon-outline": "\U000F0049", + "arrow-down-bold-outline": "\U000F09BF", + "arrow-down-box": "\U000F06C0", + "arrow-down-circle": "\U000F0CDB", + "arrow-down-circle-outline": "\U000F0CDC", + "arrow-down-drop-circle": "\U000F004A", + "arrow-down-drop-circle-outline": "\U000F004B", + "arrow-down-thick": "\U000F0046", + "arrow-down-thin-circle-outline": "\U000F1599", + "arrow-expand": "\U000F0616", + "arrow-expand-all": "\U000F004C", + "arrow-expand-down": "\U000F0796", + "arrow-expand-horizontal": "\U000F084E", + "arrow-expand-left": "\U000F0797", + "arrow-expand-right": "\U000F0798", + "arrow-expand-up": "\U000F0799", + "arrow-expand-vertical": "\U000F084F", + "arrow-horizontal-lock": "\U000F115B", + "arrow-left": "\U000F004D", + "arrow-left-bold": "\U000F0731", + "arrow-left-bold-box": "\U000F0732", + "arrow-left-bold-box-outline": "\U000F0733", + "arrow-left-bold-circle": "\U000F004F", + "arrow-left-bold-circle-outline": "\U000F0050", + "arrow-left-bold-hexagon-outline": "\U000F0051", + "arrow-left-bold-outline": "\U000F09C0", + "arrow-left-box": "\U000F06C1", + "arrow-left-circle": "\U000F0CDD", + "arrow-left-circle-outline": "\U000F0CDE", + "arrow-left-drop-circle": "\U000F0052", + "arrow-left-drop-circle-outline": "\U000F0053", + "arrow-left-right": "\U000F0E73", + "arrow-left-right-bold": "\U000F0E74", + "arrow-left-right-bold-outline": "\U000F09C1", + "arrow-left-thick": "\U000F004E", + "arrow-left-thin-circle-outline": "\U000F159A", + "arrow-right": "\U000F0054", + "arrow-right-bold": "\U000F0734", + "arrow-right-bold-box": "\U000F0735", + "arrow-right-bold-box-outline": "\U000F0736", + "arrow-right-bold-circle": "\U000F0056", + "arrow-right-bold-circle-outline": "\U000F0057", + "arrow-right-bold-hexagon-outline": "\U000F0058", + "arrow-right-bold-outline": "\U000F09C2", + "arrow-right-box": "\U000F06C2", + "arrow-right-circle": "\U000F0CDF", + "arrow-right-circle-outline": "\U000F0CE0", + "arrow-right-drop-circle": "\U000F0059", + "arrow-right-drop-circle-outline": "\U000F005A", + "arrow-right-thick": "\U000F0055", + "arrow-right-thin-circle-outline": "\U000F1598", + "arrow-split-horizontal": "\U000F093B", + "arrow-split-vertical": "\U000F093C", + "arrow-top-left": "\U000F005B", + "arrow-top-left-bold-outline": "\U000F09C3", + "arrow-top-left-bottom-right": "\U000F0E75", + "arrow-top-left-bottom-right-bold": "\U000F0E76", + "arrow-top-left-thick": "\U000F09C4", + "arrow-top-left-thin-circle-outline": "\U000F1593", + "arrow-top-right": "\U000F005C", + "arrow-top-right-bold-outline": "\U000F09C5", + "arrow-top-right-bottom-left": "\U000F0E77", + "arrow-top-right-bottom-left-bold": "\U000F0E78", + "arrow-top-right-thick": "\U000F09C6", + "arrow-top-right-thin-circle-outline": "\U000F1594", + "arrow-up": "\U000F005D", + "arrow-up-bold": "\U000F0737", + "arrow-up-bold-box": "\U000F0738", + "arrow-up-bold-box-outline": "\U000F0739", + "arrow-up-bold-circle": "\U000F005F", + "arrow-up-bold-circle-outline": "\U000F0060", + "arrow-up-bold-hexagon-outline": "\U000F0061", + "arrow-up-bold-outline": "\U000F09C7", + "arrow-up-box": "\U000F06C3", + "arrow-up-circle": "\U000F0CE1", + "arrow-up-circle-outline": "\U000F0CE2", + "arrow-up-down": "\U000F0E79", + "arrow-up-down-bold": "\U000F0E7A", + "arrow-up-down-bold-outline": "\U000F09C8", + "arrow-up-drop-circle": "\U000F0062", + "arrow-up-drop-circle-outline": "\U000F0063", + "arrow-up-thick": "\U000F005E", + "arrow-up-thin-circle-outline": "\U000F1597", + "arrow-vertical-lock": "\U000F115C", + "artstation": "\U000F0B5B", + "aspect-ratio": "\U000F0A24", + "assistant": "\U000F0064", + "asterisk": "\U000F06C4", + "at": "\U000F0065", + "atlassian": "\U000F0804", + "atm": "\U000F0D47", + "atom": "\U000F0768", + "atom-variant": "\U000F0E7B", + "attachment": "\U000F0066", + "audio-video": "\U000F093D", + "audio-video-off": "\U000F11B6", + "augmented-reality": "\U000F0850", + "auto-download": "\U000F137E", + "auto-fix": "\U000F0068", + "auto-upload": "\U000F0069", + "autorenew": "\U000F006A", + "av-timer": "\U000F006B", + "aws": "\U000F0E0F", + "axe": "\U000F08C8", + "axis": "\U000F0D48", + "axis-arrow": "\U000F0D49", + "axis-arrow-info": "\U000F140E", + "axis-arrow-lock": "\U000F0D4A", + "axis-lock": "\U000F0D4B", + "axis-x-arrow": "\U000F0D4C", + "axis-x-arrow-lock": "\U000F0D4D", + "axis-x-rotate-clockwise": "\U000F0D4E", + "axis-x-rotate-counterclockwise": "\U000F0D4F", + "axis-x-y-arrow-lock": "\U000F0D50", + "axis-y-arrow": "\U000F0D51", + "axis-y-arrow-lock": "\U000F0D52", + "axis-y-rotate-clockwise": "\U000F0D53", + "axis-y-rotate-counterclockwise": "\U000F0D54", + "axis-z-arrow": "\U000F0D55", + "axis-z-arrow-lock": "\U000F0D56", + "axis-z-rotate-clockwise": "\U000F0D57", + "axis-z-rotate-counterclockwise": "\U000F0D58", + "babel": "\U000F0A25", + "baby": "\U000F006C", + "baby-bottle": "\U000F0F39", + "baby-bottle-outline": "\U000F0F3A", + "baby-buggy": "\U000F13E0", + "baby-carriage": "\U000F068F", + "baby-carriage-off": "\U000F0FA0", + "baby-face": "\U000F0E7C", + "baby-face-outline": "\U000F0E7D", + "backburger": "\U000F006D", + "backspace": "\U000F006E", + "backspace-outline": "\U000F0B5C", + "backspace-reverse": "\U000F0E7E", + "backspace-reverse-outline": "\U000F0E7F", + "backup-restore": "\U000F006F", + "bacteria": "\U000F0ED5", + "bacteria-outline": "\U000F0ED6", + "badge-account": "\U000F0DA7", + "badge-account-alert": "\U000F0DA8", + "badge-account-alert-outline": "\U000F0DA9", + "badge-account-horizontal": "\U000F0E0D", + "badge-account-horizontal-outline": "\U000F0E0E", + "badge-account-outline": "\U000F0DAA", + "badminton": "\U000F0851", + "bag-carry-on": "\U000F0F3B", + "bag-carry-on-check": "\U000F0D65", + "bag-carry-on-off": "\U000F0F3C", + "bag-checked": "\U000F0F3D", + "bag-personal": "\U000F0E10", + "bag-personal-off": "\U000F0E11", + "bag-personal-off-outline": "\U000F0E12", + "bag-personal-outline": "\U000F0E13", + "bag-suitcase": "\U000F158B", + "bag-suitcase-off": "\U000F158D", + "bag-suitcase-off-outline": "\U000F158E", + "bag-suitcase-outline": "\U000F158C", + "baguette": "\U000F0F3E", + "balloon": "\U000F0A26", + "ballot": "\U000F09C9", + "ballot-outline": "\U000F09CA", + "ballot-recount": "\U000F0C39", + "ballot-recount-outline": "\U000F0C3A", + "bandage": "\U000F0DAF", + "bandcamp": "\U000F0675", + "bank": "\U000F0070", + "bank-minus": "\U000F0DB0", + "bank-outline": "\U000F0E80", + "bank-plus": "\U000F0DB1", + "bank-remove": "\U000F0DB2", + "bank-transfer": "\U000F0A27", + "bank-transfer-in": "\U000F0A28", + "bank-transfer-out": "\U000F0A29", + "barcode": "\U000F0071", + "barcode-off": "\U000F1236", + "barcode-scan": "\U000F0072", + "barley": "\U000F0073", + "barley-off": "\U000F0B5D", + "barn": "\U000F0B5E", + "barrel": "\U000F0074", + "baseball": "\U000F0852", + "baseball-bat": "\U000F0853", + "baseball-diamond": "\U000F15EC", + "baseball-diamond-outline": "\U000F15ED", + "bash": "\U000F1183", + "basket": "\U000F0076", + "basket-fill": "\U000F0077", + "basket-minus": "\U000F1523", + "basket-minus-outline": "\U000F1524", + "basket-off": "\U000F1525", + "basket-off-outline": "\U000F1526", + "basket-outline": "\U000F1181", + "basket-plus": "\U000F1527", + "basket-plus-outline": "\U000F1528", + "basket-remove": "\U000F1529", + "basket-remove-outline": "\U000F152A", + "basket-unfill": "\U000F0078", + "basketball": "\U000F0806", + "basketball-hoop": "\U000F0C3B", + "basketball-hoop-outline": "\U000F0C3C", + "bat": "\U000F0B5F", + "battery": "\U000F0079", + "battery-10": "\U000F007A", + "battery-10-bluetooth": "\U000F093E", + "battery-20": "\U000F007B", + "battery-20-bluetooth": "\U000F093F", + "battery-30": "\U000F007C", + "battery-30-bluetooth": "\U000F0940", + "battery-40": "\U000F007D", + "battery-40-bluetooth": "\U000F0941", + "battery-50": "\U000F007E", + "battery-50-bluetooth": "\U000F0942", + "battery-60": "\U000F007F", + "battery-60-bluetooth": "\U000F0943", + "battery-70": "\U000F0080", + "battery-70-bluetooth": "\U000F0944", + "battery-80": "\U000F0081", + "battery-80-bluetooth": "\U000F0945", + "battery-90": "\U000F0082", + "battery-90-bluetooth": "\U000F0946", + "battery-alert": "\U000F0083", + "battery-alert-bluetooth": "\U000F0947", + "battery-alert-variant": "\U000F10CC", + "battery-alert-variant-outline": "\U000F10CD", + "battery-bluetooth": "\U000F0948", + "battery-bluetooth-variant": "\U000F0949", + "battery-charging": "\U000F0084", + "battery-charging-10": "\U000F089C", + "battery-charging-100": "\U000F0085", + "battery-charging-20": "\U000F0086", + "battery-charging-30": "\U000F0087", + "battery-charging-40": "\U000F0088", + "battery-charging-50": "\U000F089D", + "battery-charging-60": "\U000F0089", + "battery-charging-70": "\U000F089E", + "battery-charging-80": "\U000F008A", + "battery-charging-90": "\U000F008B", + "battery-charging-high": "\U000F12A6", + "battery-charging-low": "\U000F12A4", + "battery-charging-medium": "\U000F12A5", + "battery-charging-outline": "\U000F089F", + "battery-charging-wireless": "\U000F0807", + "battery-charging-wireless-10": "\U000F0808", + "battery-charging-wireless-20": "\U000F0809", + "battery-charging-wireless-30": "\U000F080A", + "battery-charging-wireless-40": "\U000F080B", + "battery-charging-wireless-50": "\U000F080C", + "battery-charging-wireless-60": "\U000F080D", + "battery-charging-wireless-70": "\U000F080E", + "battery-charging-wireless-80": "\U000F080F", + "battery-charging-wireless-90": "\U000F0810", + "battery-charging-wireless-alert": "\U000F0811", + "battery-charging-wireless-outline": "\U000F0812", + "battery-heart": "\U000F120F", + "battery-heart-outline": "\U000F1210", + "battery-heart-variant": "\U000F1211", + "battery-high": "\U000F12A3", + "battery-low": "\U000F12A1", + "battery-medium": "\U000F12A2", + "battery-minus": "\U000F008C", + "battery-negative": "\U000F008D", + "battery-off": "\U000F125D", + "battery-off-outline": "\U000F125E", + "battery-outline": "\U000F008E", + "battery-plus": "\U000F008F", + "battery-positive": "\U000F0090", + "battery-unknown": "\U000F0091", + "battery-unknown-bluetooth": "\U000F094A", + "battlenet": "\U000F0B60", + "beach": "\U000F0092", + "beaker": "\U000F0CEA", + "beaker-alert": "\U000F1229", + "beaker-alert-outline": "\U000F122A", + "beaker-check": "\U000F122B", + "beaker-check-outline": "\U000F122C", + "beaker-minus": "\U000F122D", + "beaker-minus-outline": "\U000F122E", + "beaker-outline": "\U000F0690", + "beaker-plus": "\U000F122F", + "beaker-plus-outline": "\U000F1230", + "beaker-question": "\U000F1231", + "beaker-question-outline": "\U000F1232", + "beaker-remove": "\U000F1233", + "beaker-remove-outline": "\U000F1234", + "bed": "\U000F02E3", + "bed-double": "\U000F0FD4", + "bed-double-outline": "\U000F0FD3", + "bed-empty": "\U000F08A0", + "bed-king": "\U000F0FD2", + "bed-king-outline": "\U000F0FD1", + "bed-outline": "\U000F0099", + "bed-queen": "\U000F0FD0", + "bed-queen-outline": "\U000F0FDB", + "bed-single": "\U000F106D", + "bed-single-outline": "\U000F106E", + "bee": "\U000F0FA1", + "bee-flower": "\U000F0FA2", + "beehive-off-outline": "\U000F13ED", + "beehive-outline": "\U000F10CE", + "beekeeper": "\U000F14E2", + "beer": "\U000F0098", + "beer-outline": "\U000F130C", + "bell": "\U000F009A", + "bell-alert": "\U000F0D59", + "bell-alert-outline": "\U000F0E81", + "bell-cancel": "\U000F13E7", + "bell-cancel-outline": "\U000F13E8", + "bell-check": "\U000F11E5", + "bell-check-outline": "\U000F11E6", + "bell-circle": "\U000F0D5A", + "bell-circle-outline": "\U000F0D5B", + "bell-minus": "\U000F13E9", + "bell-minus-outline": "\U000F13EA", + "bell-off": "\U000F009B", + "bell-off-outline": "\U000F0A91", + "bell-outline": "\U000F009C", + "bell-plus": "\U000F009D", + "bell-plus-outline": "\U000F0A92", + "bell-remove": "\U000F13EB", + "bell-remove-outline": "\U000F13EC", + "bell-ring": "\U000F009E", + "bell-ring-outline": "\U000F009F", + "bell-sleep": "\U000F00A0", + "bell-sleep-outline": "\U000F0A93", + "beta": "\U000F00A1", + "betamax": "\U000F09CB", + "biathlon": "\U000F0E14", + "bicycle": "\U000F109C", + "bicycle-basket": "\U000F1235", + "bicycle-electric": "\U000F15B4", + "bicycle-penny-farthing": "\U000F15E9", + "bike": "\U000F00A3", + "bike-fast": "\U000F111F", + "billboard": "\U000F1010", + "billiards": "\U000F0B61", + "billiards-rack": "\U000F0B62", + "binoculars": "\U000F00A5", + "bio": "\U000F00A6", + "biohazard": "\U000F00A7", + "bird": "\U000F15C6", + "bitbucket": "\U000F00A8", + "bitcoin": "\U000F0813", + "black-mesa": "\U000F00A9", + "blender": "\U000F0CEB", + "blender-software": "\U000F00AB", + "blinds": "\U000F00AC", + "blinds-open": "\U000F1011", + "block-helper": "\U000F00AD", + "blogger": "\U000F00AE", + "blood-bag": "\U000F0CEC", + "bluetooth": "\U000F00AF", + "bluetooth-audio": "\U000F00B0", + "bluetooth-connect": "\U000F00B1", + "bluetooth-off": "\U000F00B2", + "bluetooth-settings": "\U000F00B3", + "bluetooth-transfer": "\U000F00B4", + "blur": "\U000F00B5", + "blur-linear": "\U000F00B6", + "blur-off": "\U000F00B7", + "blur-radial": "\U000F00B8", + "bolnisi-cross": "\U000F0CED", + "bolt": "\U000F0DB3", + "bomb": "\U000F0691", + "bomb-off": "\U000F06C5", + "bone": "\U000F00B9", + "book": "\U000F00BA", + "book-account": "\U000F13AD", + "book-account-outline": "\U000F13AE", + "book-alphabet": "\U000F061D", + "book-check": "\U000F14F3", + "book-check-outline": "\U000F14F4", + "book-cross": "\U000F00A2", + "book-information-variant": "\U000F106F", + "book-lock": "\U000F079A", + "book-lock-open": "\U000F079B", + "book-minus": "\U000F05D9", + "book-minus-multiple": "\U000F0A94", + "book-minus-multiple-outline": "\U000F090B", + "book-multiple": "\U000F00BB", + "book-multiple-outline": "\U000F0436", + "book-music": "\U000F0067", + "book-open": "\U000F00BD", + "book-open-blank-variant": "\U000F00BE", + "book-open-outline": "\U000F0B63", + "book-open-page-variant": "\U000F05DA", + "book-open-page-variant-outline": "\U000F15D6", + "book-open-variant": "\U000F14F7", + "book-outline": "\U000F0B64", + "book-play": "\U000F0E82", + "book-play-outline": "\U000F0E83", + "book-plus": "\U000F05DB", + "book-plus-multiple": "\U000F0A95", + "book-plus-multiple-outline": "\U000F0ADE", + "book-remove": "\U000F0A97", + "book-remove-multiple": "\U000F0A96", + "book-remove-multiple-outline": "\U000F04CA", + "book-search": "\U000F0E84", + "book-search-outline": "\U000F0E85", + "book-variant": "\U000F00BF", + "book-variant-multiple": "\U000F00BC", + "bookmark": "\U000F00C0", + "bookmark-check": "\U000F00C1", + "bookmark-check-outline": "\U000F137B", + "bookmark-minus": "\U000F09CC", + "bookmark-minus-outline": "\U000F09CD", + "bookmark-multiple": "\U000F0E15", + "bookmark-multiple-outline": "\U000F0E16", + "bookmark-music": "\U000F00C2", + "bookmark-music-outline": "\U000F1379", + "bookmark-off": "\U000F09CE", + "bookmark-off-outline": "\U000F09CF", + "bookmark-outline": "\U000F00C3", + "bookmark-plus": "\U000F00C5", + "bookmark-plus-outline": "\U000F00C4", + "bookmark-remove": "\U000F00C6", + "bookmark-remove-outline": "\U000F137A", + "bookshelf": "\U000F125F", + "boom-gate": "\U000F0E86", + "boom-gate-alert": "\U000F0E87", + "boom-gate-alert-outline": "\U000F0E88", + "boom-gate-down": "\U000F0E89", + "boom-gate-down-outline": "\U000F0E8A", + "boom-gate-outline": "\U000F0E8B", + "boom-gate-up": "\U000F0E8C", + "boom-gate-up-outline": "\U000F0E8D", + "boombox": "\U000F05DC", + "boomerang": "\U000F10CF", + "bootstrap": "\U000F06C6", + "border-all": "\U000F00C7", + "border-all-variant": "\U000F08A1", + "border-bottom": "\U000F00C8", + "border-bottom-variant": "\U000F08A2", + "border-color": "\U000F00C9", + "border-horizontal": "\U000F00CA", + "border-inside": "\U000F00CB", + "border-left": "\U000F00CC", + "border-left-variant": "\U000F08A3", + "border-none": "\U000F00CD", + "border-none-variant": "\U000F08A4", + "border-outside": "\U000F00CE", + "border-right": "\U000F00CF", + "border-right-variant": "\U000F08A5", + "border-style": "\U000F00D0", + "border-top": "\U000F00D1", + "border-top-variant": "\U000F08A6", + "border-vertical": "\U000F00D2", + "bottle-soda": "\U000F1070", + "bottle-soda-classic": "\U000F1071", + "bottle-soda-classic-outline": "\U000F1363", + "bottle-soda-outline": "\U000F1072", + "bottle-tonic": "\U000F112E", + "bottle-tonic-outline": "\U000F112F", + "bottle-tonic-plus": "\U000F1130", + "bottle-tonic-plus-outline": "\U000F1131", + "bottle-tonic-skull": "\U000F1132", + "bottle-tonic-skull-outline": "\U000F1133", + "bottle-wine": "\U000F0854", + "bottle-wine-outline": "\U000F1310", + "bow-tie": "\U000F0678", + "bowl": "\U000F028E", + "bowl-mix": "\U000F0617", + "bowl-mix-outline": "\U000F02E4", + "bowl-outline": "\U000F02A9", + "bowling": "\U000F00D3", + "box": "\U000F00D4", + "box-cutter": "\U000F00D5", + "box-cutter-off": "\U000F0B4A", + "box-shadow": "\U000F0637", + "boxing-glove": "\U000F0B65", + "braille": "\U000F09D0", + "brain": "\U000F09D1", + "bread-slice": "\U000F0CEE", + "bread-slice-outline": "\U000F0CEF", + "bridge": "\U000F0618", + "briefcase": "\U000F00D6", + "briefcase-account": "\U000F0CF0", + "briefcase-account-outline": "\U000F0CF1", + "briefcase-check": "\U000F00D7", + "briefcase-check-outline": "\U000F131E", + "briefcase-clock": "\U000F10D0", + "briefcase-clock-outline": "\U000F10D1", + "briefcase-download": "\U000F00D8", + "briefcase-download-outline": "\U000F0C3D", + "briefcase-edit": "\U000F0A98", + "briefcase-edit-outline": "\U000F0C3E", + "briefcase-minus": "\U000F0A2A", + "briefcase-minus-outline": "\U000F0C3F", + "briefcase-outline": "\U000F0814", + "briefcase-plus": "\U000F0A2B", + "briefcase-plus-outline": "\U000F0C40", + "briefcase-remove": "\U000F0A2C", + "briefcase-remove-outline": "\U000F0C41", + "briefcase-search": "\U000F0A2D", + "briefcase-search-outline": "\U000F0C42", + "briefcase-upload": "\U000F00D9", + "briefcase-upload-outline": "\U000F0C43", + "briefcase-variant": "\U000F1494", + "briefcase-variant-outline": "\U000F1495", + "brightness-1": "\U000F00DA", + "brightness-2": "\U000F00DB", + "brightness-3": "\U000F00DC", + "brightness-4": "\U000F00DD", + "brightness-5": "\U000F00DE", + "brightness-6": "\U000F00DF", + "brightness-7": "\U000F00E0", + "brightness-auto": "\U000F00E1", + "brightness-percent": "\U000F0CF2", + "broom": "\U000F00E2", + "brush": "\U000F00E3", + "bucket": "\U000F1415", + "bucket-outline": "\U000F1416", + "buddhism": "\U000F094B", + "buffer": "\U000F0619", + "buffet": "\U000F0578", + "bug": "\U000F00E4", + "bug-check": "\U000F0A2E", + "bug-check-outline": "\U000F0A2F", + "bug-outline": "\U000F0A30", + "bugle": "\U000F0DB4", + "bulldozer": "\U000F0B22", + "bullet": "\U000F0CF3", + "bulletin-board": "\U000F00E5", + "bullhorn": "\U000F00E6", + "bullhorn-outline": "\U000F0B23", + "bullseye": "\U000F05DD", + "bullseye-arrow": "\U000F08C9", + "bulma": "\U000F12E7", + "bunk-bed": "\U000F1302", + "bunk-bed-outline": "\U000F0097", + "bus": "\U000F00E7", + "bus-alert": "\U000F0A99", + "bus-articulated-end": "\U000F079C", + "bus-articulated-front": "\U000F079D", + "bus-clock": "\U000F08CA", + "bus-double-decker": "\U000F079E", + "bus-marker": "\U000F1212", + "bus-multiple": "\U000F0F3F", + "bus-school": "\U000F079F", + "bus-side": "\U000F07A0", + "bus-stop": "\U000F1012", + "bus-stop-covered": "\U000F1013", + "bus-stop-uncovered": "\U000F1014", + "butterfly": "\U000F1589", + "butterfly-outline": "\U000F158A", + "cable-data": "\U000F1394", + "cached": "\U000F00E8", + "cactus": "\U000F0DB5", + "cake": "\U000F00E9", + "cake-layered": "\U000F00EA", + "cake-variant": "\U000F00EB", + "calculator": "\U000F00EC", + "calculator-variant": "\U000F0A9A", + "calculator-variant-outline": "\U000F15A6", + "calendar": "\U000F00ED", + "calendar-account": "\U000F0ED7", + "calendar-account-outline": "\U000F0ED8", + "calendar-alert": "\U000F0A31", + "calendar-arrow-left": "\U000F1134", + "calendar-arrow-right": "\U000F1135", + "calendar-blank": "\U000F00EE", + "calendar-blank-multiple": "\U000F1073", + "calendar-blank-outline": "\U000F0B66", + "calendar-check": "\U000F00EF", + "calendar-check-outline": "\U000F0C44", + "calendar-clock": "\U000F00F0", + "calendar-cursor": "\U000F157B", + "calendar-edit": "\U000F08A7", + "calendar-export": "\U000F0B24", + "calendar-heart": "\U000F09D2", + "calendar-import": "\U000F0B25", + "calendar-minus": "\U000F0D5C", + "calendar-month": "\U000F0E17", + "calendar-month-outline": "\U000F0E18", + "calendar-multiple": "\U000F00F1", + "calendar-multiple-check": "\U000F00F2", + "calendar-multiselect": "\U000F0A32", + "calendar-outline": "\U000F0B67", + "calendar-plus": "\U000F00F3", + "calendar-question": "\U000F0692", + "calendar-range": "\U000F0679", + "calendar-range-outline": "\U000F0B68", + "calendar-refresh": "\U000F01E1", + "calendar-refresh-outline": "\U000F0203", + "calendar-remove": "\U000F00F4", + "calendar-remove-outline": "\U000F0C45", + "calendar-search": "\U000F094C", + "calendar-star": "\U000F09D3", + "calendar-sync": "\U000F0E8E", + "calendar-sync-outline": "\U000F0E8F", + "calendar-text": "\U000F00F5", + "calendar-text-outline": "\U000F0C46", + "calendar-today": "\U000F00F6", + "calendar-week": "\U000F0A33", + "calendar-week-begin": "\U000F0A34", + "calendar-weekend": "\U000F0ED9", + "calendar-weekend-outline": "\U000F0EDA", + "call-made": "\U000F00F7", + "call-merge": "\U000F00F8", + "call-missed": "\U000F00F9", + "call-received": "\U000F00FA", + "call-split": "\U000F00FB", + "camcorder": "\U000F00FC", + "camcorder-off": "\U000F00FF", + "camera": "\U000F0100", + "camera-account": "\U000F08CB", + "camera-burst": "\U000F0693", + "camera-control": "\U000F0B69", + "camera-enhance": "\U000F0101", + "camera-enhance-outline": "\U000F0B6A", + "camera-flip": "\U000F15D9", + "camera-flip-outline": "\U000F15DA", + "camera-front": "\U000F0102", + "camera-front-variant": "\U000F0103", + "camera-gopro": "\U000F07A1", + "camera-image": "\U000F08CC", + "camera-iris": "\U000F0104", + "camera-metering-center": "\U000F07A2", + "camera-metering-matrix": "\U000F07A3", + "camera-metering-partial": "\U000F07A4", + "camera-metering-spot": "\U000F07A5", + "camera-off": "\U000F05DF", + "camera-outline": "\U000F0D5D", + "camera-party-mode": "\U000F0105", + "camera-plus": "\U000F0EDB", + "camera-plus-outline": "\U000F0EDC", + "camera-rear": "\U000F0106", + "camera-rear-variant": "\U000F0107", + "camera-retake": "\U000F0E19", + "camera-retake-outline": "\U000F0E1A", + "camera-switch": "\U000F0108", + "camera-switch-outline": "\U000F084A", + "camera-timer": "\U000F0109", + "camera-wireless": "\U000F0DB6", + "camera-wireless-outline": "\U000F0DB7", + "campfire": "\U000F0EDD", + "cancel": "\U000F073A", + "candle": "\U000F05E2", + "candycane": "\U000F010A", + "cannabis": "\U000F07A6", + "caps-lock": "\U000F0A9B", + "car": "\U000F010B", + "car-2-plus": "\U000F1015", + "car-3-plus": "\U000F1016", + "car-arrow-left": "\U000F13B2", + "car-arrow-right": "\U000F13B3", + "car-back": "\U000F0E1B", + "car-battery": "\U000F010C", + "car-brake-abs": "\U000F0C47", + "car-brake-alert": "\U000F0C48", + "car-brake-hold": "\U000F0D5E", + "car-brake-parking": "\U000F0D5F", + "car-brake-retarder": "\U000F1017", + "car-child-seat": "\U000F0FA3", + "car-clutch": "\U000F1018", + "car-cog": "\U000F13CC", + "car-connected": "\U000F010D", + "car-convertible": "\U000F07A7", + "car-coolant-level": "\U000F1019", + "car-cruise-control": "\U000F0D60", + "car-defrost-front": "\U000F0D61", + "car-defrost-rear": "\U000F0D62", + "car-door": "\U000F0B6B", + "car-door-lock": "\U000F109D", + "car-electric": "\U000F0B6C", + "car-electric-outline": "\U000F15B5", + "car-emergency": "\U000F160F", + "car-esp": "\U000F0C49", + "car-estate": "\U000F07A8", + "car-hatchback": "\U000F07A9", + "car-info": "\U000F11BE", + "car-key": "\U000F0B6D", + "car-lifted-pickup": "\U000F152D", + "car-light-dimmed": "\U000F0C4A", + "car-light-fog": "\U000F0C4B", + "car-light-high": "\U000F0C4C", + "car-limousine": "\U000F08CD", + "car-multiple": "\U000F0B6E", + "car-off": "\U000F0E1C", + "car-outline": "\U000F14ED", + "car-parking-lights": "\U000F0D63", + "car-pickup": "\U000F07AA", + "car-seat": "\U000F0FA4", + "car-seat-cooler": "\U000F0FA5", + "car-seat-heater": "\U000F0FA6", + "car-settings": "\U000F13CD", + "car-shift-pattern": "\U000F0F40", + "car-side": "\U000F07AB", + "car-sports": "\U000F07AC", + "car-tire-alert": "\U000F0C4D", + "car-traction-control": "\U000F0D64", + "car-turbocharger": "\U000F101A", + "car-wash": "\U000F010E", + "car-windshield": "\U000F101B", + "car-windshield-outline": "\U000F101C", + "carabiner": "\U000F14C0", + "caravan": "\U000F07AD", + "card": "\U000F0B6F", + "card-account-details": "\U000F05D2", + "card-account-details-outline": "\U000F0DAB", + "card-account-details-star": "\U000F02A3", + "card-account-details-star-outline": "\U000F06DB", + "card-account-mail": "\U000F018E", + "card-account-mail-outline": "\U000F0E98", + "card-account-phone": "\U000F0E99", + "card-account-phone-outline": "\U000F0E9A", + "card-bulleted": "\U000F0B70", + "card-bulleted-off": "\U000F0B71", + "card-bulleted-off-outline": "\U000F0B72", + "card-bulleted-outline": "\U000F0B73", + "card-bulleted-settings": "\U000F0B74", + "card-bulleted-settings-outline": "\U000F0B75", + "card-minus": "\U000F1600", + "card-minus-outline": "\U000F1601", + "card-off": "\U000F1602", + "card-off-outline": "\U000F1603", + "card-outline": "\U000F0B76", + "card-plus": "\U000F11FF", + "card-plus-outline": "\U000F1200", + "card-remove": "\U000F1604", + "card-remove-outline": "\U000F1605", + "card-search": "\U000F1074", + "card-search-outline": "\U000F1075", + "card-text": "\U000F0B77", + "card-text-outline": "\U000F0B78", + "cards": "\U000F0638", + "cards-club": "\U000F08CE", + "cards-diamond": "\U000F08CF", + "cards-diamond-outline": "\U000F101D", + "cards-heart": "\U000F08D0", + "cards-outline": "\U000F0639", + "cards-playing-outline": "\U000F063A", + "cards-spade": "\U000F08D1", + "cards-variant": "\U000F06C7", + "carrot": "\U000F010F", + "cart": "\U000F0110", + "cart-arrow-down": "\U000F0D66", + "cart-arrow-right": "\U000F0C4E", + "cart-arrow-up": "\U000F0D67", + "cart-check": "\U000F15EA", + "cart-minus": "\U000F0D68", + "cart-off": "\U000F066B", + "cart-outline": "\U000F0111", + "cart-plus": "\U000F0112", + "cart-remove": "\U000F0D69", + "cart-variant": "\U000F15EB", + "case-sensitive-alt": "\U000F0113", + "cash": "\U000F0114", + "cash-100": "\U000F0115", + "cash-check": "\U000F14EE", + "cash-lock": "\U000F14EA", + "cash-lock-open": "\U000F14EB", + "cash-marker": "\U000F0DB8", + "cash-minus": "\U000F1260", + "cash-multiple": "\U000F0116", + "cash-plus": "\U000F1261", + "cash-refund": "\U000F0A9C", + "cash-register": "\U000F0CF4", + "cash-remove": "\U000F1262", + "cash-usd": "\U000F1176", + "cash-usd-outline": "\U000F0117", + "cassette": "\U000F09D4", + "cast": "\U000F0118", + "cast-audio": "\U000F101E", + "cast-connected": "\U000F0119", + "cast-education": "\U000F0E1D", + "cast-off": "\U000F078A", + "castle": "\U000F011A", + "cat": "\U000F011B", + "cctv": "\U000F07AE", + "ceiling-light": "\U000F0769", + "cellphone": "\U000F011C", + "cellphone-android": "\U000F011D", + "cellphone-arrow-down": "\U000F09D5", + "cellphone-basic": "\U000F011E", + "cellphone-charging": "\U000F1397", + "cellphone-cog": "\U000F0951", + "cellphone-dock": "\U000F011F", + "cellphone-erase": "\U000F094D", + "cellphone-information": "\U000F0F41", + "cellphone-iphone": "\U000F0120", + "cellphone-key": "\U000F094E", + "cellphone-link": "\U000F0121", + "cellphone-link-off": "\U000F0122", + "cellphone-lock": "\U000F094F", + "cellphone-message": "\U000F08D3", + "cellphone-message-off": "\U000F10D2", + "cellphone-nfc": "\U000F0E90", + "cellphone-nfc-off": "\U000F12D8", + "cellphone-off": "\U000F0950", + "cellphone-play": "\U000F101F", + "cellphone-screenshot": "\U000F0A35", + "cellphone-settings": "\U000F0123", + "cellphone-sound": "\U000F0952", + "cellphone-text": "\U000F08D2", + "cellphone-wireless": "\U000F0815", + "celtic-cross": "\U000F0CF5", + "centos": "\U000F111A", + "certificate": "\U000F0124", + "certificate-outline": "\U000F1188", + "chair-rolling": "\U000F0F48", + "chair-school": "\U000F0125", + "charity": "\U000F0C4F", + "chart-arc": "\U000F0126", + "chart-areaspline": "\U000F0127", + "chart-areaspline-variant": "\U000F0E91", + "chart-bar": "\U000F0128", + "chart-bar-stacked": "\U000F076A", + "chart-bell-curve": "\U000F0C50", + "chart-bell-curve-cumulative": "\U000F0FA7", + "chart-box": "\U000F154D", + "chart-box-outline": "\U000F154E", + "chart-box-plus-outline": "\U000F154F", + "chart-bubble": "\U000F05E3", + "chart-donut": "\U000F07AF", + "chart-donut-variant": "\U000F07B0", + "chart-gantt": "\U000F066C", + "chart-histogram": "\U000F0129", + "chart-line": "\U000F012A", + "chart-line-stacked": "\U000F076B", + "chart-line-variant": "\U000F07B1", + "chart-multiline": "\U000F08D4", + "chart-multiple": "\U000F1213", + "chart-pie": "\U000F012B", + "chart-ppf": "\U000F1380", + "chart-sankey": "\U000F11DF", + "chart-sankey-variant": "\U000F11E0", + "chart-scatter-plot": "\U000F0E92", + "chart-scatter-plot-hexbin": "\U000F066D", + "chart-timeline": "\U000F066E", + "chart-timeline-variant": "\U000F0E93", + "chart-timeline-variant-shimmer": "\U000F15B6", + "chart-tree": "\U000F0E94", + "chat": "\U000F0B79", + "chat-alert": "\U000F0B7A", + "chat-alert-outline": "\U000F12C9", + "chat-minus": "\U000F1410", + "chat-minus-outline": "\U000F1413", + "chat-outline": "\U000F0EDE", + "chat-plus": "\U000F140F", + "chat-plus-outline": "\U000F1412", + "chat-processing": "\U000F0B7B", + "chat-processing-outline": "\U000F12CA", + "chat-remove": "\U000F1411", + "chat-remove-outline": "\U000F1414", + "chat-sleep": "\U000F12D1", + "chat-sleep-outline": "\U000F12D2", + "check": "\U000F012C", + "check-all": "\U000F012D", + "check-bold": "\U000F0E1E", + "check-box-multiple-outline": "\U000F0C51", + "check-box-outline": "\U000F0C52", + "check-circle": "\U000F05E0", + "check-circle-outline": "\U000F05E1", + "check-decagram": "\U000F0791", + "check-network": "\U000F0C53", + "check-network-outline": "\U000F0C54", + "check-outline": "\U000F0855", + "check-underline": "\U000F0E1F", + "check-underline-circle": "\U000F0E20", + "check-underline-circle-outline": "\U000F0E21", + "checkbook": "\U000F0A9D", + "checkbox-blank": "\U000F012E", + "checkbox-blank-circle": "\U000F012F", + "checkbox-blank-circle-outline": "\U000F0130", + "checkbox-blank-off": "\U000F12EC", + "checkbox-blank-off-outline": "\U000F12ED", + "checkbox-blank-outline": "\U000F0131", + "checkbox-intermediate": "\U000F0856", + "checkbox-marked": "\U000F0132", + "checkbox-marked-circle": "\U000F0133", + "checkbox-marked-circle-outline": "\U000F0134", + "checkbox-marked-outline": "\U000F0135", + "checkbox-multiple-blank": "\U000F0136", + "checkbox-multiple-blank-circle": "\U000F063B", + "checkbox-multiple-blank-circle-outline": "\U000F063C", + "checkbox-multiple-blank-outline": "\U000F0137", + "checkbox-multiple-marked": "\U000F0138", + "checkbox-multiple-marked-circle": "\U000F063D", + "checkbox-multiple-marked-circle-outline": "\U000F063E", + "checkbox-multiple-marked-outline": "\U000F0139", + "checkerboard": "\U000F013A", + "checkerboard-minus": "\U000F1202", + "checkerboard-plus": "\U000F1201", + "checkerboard-remove": "\U000F1203", + "cheese": "\U000F12B9", + "cheese-off": "\U000F13EE", + "chef-hat": "\U000F0B7C", + "chemical-weapon": "\U000F013B", + "chess-bishop": "\U000F085C", + "chess-king": "\U000F0857", + "chess-knight": "\U000F0858", + "chess-pawn": "\U000F0859", + "chess-queen": "\U000F085A", + "chess-rook": "\U000F085B", + "chevron-double-down": "\U000F013C", + "chevron-double-left": "\U000F013D", + "chevron-double-right": "\U000F013E", + "chevron-double-up": "\U000F013F", + "chevron-down": "\U000F0140", + "chevron-down-box": "\U000F09D6", + "chevron-down-box-outline": "\U000F09D7", + "chevron-down-circle": "\U000F0B26", + "chevron-down-circle-outline": "\U000F0B27", + "chevron-left": "\U000F0141", + "chevron-left-box": "\U000F09D8", + "chevron-left-box-outline": "\U000F09D9", + "chevron-left-circle": "\U000F0B28", + "chevron-left-circle-outline": "\U000F0B29", + "chevron-right": "\U000F0142", + "chevron-right-box": "\U000F09DA", + "chevron-right-box-outline": "\U000F09DB", + "chevron-right-circle": "\U000F0B2A", + "chevron-right-circle-outline": "\U000F0B2B", + "chevron-triple-down": "\U000F0DB9", + "chevron-triple-left": "\U000F0DBA", + "chevron-triple-right": "\U000F0DBB", + "chevron-triple-up": "\U000F0DBC", + "chevron-up": "\U000F0143", + "chevron-up-box": "\U000F09DC", + "chevron-up-box-outline": "\U000F09DD", + "chevron-up-circle": "\U000F0B2C", + "chevron-up-circle-outline": "\U000F0B2D", + "chili-hot": "\U000F07B2", + "chili-medium": "\U000F07B3", + "chili-mild": "\U000F07B4", + "chili-off": "\U000F1467", + "chip": "\U000F061A", + "christianity": "\U000F0953", + "christianity-outline": "\U000F0CF6", + "church": "\U000F0144", + "cigar": "\U000F1189", + "cigar-off": "\U000F141B", + "circle": "\U000F0765", + "circle-box": "\U000F15DC", + "circle-box-outline": "\U000F15DD", + "circle-double": "\U000F0E95", + "circle-edit-outline": "\U000F08D5", + "circle-expand": "\U000F0E96", + "circle-half": "\U000F1395", + "circle-half-full": "\U000F1396", + "circle-medium": "\U000F09DE", + "circle-multiple": "\U000F0B38", + "circle-multiple-outline": "\U000F0695", + "circle-off-outline": "\U000F10D3", + "circle-outline": "\U000F0766", + "circle-slice-1": "\U000F0A9E", + "circle-slice-2": "\U000F0A9F", + "circle-slice-3": "\U000F0AA0", + "circle-slice-4": "\U000F0AA1", + "circle-slice-5": "\U000F0AA2", + "circle-slice-6": "\U000F0AA3", + "circle-slice-7": "\U000F0AA4", + "circle-slice-8": "\U000F0AA5", + "circle-small": "\U000F09DF", + "circular-saw": "\U000F0E22", + "city": "\U000F0146", + "city-variant": "\U000F0A36", + "city-variant-outline": "\U000F0A37", + "clipboard": "\U000F0147", + "clipboard-account": "\U000F0148", + "clipboard-account-outline": "\U000F0C55", + "clipboard-alert": "\U000F0149", + "clipboard-alert-outline": "\U000F0CF7", + "clipboard-arrow-down": "\U000F014A", + "clipboard-arrow-down-outline": "\U000F0C56", + "clipboard-arrow-left": "\U000F014B", + "clipboard-arrow-left-outline": "\U000F0CF8", + "clipboard-arrow-right": "\U000F0CF9", + "clipboard-arrow-right-outline": "\U000F0CFA", + "clipboard-arrow-up": "\U000F0C57", + "clipboard-arrow-up-outline": "\U000F0C58", + "clipboard-check": "\U000F014E", + "clipboard-check-multiple": "\U000F1263", + "clipboard-check-multiple-outline": "\U000F1264", + "clipboard-check-outline": "\U000F08A8", + "clipboard-edit": "\U000F14E5", + "clipboard-edit-outline": "\U000F14E6", + "clipboard-file": "\U000F1265", + "clipboard-file-outline": "\U000F1266", + "clipboard-flow": "\U000F06C8", + "clipboard-flow-outline": "\U000F1117", + "clipboard-list": "\U000F10D4", + "clipboard-list-outline": "\U000F10D5", + "clipboard-multiple": "\U000F1267", + "clipboard-multiple-outline": "\U000F1268", + "clipboard-outline": "\U000F014C", + "clipboard-play": "\U000F0C59", + "clipboard-play-multiple": "\U000F1269", + "clipboard-play-multiple-outline": "\U000F126A", + "clipboard-play-outline": "\U000F0C5A", + "clipboard-plus": "\U000F0751", + "clipboard-plus-outline": "\U000F131F", + "clipboard-pulse": "\U000F085D", + "clipboard-pulse-outline": "\U000F085E", + "clipboard-text": "\U000F014D", + "clipboard-text-multiple": "\U000F126B", + "clipboard-text-multiple-outline": "\U000F126C", + "clipboard-text-outline": "\U000F0A38", + "clipboard-text-play": "\U000F0C5B", + "clipboard-text-play-outline": "\U000F0C5C", + "clippy": "\U000F014F", + "clock": "\U000F0954", + "clock-alert": "\U000F0955", + "clock-alert-outline": "\U000F05CE", + "clock-check": "\U000F0FA8", + "clock-check-outline": "\U000F0FA9", + "clock-digital": "\U000F0E97", + "clock-end": "\U000F0151", + "clock-fast": "\U000F0152", + "clock-in": "\U000F0153", + "clock-out": "\U000F0154", + "clock-outline": "\U000F0150", + "clock-start": "\U000F0155", + "clock-time-eight": "\U000F1446", + "clock-time-eight-outline": "\U000F1452", + "clock-time-eleven": "\U000F1449", + "clock-time-eleven-outline": "\U000F1455", + "clock-time-five": "\U000F1443", + "clock-time-five-outline": "\U000F144F", + "clock-time-four": "\U000F1442", + "clock-time-four-outline": "\U000F144E", + "clock-time-nine": "\U000F1447", + "clock-time-nine-outline": "\U000F1453", + "clock-time-one": "\U000F143F", + "clock-time-one-outline": "\U000F144B", + "clock-time-seven": "\U000F1445", + "clock-time-seven-outline": "\U000F1451", + "clock-time-six": "\U000F1444", + "clock-time-six-outline": "\U000F1450", + "clock-time-ten": "\U000F1448", + "clock-time-ten-outline": "\U000F1454", + "clock-time-three": "\U000F1441", + "clock-time-three-outline": "\U000F144D", + "clock-time-twelve": "\U000F144A", + "clock-time-twelve-outline": "\U000F1456", + "clock-time-two": "\U000F1440", + "clock-time-two-outline": "\U000F144C", + "close": "\U000F0156", + "close-box": "\U000F0157", + "close-box-multiple": "\U000F0C5D", + "close-box-multiple-outline": "\U000F0C5E", + "close-box-outline": "\U000F0158", + "close-circle": "\U000F0159", + "close-circle-multiple": "\U000F062A", + "close-circle-multiple-outline": "\U000F0883", + "close-circle-outline": "\U000F015A", + "close-network": "\U000F015B", + "close-network-outline": "\U000F0C5F", + "close-octagon": "\U000F015C", + "close-octagon-outline": "\U000F015D", + "close-outline": "\U000F06C9", + "close-thick": "\U000F1398", + "closed-caption": "\U000F015E", + "closed-caption-outline": "\U000F0DBD", + "cloud": "\U000F015F", + "cloud-alert": "\U000F09E0", + "cloud-braces": "\U000F07B5", + "cloud-check": "\U000F0160", + "cloud-check-outline": "\U000F12CC", + "cloud-circle": "\U000F0161", + "cloud-download": "\U000F0162", + "cloud-download-outline": "\U000F0B7D", + "cloud-lock": "\U000F11F1", + "cloud-lock-outline": "\U000F11F2", + "cloud-off-outline": "\U000F0164", + "cloud-outline": "\U000F0163", + "cloud-print": "\U000F0165", + "cloud-print-outline": "\U000F0166", + "cloud-question": "\U000F0A39", + "cloud-refresh": "\U000F052A", + "cloud-search": "\U000F0956", + "cloud-search-outline": "\U000F0957", + "cloud-sync": "\U000F063F", + "cloud-sync-outline": "\U000F12D6", + "cloud-tags": "\U000F07B6", + "cloud-upload": "\U000F0167", + "cloud-upload-outline": "\U000F0B7E", + "clover": "\U000F0816", + "coach-lamp": "\U000F1020", + "coat-rack": "\U000F109E", + "code-array": "\U000F0168", + "code-braces": "\U000F0169", + "code-braces-box": "\U000F10D6", + "code-brackets": "\U000F016A", + "code-equal": "\U000F016B", + "code-greater-than": "\U000F016C", + "code-greater-than-or-equal": "\U000F016D", + "code-json": "\U000F0626", + "code-less-than": "\U000F016E", + "code-less-than-or-equal": "\U000F016F", + "code-not-equal": "\U000F0170", + "code-not-equal-variant": "\U000F0171", + "code-parentheses": "\U000F0172", + "code-parentheses-box": "\U000F10D7", + "code-string": "\U000F0173", + "code-tags": "\U000F0174", + "code-tags-check": "\U000F0694", + "codepen": "\U000F0175", + "coffee": "\U000F0176", + "coffee-maker": "\U000F109F", + "coffee-off": "\U000F0FAA", + "coffee-off-outline": "\U000F0FAB", + "coffee-outline": "\U000F06CA", + "coffee-to-go": "\U000F0177", + "coffee-to-go-outline": "\U000F130E", + "coffin": "\U000F0B7F", + "cog": "\U000F0493", + "cog-box": "\U000F0494", + "cog-clockwise": "\U000F11DD", + "cog-counterclockwise": "\U000F11DE", + "cog-off": "\U000F13CE", + "cog-off-outline": "\U000F13CF", + "cog-outline": "\U000F08BB", + "cog-refresh": "\U000F145E", + "cog-refresh-outline": "\U000F145F", + "cog-sync": "\U000F1460", + "cog-sync-outline": "\U000F1461", + "cog-transfer": "\U000F105B", + "cog-transfer-outline": "\U000F105C", + "cogs": "\U000F08D6", + "collage": "\U000F0640", + "collapse-all": "\U000F0AA6", + "collapse-all-outline": "\U000F0AA7", + "color-helper": "\U000F0179", + "comma": "\U000F0E23", + "comma-box": "\U000F0E2B", + "comma-box-outline": "\U000F0E24", + "comma-circle": "\U000F0E25", + "comma-circle-outline": "\U000F0E26", + "comment": "\U000F017A", + "comment-account": "\U000F017B", + "comment-account-outline": "\U000F017C", + "comment-alert": "\U000F017D", + "comment-alert-outline": "\U000F017E", + "comment-arrow-left": "\U000F09E1", + "comment-arrow-left-outline": "\U000F09E2", + "comment-arrow-right": "\U000F09E3", + "comment-arrow-right-outline": "\U000F09E4", + "comment-bookmark": "\U000F15AE", + "comment-bookmark-outline": "\U000F15AF", + "comment-check": "\U000F017F", + "comment-check-outline": "\U000F0180", + "comment-edit": "\U000F11BF", + "comment-edit-outline": "\U000F12C4", + "comment-eye": "\U000F0A3A", + "comment-eye-outline": "\U000F0A3B", + "comment-flash": "\U000F15B0", + "comment-flash-outline": "\U000F15B1", + "comment-minus": "\U000F15DF", + "comment-minus-outline": "\U000F15E0", + "comment-multiple": "\U000F085F", + "comment-multiple-outline": "\U000F0181", + "comment-off": "\U000F15E1", + "comment-off-outline": "\U000F15E2", + "comment-outline": "\U000F0182", + "comment-plus": "\U000F09E5", + "comment-plus-outline": "\U000F0183", + "comment-processing": "\U000F0184", + "comment-processing-outline": "\U000F0185", + "comment-question": "\U000F0817", + "comment-question-outline": "\U000F0186", + "comment-quote": "\U000F1021", + "comment-quote-outline": "\U000F1022", + "comment-remove": "\U000F05DE", + "comment-remove-outline": "\U000F0187", + "comment-search": "\U000F0A3C", + "comment-search-outline": "\U000F0A3D", + "comment-text": "\U000F0188", + "comment-text-multiple": "\U000F0860", + "comment-text-multiple-outline": "\U000F0861", + "comment-text-outline": "\U000F0189", + "compare": "\U000F018A", + "compare-horizontal": "\U000F1492", + "compare-vertical": "\U000F1493", + "compass": "\U000F018B", + "compass-off": "\U000F0B80", + "compass-off-outline": "\U000F0B81", + "compass-outline": "\U000F018C", + "compass-rose": "\U000F1382", + "concourse-ci": "\U000F10A0", + "connection": "\U000F1616", + "console": "\U000F018D", + "console-line": "\U000F07B7", + "console-network": "\U000F08A9", + "console-network-outline": "\U000F0C60", + "consolidate": "\U000F10D8", + "contactless-payment": "\U000F0D6A", + "contactless-payment-circle": "\U000F0321", + "contactless-payment-circle-outline": "\U000F0408", + "contacts": "\U000F06CB", + "contacts-outline": "\U000F05B8", + "contain": "\U000F0A3E", + "contain-end": "\U000F0A3F", + "contain-start": "\U000F0A40", + "content-copy": "\U000F018F", + "content-cut": "\U000F0190", + "content-duplicate": "\U000F0191", + "content-paste": "\U000F0192", + "content-save": "\U000F0193", + "content-save-alert": "\U000F0F42", + "content-save-alert-outline": "\U000F0F43", + "content-save-all": "\U000F0194", + "content-save-all-outline": "\U000F0F44", + "content-save-cog": "\U000F145B", + "content-save-cog-outline": "\U000F145C", + "content-save-edit": "\U000F0CFB", + "content-save-edit-outline": "\U000F0CFC", + "content-save-move": "\U000F0E27", + "content-save-move-outline": "\U000F0E28", + "content-save-outline": "\U000F0818", + "content-save-settings": "\U000F061B", + "content-save-settings-outline": "\U000F0B2E", + "contrast": "\U000F0195", + "contrast-box": "\U000F0196", + "contrast-circle": "\U000F0197", + "controller-classic": "\U000F0B82", + "controller-classic-outline": "\U000F0B83", + "cookie": "\U000F0198", + "coolant-temperature": "\U000F03C8", + "copyright": "\U000F05E6", + "cordova": "\U000F0958", + "corn": "\U000F07B8", + "corn-off": "\U000F13EF", + "cosine-wave": "\U000F1479", + "counter": "\U000F0199", + "cow": "\U000F019A", + "cpu-32-bit": "\U000F0EDF", + "cpu-64-bit": "\U000F0EE0", + "crane": "\U000F0862", + "creation": "\U000F0674", + "creative-commons": "\U000F0D6B", + "credit-card": "\U000F0FEF", + "credit-card-check": "\U000F13D0", + "credit-card-check-outline": "\U000F13D1", + "credit-card-clock": "\U000F0EE1", + "credit-card-clock-outline": "\U000F0EE2", + "credit-card-marker": "\U000F06A8", + "credit-card-marker-outline": "\U000F0DBE", + "credit-card-minus": "\U000F0FAC", + "credit-card-minus-outline": "\U000F0FAD", + "credit-card-multiple": "\U000F0FF0", + "credit-card-multiple-outline": "\U000F019C", + "credit-card-off": "\U000F0FF1", + "credit-card-off-outline": "\U000F05E4", + "credit-card-outline": "\U000F019B", + "credit-card-plus": "\U000F0FF2", + "credit-card-plus-outline": "\U000F0676", + "credit-card-refund": "\U000F0FF3", + "credit-card-refund-outline": "\U000F0AA8", + "credit-card-remove": "\U000F0FAE", + "credit-card-remove-outline": "\U000F0FAF", + "credit-card-scan": "\U000F0FF4", + "credit-card-scan-outline": "\U000F019D", + "credit-card-settings": "\U000F0FF5", + "credit-card-settings-outline": "\U000F08D7", + "credit-card-wireless": "\U000F0802", + "credit-card-wireless-off": "\U000F057A", + "credit-card-wireless-off-outline": "\U000F057B", + "credit-card-wireless-outline": "\U000F0D6C", + "cricket": "\U000F0D6D", + "crop": "\U000F019E", + "crop-free": "\U000F019F", + "crop-landscape": "\U000F01A0", + "crop-portrait": "\U000F01A1", + "crop-rotate": "\U000F0696", + "crop-square": "\U000F01A2", + "crosshairs": "\U000F01A3", + "crosshairs-gps": "\U000F01A4", + "crosshairs-off": "\U000F0F45", + "crosshairs-question": "\U000F1136", + "crown": "\U000F01A5", + "crown-outline": "\U000F11D0", + "cryengine": "\U000F0959", + "crystal-ball": "\U000F0B2F", + "cube": "\U000F01A6", + "cube-off": "\U000F141C", + "cube-off-outline": "\U000F141D", + "cube-outline": "\U000F01A7", + "cube-scan": "\U000F0B84", + "cube-send": "\U000F01A8", + "cube-unfolded": "\U000F01A9", + "cup": "\U000F01AA", + "cup-off": "\U000F05E5", + "cup-off-outline": "\U000F137D", + "cup-outline": "\U000F130F", + "cup-water": "\U000F01AB", + "cupboard": "\U000F0F46", + "cupboard-outline": "\U000F0F47", + "cupcake": "\U000F095A", + "curling": "\U000F0863", + "currency-bdt": "\U000F0864", + "currency-brl": "\U000F0B85", + "currency-btc": "\U000F01AC", + "currency-cny": "\U000F07BA", + "currency-eth": "\U000F07BB", + "currency-eur": "\U000F01AD", + "currency-eur-off": "\U000F1315", + "currency-gbp": "\U000F01AE", + "currency-ils": "\U000F0C61", + "currency-inr": "\U000F01AF", + "currency-jpy": "\U000F07BC", + "currency-krw": "\U000F07BD", + "currency-kzt": "\U000F0865", + "currency-mnt": "\U000F1512", + "currency-ngn": "\U000F01B0", + "currency-php": "\U000F09E6", + "currency-rial": "\U000F0E9C", + "currency-rub": "\U000F01B1", + "currency-sign": "\U000F07BE", + "currency-try": "\U000F01B2", + "currency-twd": "\U000F07BF", + "currency-usd": "\U000F01C1", + "currency-usd-circle": "\U000F116B", + "currency-usd-circle-outline": "\U000F0178", + "currency-usd-off": "\U000F067A", + "current-ac": "\U000F1480", + "current-dc": "\U000F095C", + "cursor-default": "\U000F01C0", + "cursor-default-click": "\U000F0CFD", + "cursor-default-click-outline": "\U000F0CFE", + "cursor-default-gesture": "\U000F1127", + "cursor-default-gesture-outline": "\U000F1128", + "cursor-default-outline": "\U000F01BF", + "cursor-move": "\U000F01BE", + "cursor-pointer": "\U000F01BD", + "cursor-text": "\U000F05E7", + "dance-ballroom": "\U000F15FB", + "dance-pole": "\U000F1578", + "data-matrix": "\U000F153C", + "data-matrix-edit": "\U000F153D", + "data-matrix-minus": "\U000F153E", + "data-matrix-plus": "\U000F153F", + "data-matrix-remove": "\U000F1540", + "data-matrix-scan": "\U000F1541", + "database": "\U000F01BC", + "database-check": "\U000F0AA9", + "database-edit": "\U000F0B86", + "database-export": "\U000F095E", + "database-import": "\U000F095D", + "database-lock": "\U000F0AAA", + "database-marker": "\U000F12F6", + "database-minus": "\U000F01BB", + "database-plus": "\U000F01BA", + "database-refresh": "\U000F05C2", + "database-remove": "\U000F0D00", + "database-search": "\U000F0866", + "database-settings": "\U000F0D01", + "database-sync": "\U000F0CFF", + "death-star": "\U000F08D8", + "death-star-variant": "\U000F08D9", + "deathly-hallows": "\U000F0B87", + "debian": "\U000F08DA", + "debug-step-into": "\U000F01B9", + "debug-step-out": "\U000F01B8", + "debug-step-over": "\U000F01B7", + "decagram": "\U000F076C", + "decagram-outline": "\U000F076D", + "decimal": "\U000F10A1", + "decimal-comma": "\U000F10A2", + "decimal-comma-decrease": "\U000F10A3", + "decimal-comma-increase": "\U000F10A4", + "decimal-decrease": "\U000F01B6", + "decimal-increase": "\U000F01B5", + "delete": "\U000F01B4", + "delete-alert": "\U000F10A5", + "delete-alert-outline": "\U000F10A6", + "delete-circle": "\U000F0683", + "delete-circle-outline": "\U000F0B88", + "delete-clock": "\U000F1556", + "delete-clock-outline": "\U000F1557", + "delete-empty": "\U000F06CC", + "delete-empty-outline": "\U000F0E9D", + "delete-forever": "\U000F05E8", + "delete-forever-outline": "\U000F0B89", + "delete-off": "\U000F10A7", + "delete-off-outline": "\U000F10A8", + "delete-outline": "\U000F09E7", + "delete-restore": "\U000F0819", + "delete-sweep": "\U000F05E9", + "delete-sweep-outline": "\U000F0C62", + "delete-variant": "\U000F01B3", + "delta": "\U000F01C2", + "desk": "\U000F1239", + "desk-lamp": "\U000F095F", + "deskphone": "\U000F01C3", + "desktop-classic": "\U000F07C0", + "desktop-mac": "\U000F01C4", + "desktop-mac-dashboard": "\U000F09E8", + "desktop-tower": "\U000F01C5", + "desktop-tower-monitor": "\U000F0AAB", + "details": "\U000F01C6", + "dev-to": "\U000F0D6E", + "developer-board": "\U000F0697", + "deviantart": "\U000F01C7", + "devices": "\U000F0FB0", + "diabetes": "\U000F1126", + "dialpad": "\U000F061C", + "diameter": "\U000F0C63", + "diameter-outline": "\U000F0C64", + "diameter-variant": "\U000F0C65", + "diamond": "\U000F0B8A", + "diamond-outline": "\U000F0B8B", + "diamond-stone": "\U000F01C8", + "dice-1": "\U000F01CA", + "dice-1-outline": "\U000F114A", + "dice-2": "\U000F01CB", + "dice-2-outline": "\U000F114B", + "dice-3": "\U000F01CC", + "dice-3-outline": "\U000F114C", + "dice-4": "\U000F01CD", + "dice-4-outline": "\U000F114D", + "dice-5": "\U000F01CE", + "dice-5-outline": "\U000F114E", + "dice-6": "\U000F01CF", + "dice-6-outline": "\U000F114F", + "dice-d10": "\U000F1153", + "dice-d10-outline": "\U000F076F", + "dice-d12": "\U000F1154", + "dice-d12-outline": "\U000F0867", + "dice-d20": "\U000F1155", + "dice-d20-outline": "\U000F05EA", + "dice-d4": "\U000F1150", + "dice-d4-outline": "\U000F05EB", + "dice-d6": "\U000F1151", + "dice-d6-outline": "\U000F05ED", + "dice-d8": "\U000F1152", + "dice-d8-outline": "\U000F05EC", + "dice-multiple": "\U000F076E", + "dice-multiple-outline": "\U000F1156", + "digital-ocean": "\U000F1237", + "dip-switch": "\U000F07C1", + "directions": "\U000F01D0", + "directions-fork": "\U000F0641", + "disc": "\U000F05EE", + "disc-alert": "\U000F01D1", + "disc-player": "\U000F0960", + "discord": "\U000F066F", + "dishwasher": "\U000F0AAC", + "dishwasher-alert": "\U000F11B8", + "dishwasher-off": "\U000F11B9", + "disqus": "\U000F01D2", + "distribute-horizontal-center": "\U000F11C9", + "distribute-horizontal-left": "\U000F11C8", + "distribute-horizontal-right": "\U000F11CA", + "distribute-vertical-bottom": "\U000F11CB", + "distribute-vertical-center": "\U000F11CC", + "distribute-vertical-top": "\U000F11CD", + "diving-flippers": "\U000F0DBF", + "diving-helmet": "\U000F0DC0", + "diving-scuba": "\U000F0DC1", + "diving-scuba-flag": "\U000F0DC2", + "diving-scuba-tank": "\U000F0DC3", + "diving-scuba-tank-multiple": "\U000F0DC4", + "diving-snorkel": "\U000F0DC5", + "division": "\U000F01D4", + "division-box": "\U000F01D5", + "dlna": "\U000F0A41", + "dna": "\U000F0684", + "dns": "\U000F01D6", + "dns-outline": "\U000F0B8C", + "do-not-disturb": "\U000F0698", + "do-not-disturb-off": "\U000F0699", + "dock-bottom": "\U000F10A9", + "dock-left": "\U000F10AA", + "dock-right": "\U000F10AB", + "dock-top": "\U000F1513", + "dock-window": "\U000F10AC", + "docker": "\U000F0868", + "doctor": "\U000F0A42", + "dog": "\U000F0A43", + "dog-service": "\U000F0AAD", + "dog-side": "\U000F0A44", + "dolby": "\U000F06B3", + "dolly": "\U000F0E9E", + "domain": "\U000F01D7", + "domain-off": "\U000F0D6F", + "domain-plus": "\U000F10AD", + "domain-remove": "\U000F10AE", + "dome-light": "\U000F141E", + "domino-mask": "\U000F1023", + "donkey": "\U000F07C2", + "door": "\U000F081A", + "door-closed": "\U000F081B", + "door-closed-lock": "\U000F10AF", + "door-open": "\U000F081C", + "doorbell": "\U000F12E6", + "doorbell-video": "\U000F0869", + "dot-net": "\U000F0AAE", + "dots-grid": "\U000F15FC", + "dots-hexagon": "\U000F15FF", + "dots-horizontal": "\U000F01D8", + "dots-horizontal-circle": "\U000F07C3", + "dots-horizontal-circle-outline": "\U000F0B8D", + "dots-square": "\U000F15FD", + "dots-triangle": "\U000F15FE", + "dots-vertical": "\U000F01D9", + "dots-vertical-circle": "\U000F07C4", + "dots-vertical-circle-outline": "\U000F0B8E", + "douban": "\U000F069A", + "download": "\U000F01DA", + "download-box": "\U000F1462", + "download-box-outline": "\U000F1463", + "download-circle": "\U000F1464", + "download-circle-outline": "\U000F1465", + "download-lock": "\U000F1320", + "download-lock-outline": "\U000F1321", + "download-multiple": "\U000F09E9", + "download-network": "\U000F06F4", + "download-network-outline": "\U000F0C66", + "download-off": "\U000F10B0", + "download-off-outline": "\U000F10B1", + "download-outline": "\U000F0B8F", + "drag": "\U000F01DB", + "drag-horizontal": "\U000F01DC", + "drag-horizontal-variant": "\U000F12F0", + "drag-variant": "\U000F0B90", + "drag-vertical": "\U000F01DD", + "drag-vertical-variant": "\U000F12F1", + "drama-masks": "\U000F0D02", + "draw": "\U000F0F49", + "drawing": "\U000F01DE", + "drawing-box": "\U000F01DF", + "dresser": "\U000F0F4A", + "dresser-outline": "\U000F0F4B", + "drone": "\U000F01E2", + "dropbox": "\U000F01E3", + "drupal": "\U000F01E4", + "duck": "\U000F01E5", + "dumbbell": "\U000F01E6", + "dump-truck": "\U000F0C67", + "ear-hearing": "\U000F07C5", + "ear-hearing-off": "\U000F0A45", + "earth": "\U000F01E7", + "earth-arrow-right": "\U000F1311", + "earth-box": "\U000F06CD", + "earth-box-minus": "\U000F1407", + "earth-box-off": "\U000F06CE", + "earth-box-plus": "\U000F1406", + "earth-box-remove": "\U000F1408", + "earth-minus": "\U000F1404", + "earth-off": "\U000F01E8", + "earth-plus": "\U000F1403", + "earth-remove": "\U000F1405", + "egg": "\U000F0AAF", + "egg-easter": "\U000F0AB0", + "egg-off": "\U000F13F0", + "egg-off-outline": "\U000F13F1", + "egg-outline": "\U000F13F2", + "eiffel-tower": "\U000F156B", + "eight-track": "\U000F09EA", + "eject": "\U000F01EA", + "eject-outline": "\U000F0B91", + "electric-switch": "\U000F0E9F", + "electric-switch-closed": "\U000F10D9", + "electron-framework": "\U000F1024", + "elephant": "\U000F07C6", + "elevation-decline": "\U000F01EB", + "elevation-rise": "\U000F01EC", + "elevator": "\U000F01ED", + "elevator-down": "\U000F12C2", + "elevator-passenger": "\U000F1381", + "elevator-up": "\U000F12C1", + "ellipse": "\U000F0EA0", + "ellipse-outline": "\U000F0EA1", + "email": "\U000F01EE", + "email-alert": "\U000F06CF", + "email-alert-outline": "\U000F0D42", + "email-box": "\U000F0D03", + "email-check": "\U000F0AB1", + "email-check-outline": "\U000F0AB2", + "email-edit": "\U000F0EE3", + "email-edit-outline": "\U000F0EE4", + "email-lock": "\U000F01F1", + "email-mark-as-unread": "\U000F0B92", + "email-minus": "\U000F0EE5", + "email-minus-outline": "\U000F0EE6", + "email-multiple": "\U000F0EE7", + "email-multiple-outline": "\U000F0EE8", + "email-newsletter": "\U000F0FB1", + "email-off": "\U000F13E3", + "email-off-outline": "\U000F13E4", + "email-open": "\U000F01EF", + "email-open-multiple": "\U000F0EE9", + "email-open-multiple-outline": "\U000F0EEA", + "email-open-outline": "\U000F05EF", + "email-outline": "\U000F01F0", + "email-plus": "\U000F09EB", + "email-plus-outline": "\U000F09EC", + "email-receive": "\U000F10DA", + "email-receive-outline": "\U000F10DB", + "email-search": "\U000F0961", + "email-search-outline": "\U000F0962", + "email-send": "\U000F10DC", + "email-send-outline": "\U000F10DD", + "email-sync": "\U000F12C7", + "email-sync-outline": "\U000F12C8", + "email-variant": "\U000F05F0", + "ember": "\U000F0B30", + "emby": "\U000F06B4", + "emoticon": "\U000F0C68", + "emoticon-angry": "\U000F0C69", + "emoticon-angry-outline": "\U000F0C6A", + "emoticon-confused": "\U000F10DE", + "emoticon-confused-outline": "\U000F10DF", + "emoticon-cool": "\U000F0C6B", + "emoticon-cool-outline": "\U000F01F3", + "emoticon-cry": "\U000F0C6C", + "emoticon-cry-outline": "\U000F0C6D", + "emoticon-dead": "\U000F0C6E", + "emoticon-dead-outline": "\U000F069B", + "emoticon-devil": "\U000F0C6F", + "emoticon-devil-outline": "\U000F01F4", + "emoticon-excited": "\U000F0C70", + "emoticon-excited-outline": "\U000F069C", + "emoticon-frown": "\U000F0F4C", + "emoticon-frown-outline": "\U000F0F4D", + "emoticon-happy": "\U000F0C71", + "emoticon-happy-outline": "\U000F01F5", + "emoticon-kiss": "\U000F0C72", + "emoticon-kiss-outline": "\U000F0C73", + "emoticon-lol": "\U000F1214", + "emoticon-lol-outline": "\U000F1215", + "emoticon-neutral": "\U000F0C74", + "emoticon-neutral-outline": "\U000F01F6", + "emoticon-outline": "\U000F01F2", + "emoticon-poop": "\U000F01F7", + "emoticon-poop-outline": "\U000F0C75", + "emoticon-sad": "\U000F0C76", + "emoticon-sad-outline": "\U000F01F8", + "emoticon-sick": "\U000F157C", + "emoticon-sick-outline": "\U000F157D", + "emoticon-tongue": "\U000F01F9", + "emoticon-tongue-outline": "\U000F0C77", + "emoticon-wink": "\U000F0C78", + "emoticon-wink-outline": "\U000F0C79", + "engine": "\U000F01FA", + "engine-off": "\U000F0A46", + "engine-off-outline": "\U000F0A47", + "engine-outline": "\U000F01FB", + "epsilon": "\U000F10E0", + "equal": "\U000F01FC", + "equal-box": "\U000F01FD", + "equalizer": "\U000F0EA2", + "equalizer-outline": "\U000F0EA3", + "eraser": "\U000F01FE", + "eraser-variant": "\U000F0642", + "escalator": "\U000F01FF", + "escalator-box": "\U000F1399", + "escalator-down": "\U000F12C0", + "escalator-up": "\U000F12BF", + "eslint": "\U000F0C7A", + "et": "\U000F0AB3", + "ethereum": "\U000F086A", + "ethernet": "\U000F0200", + "ethernet-cable": "\U000F0201", + "ethernet-cable-off": "\U000F0202", + "ev-plug-ccs1": "\U000F1519", + "ev-plug-ccs2": "\U000F151A", + "ev-plug-chademo": "\U000F151B", + "ev-plug-tesla": "\U000F151C", + "ev-plug-type1": "\U000F151D", + "ev-plug-type2": "\U000F151E", + "ev-station": "\U000F05F1", + "evernote": "\U000F0204", + "excavator": "\U000F1025", + "exclamation": "\U000F0205", + "exclamation-thick": "\U000F1238", + "exit-run": "\U000F0A48", + "exit-to-app": "\U000F0206", + "expand-all": "\U000F0AB4", + "expand-all-outline": "\U000F0AB5", + "expansion-card": "\U000F08AE", + "expansion-card-variant": "\U000F0FB2", + "exponent": "\U000F0963", + "exponent-box": "\U000F0964", + "export": "\U000F0207", + "export-variant": "\U000F0B93", + "eye": "\U000F0208", + "eye-check": "\U000F0D04", + "eye-check-outline": "\U000F0D05", + "eye-circle": "\U000F0B94", + "eye-circle-outline": "\U000F0B95", + "eye-minus": "\U000F1026", + "eye-minus-outline": "\U000F1027", + "eye-off": "\U000F0209", + "eye-off-outline": "\U000F06D1", + "eye-outline": "\U000F06D0", + "eye-plus": "\U000F086B", + "eye-plus-outline": "\U000F086C", + "eye-remove": "\U000F15E3", + "eye-remove-outline": "\U000F15E4", + "eye-settings": "\U000F086D", + "eye-settings-outline": "\U000F086E", + "eyedropper": "\U000F020A", + "eyedropper-minus": "\U000F13DD", + "eyedropper-off": "\U000F13DF", + "eyedropper-plus": "\U000F13DC", + "eyedropper-remove": "\U000F13DE", + "eyedropper-variant": "\U000F020B", + "face": "\U000F0643", + "face-agent": "\U000F0D70", + "face-mask": "\U000F1586", + "face-mask-outline": "\U000F1587", + "face-outline": "\U000F0B96", + "face-profile": "\U000F0644", + "face-profile-woman": "\U000F1076", + "face-recognition": "\U000F0C7B", + "face-shimmer": "\U000F15CC", + "face-shimmer-outline": "\U000F15CD", + "face-woman": "\U000F1077", + "face-woman-outline": "\U000F1078", + "face-woman-shimmer": "\U000F15CE", + "face-woman-shimmer-outline": "\U000F15CF", + "facebook": "\U000F020C", + "facebook-gaming": "\U000F07DD", + "facebook-messenger": "\U000F020E", + "facebook-workplace": "\U000F0B31", + "factory": "\U000F020F", + "family-tree": "\U000F160E", + "fan": "\U000F0210", + "fan-alert": "\U000F146C", + "fan-chevron-down": "\U000F146D", + "fan-chevron-up": "\U000F146E", + "fan-minus": "\U000F1470", + "fan-off": "\U000F081D", + "fan-plus": "\U000F146F", + "fan-remove": "\U000F1471", + "fan-speed-1": "\U000F1472", + "fan-speed-2": "\U000F1473", + "fan-speed-3": "\U000F1474", + "fast-forward": "\U000F0211", + "fast-forward-10": "\U000F0D71", + "fast-forward-30": "\U000F0D06", + "fast-forward-5": "\U000F11F8", + "fast-forward-60": "\U000F160B", + "fast-forward-outline": "\U000F06D2", + "fax": "\U000F0212", + "feather": "\U000F06D3", + "feature-search": "\U000F0A49", + "feature-search-outline": "\U000F0A4A", + "fedora": "\U000F08DB", + "fencing": "\U000F14C1", + "ferris-wheel": "\U000F0EA4", + "ferry": "\U000F0213", + "file": "\U000F0214", + "file-account": "\U000F073B", + "file-account-outline": "\U000F1028", + "file-alert": "\U000F0A4B", + "file-alert-outline": "\U000F0A4C", + "file-cabinet": "\U000F0AB6", + "file-cad": "\U000F0EEB", + "file-cad-box": "\U000F0EEC", + "file-cancel": "\U000F0DC6", + "file-cancel-outline": "\U000F0DC7", + "file-certificate": "\U000F1186", + "file-certificate-outline": "\U000F1187", + "file-chart": "\U000F0215", + "file-chart-outline": "\U000F1029", + "file-check": "\U000F0216", + "file-check-outline": "\U000F0E29", + "file-clock": "\U000F12E1", + "file-clock-outline": "\U000F12E2", + "file-cloud": "\U000F0217", + "file-cloud-outline": "\U000F102A", + "file-code": "\U000F022E", + "file-code-outline": "\U000F102B", + "file-cog": "\U000F107B", + "file-cog-outline": "\U000F107C", + "file-compare": "\U000F08AA", + "file-delimited": "\U000F0218", + "file-delimited-outline": "\U000F0EA5", + "file-document": "\U000F0219", + "file-document-edit": "\U000F0DC8", + "file-document-edit-outline": "\U000F0DC9", + "file-document-multiple": "\U000F1517", + "file-document-multiple-outline": "\U000F1518", + "file-document-outline": "\U000F09EE", + "file-download": "\U000F0965", + "file-download-outline": "\U000F0966", + "file-edit": "\U000F11E7", + "file-edit-outline": "\U000F11E8", + "file-excel": "\U000F021B", + "file-excel-box": "\U000F021C", + "file-excel-box-outline": "\U000F102C", + "file-excel-outline": "\U000F102D", + "file-export": "\U000F021D", + "file-export-outline": "\U000F102E", + "file-eye": "\U000F0DCA", + "file-eye-outline": "\U000F0DCB", + "file-find": "\U000F021E", + "file-find-outline": "\U000F0B97", + "file-hidden": "\U000F0613", + "file-image": "\U000F021F", + "file-image-outline": "\U000F0EB0", + "file-import": "\U000F0220", + "file-import-outline": "\U000F102F", + "file-key": "\U000F1184", + "file-key-outline": "\U000F1185", + "file-link": "\U000F1177", + "file-link-outline": "\U000F1178", + "file-lock": "\U000F0221", + "file-lock-outline": "\U000F1030", + "file-move": "\U000F0AB9", + "file-move-outline": "\U000F1031", + "file-multiple": "\U000F0222", + "file-multiple-outline": "\U000F1032", + "file-music": "\U000F0223", + "file-music-outline": "\U000F0E2A", + "file-outline": "\U000F0224", + "file-pdf": "\U000F0225", + "file-pdf-box": "\U000F0226", + "file-pdf-box-outline": "\U000F0FB3", + "file-pdf-outline": "\U000F0E2D", + "file-percent": "\U000F081E", + "file-percent-outline": "\U000F1033", + "file-phone": "\U000F1179", + "file-phone-outline": "\U000F117A", + "file-plus": "\U000F0752", + "file-plus-outline": "\U000F0EED", + "file-powerpoint": "\U000F0227", + "file-powerpoint-box": "\U000F0228", + "file-powerpoint-box-outline": "\U000F1034", + "file-powerpoint-outline": "\U000F1035", + "file-presentation-box": "\U000F0229", + "file-question": "\U000F086F", + "file-question-outline": "\U000F1036", + "file-refresh": "\U000F0918", + "file-refresh-outline": "\U000F0541", + "file-remove": "\U000F0B98", + "file-remove-outline": "\U000F1037", + "file-replace": "\U000F0B32", + "file-replace-outline": "\U000F0B33", + "file-restore": "\U000F0670", + "file-restore-outline": "\U000F1038", + "file-search": "\U000F0C7C", + "file-search-outline": "\U000F0C7D", + "file-send": "\U000F022A", + "file-send-outline": "\U000F1039", + "file-settings": "\U000F1079", + "file-settings-outline": "\U000F107A", + "file-star": "\U000F103A", + "file-star-outline": "\U000F103B", + "file-swap": "\U000F0FB4", + "file-swap-outline": "\U000F0FB5", + "file-sync": "\U000F1216", + "file-sync-outline": "\U000F1217", + "file-table": "\U000F0C7E", + "file-table-box": "\U000F10E1", + "file-table-box-multiple": "\U000F10E2", + "file-table-box-multiple-outline": "\U000F10E3", + "file-table-box-outline": "\U000F10E4", + "file-table-outline": "\U000F0C7F", + "file-tree": "\U000F0645", + "file-tree-outline": "\U000F13D2", + "file-undo": "\U000F08DC", + "file-undo-outline": "\U000F103C", + "file-upload": "\U000F0A4D", + "file-upload-outline": "\U000F0A4E", + "file-video": "\U000F022B", + "file-video-outline": "\U000F0E2C", + "file-word": "\U000F022C", + "file-word-box": "\U000F022D", + "file-word-box-outline": "\U000F103D", + "file-word-outline": "\U000F103E", + "film": "\U000F022F", + "filmstrip": "\U000F0230", + "filmstrip-box": "\U000F0332", + "filmstrip-box-multiple": "\U000F0D18", + "filmstrip-off": "\U000F0231", + "filter": "\U000F0232", + "filter-menu": "\U000F10E5", + "filter-menu-outline": "\U000F10E6", + "filter-minus": "\U000F0EEE", + "filter-minus-outline": "\U000F0EEF", + "filter-off": "\U000F14EF", + "filter-off-outline": "\U000F14F0", + "filter-outline": "\U000F0233", + "filter-plus": "\U000F0EF0", + "filter-plus-outline": "\U000F0EF1", + "filter-remove": "\U000F0234", + "filter-remove-outline": "\U000F0235", + "filter-variant": "\U000F0236", + "filter-variant-minus": "\U000F1112", + "filter-variant-plus": "\U000F1113", + "filter-variant-remove": "\U000F103F", + "finance": "\U000F081F", + "find-replace": "\U000F06D4", + "fingerprint": "\U000F0237", + "fingerprint-off": "\U000F0EB1", + "fire": "\U000F0238", + "fire-alert": "\U000F15D7", + "fire-extinguisher": "\U000F0EF2", + "fire-hydrant": "\U000F1137", + "fire-hydrant-alert": "\U000F1138", + "fire-hydrant-off": "\U000F1139", + "fire-truck": "\U000F08AB", + "firebase": "\U000F0967", + "firefox": "\U000F0239", + "fireplace": "\U000F0E2E", + "fireplace-off": "\U000F0E2F", + "firework": "\U000F0E30", + "fish": "\U000F023A", + "fish-off": "\U000F13F3", + "fishbowl": "\U000F0EF3", + "fishbowl-outline": "\U000F0EF4", + "fit-to-page": "\U000F0EF5", + "fit-to-page-outline": "\U000F0EF6", + "flag": "\U000F023B", + "flag-checkered": "\U000F023C", + "flag-minus": "\U000F0B99", + "flag-minus-outline": "\U000F10B2", + "flag-outline": "\U000F023D", + "flag-plus": "\U000F0B9A", + "flag-plus-outline": "\U000F10B3", + "flag-remove": "\U000F0B9B", + "flag-remove-outline": "\U000F10B4", + "flag-triangle": "\U000F023F", + "flag-variant": "\U000F0240", + "flag-variant-outline": "\U000F023E", + "flare": "\U000F0D72", + "flash": "\U000F0241", + "flash-alert": "\U000F0EF7", + "flash-alert-outline": "\U000F0EF8", + "flash-auto": "\U000F0242", + "flash-circle": "\U000F0820", + "flash-off": "\U000F0243", + "flash-outline": "\U000F06D5", + "flash-red-eye": "\U000F067B", + "flashlight": "\U000F0244", + "flashlight-off": "\U000F0245", + "flask": "\U000F0093", + "flask-empty": "\U000F0094", + "flask-empty-minus": "\U000F123A", + "flask-empty-minus-outline": "\U000F123B", + "flask-empty-off": "\U000F13F4", + "flask-empty-off-outline": "\U000F13F5", + "flask-empty-outline": "\U000F0095", + "flask-empty-plus": "\U000F123C", + "flask-empty-plus-outline": "\U000F123D", + "flask-empty-remove": "\U000F123E", + "flask-empty-remove-outline": "\U000F123F", + "flask-minus": "\U000F1240", + "flask-minus-outline": "\U000F1241", + "flask-off": "\U000F13F6", + "flask-off-outline": "\U000F13F7", + "flask-outline": "\U000F0096", + "flask-plus": "\U000F1242", + "flask-plus-outline": "\U000F1243", + "flask-remove": "\U000F1244", + "flask-remove-outline": "\U000F1245", + "flask-round-bottom": "\U000F124B", + "flask-round-bottom-empty": "\U000F124C", + "flask-round-bottom-empty-outline": "\U000F124D", + "flask-round-bottom-outline": "\U000F124E", + "fleur-de-lis": "\U000F1303", + "flip-horizontal": "\U000F10E7", + "flip-to-back": "\U000F0247", + "flip-to-front": "\U000F0248", + "flip-vertical": "\U000F10E8", + "floor-lamp": "\U000F08DD", + "floor-lamp-dual": "\U000F1040", + "floor-lamp-variant": "\U000F1041", + "floor-plan": "\U000F0821", + "floppy": "\U000F0249", + "floppy-variant": "\U000F09EF", + "flower": "\U000F024A", + "flower-outline": "\U000F09F0", + "flower-poppy": "\U000F0D08", + "flower-tulip": "\U000F09F1", + "flower-tulip-outline": "\U000F09F2", + "focus-auto": "\U000F0F4E", + "focus-field": "\U000F0F4F", + "focus-field-horizontal": "\U000F0F50", + "focus-field-vertical": "\U000F0F51", + "folder": "\U000F024B", + "folder-account": "\U000F024C", + "folder-account-outline": "\U000F0B9C", + "folder-alert": "\U000F0DCC", + "folder-alert-outline": "\U000F0DCD", + "folder-clock": "\U000F0ABA", + "folder-clock-outline": "\U000F0ABB", + "folder-cog": "\U000F107F", + "folder-cog-outline": "\U000F1080", + "folder-download": "\U000F024D", + "folder-download-outline": "\U000F10E9", + "folder-edit": "\U000F08DE", + "folder-edit-outline": "\U000F0DCE", + "folder-google-drive": "\U000F024E", + "folder-heart": "\U000F10EA", + "folder-heart-outline": "\U000F10EB", + "folder-home": "\U000F10B5", + "folder-home-outline": "\U000F10B6", + "folder-image": "\U000F024F", + "folder-information": "\U000F10B7", + "folder-information-outline": "\U000F10B8", + "folder-key": "\U000F08AC", + "folder-key-network": "\U000F08AD", + "folder-key-network-outline": "\U000F0C80", + "folder-key-outline": "\U000F10EC", + "folder-lock": "\U000F0250", + "folder-lock-open": "\U000F0251", + "folder-marker": "\U000F126D", + "folder-marker-outline": "\U000F126E", + "folder-move": "\U000F0252", + "folder-move-outline": "\U000F1246", + "folder-multiple": "\U000F0253", + "folder-multiple-image": "\U000F0254", + "folder-multiple-outline": "\U000F0255", + "folder-multiple-plus": "\U000F147E", + "folder-multiple-plus-outline": "\U000F147F", + "folder-music": "\U000F1359", + "folder-music-outline": "\U000F135A", + "folder-network": "\U000F0870", + "folder-network-outline": "\U000F0C81", + "folder-open": "\U000F0770", + "folder-open-outline": "\U000F0DCF", + "folder-outline": "\U000F0256", + "folder-plus": "\U000F0257", + "folder-plus-outline": "\U000F0B9D", + "folder-pound": "\U000F0D09", + "folder-pound-outline": "\U000F0D0A", + "folder-refresh": "\U000F0749", + "folder-refresh-outline": "\U000F0542", + "folder-remove": "\U000F0258", + "folder-remove-outline": "\U000F0B9E", + "folder-search": "\U000F0968", + "folder-search-outline": "\U000F0969", + "folder-settings": "\U000F107D", + "folder-settings-outline": "\U000F107E", + "folder-star": "\U000F069D", + "folder-star-multiple": "\U000F13D3", + "folder-star-multiple-outline": "\U000F13D4", + "folder-star-outline": "\U000F0B9F", + "folder-swap": "\U000F0FB6", + "folder-swap-outline": "\U000F0FB7", + "folder-sync": "\U000F0D0B", + "folder-sync-outline": "\U000F0D0C", + "folder-table": "\U000F12E3", + "folder-table-outline": "\U000F12E4", + "folder-text": "\U000F0C82", + "folder-text-outline": "\U000F0C83", + "folder-upload": "\U000F0259", + "folder-upload-outline": "\U000F10ED", + "folder-zip": "\U000F06EB", + "folder-zip-outline": "\U000F07B9", + "font-awesome": "\U000F003A", + "food": "\U000F025A", + "food-apple": "\U000F025B", + "food-apple-outline": "\U000F0C84", + "food-croissant": "\U000F07C8", + "food-drumstick": "\U000F141F", + "food-drumstick-off": "\U000F1468", + "food-drumstick-off-outline": "\U000F1469", + "food-drumstick-outline": "\U000F1420", + "food-fork-drink": "\U000F05F2", + "food-halal": "\U000F1572", + "food-kosher": "\U000F1573", + "food-off": "\U000F05F3", + "food-steak": "\U000F146A", + "food-steak-off": "\U000F146B", + "food-variant": "\U000F025C", + "food-variant-off": "\U000F13E5", + "foot-print": "\U000F0F52", + "football": "\U000F025D", + "football-australian": "\U000F025E", + "football-helmet": "\U000F025F", + "forklift": "\U000F07C9", + "form-dropdown": "\U000F1400", + "form-select": "\U000F1401", + "form-textarea": "\U000F1095", + "form-textbox": "\U000F060E", + "form-textbox-lock": "\U000F135D", + "form-textbox-password": "\U000F07F5", + "format-align-bottom": "\U000F0753", + "format-align-center": "\U000F0260", + "format-align-justify": "\U000F0261", + "format-align-left": "\U000F0262", + "format-align-middle": "\U000F0754", + "format-align-right": "\U000F0263", + "format-align-top": "\U000F0755", + "format-annotation-minus": "\U000F0ABC", + "format-annotation-plus": "\U000F0646", + "format-bold": "\U000F0264", + "format-clear": "\U000F0265", + "format-color-fill": "\U000F0266", + "format-color-highlight": "\U000F0E31", + "format-color-marker-cancel": "\U000F1313", + "format-color-text": "\U000F069E", + "format-columns": "\U000F08DF", + "format-float-center": "\U000F0267", + "format-float-left": "\U000F0268", + "format-float-none": "\U000F0269", + "format-float-right": "\U000F026A", + "format-font": "\U000F06D6", + "format-font-size-decrease": "\U000F09F3", + "format-font-size-increase": "\U000F09F4", + "format-header-1": "\U000F026B", + "format-header-2": "\U000F026C", + "format-header-3": "\U000F026D", + "format-header-4": "\U000F026E", + "format-header-5": "\U000F026F", + "format-header-6": "\U000F0270", + "format-header-decrease": "\U000F0271", + "format-header-equal": "\U000F0272", + "format-header-increase": "\U000F0273", + "format-header-pound": "\U000F0274", + "format-horizontal-align-center": "\U000F061E", + "format-horizontal-align-left": "\U000F061F", + "format-horizontal-align-right": "\U000F0620", + "format-indent-decrease": "\U000F0275", + "format-indent-increase": "\U000F0276", + "format-italic": "\U000F0277", + "format-letter-case": "\U000F0B34", + "format-letter-case-lower": "\U000F0B35", + "format-letter-case-upper": "\U000F0B36", + "format-letter-ends-with": "\U000F0FB8", + "format-letter-matches": "\U000F0FB9", + "format-letter-starts-with": "\U000F0FBA", + "format-line-spacing": "\U000F0278", + "format-line-style": "\U000F05C8", + "format-line-weight": "\U000F05C9", + "format-list-bulleted": "\U000F0279", + "format-list-bulleted-square": "\U000F0DD0", + "format-list-bulleted-triangle": "\U000F0EB2", + "format-list-bulleted-type": "\U000F027A", + "format-list-checkbox": "\U000F096A", + "format-list-checks": "\U000F0756", + "format-list-numbered": "\U000F027B", + "format-list-numbered-rtl": "\U000F0D0D", + "format-list-text": "\U000F126F", + "format-overline": "\U000F0EB3", + "format-page-break": "\U000F06D7", + "format-paint": "\U000F027C", + "format-paragraph": "\U000F027D", + "format-pilcrow": "\U000F06D8", + "format-quote-close": "\U000F027E", + "format-quote-close-outline": "\U000F11A8", + "format-quote-open": "\U000F0757", + "format-quote-open-outline": "\U000F11A7", + "format-rotate-90": "\U000F06AA", + "format-section": "\U000F069F", + "format-size": "\U000F027F", + "format-strikethrough": "\U000F0280", + "format-strikethrough-variant": "\U000F0281", + "format-subscript": "\U000F0282", + "format-superscript": "\U000F0283", + "format-text": "\U000F0284", + "format-text-rotation-angle-down": "\U000F0FBB", + "format-text-rotation-angle-up": "\U000F0FBC", + "format-text-rotation-down": "\U000F0D73", + "format-text-rotation-down-vertical": "\U000F0FBD", + "format-text-rotation-none": "\U000F0D74", + "format-text-rotation-up": "\U000F0FBE", + "format-text-rotation-vertical": "\U000F0FBF", + "format-text-variant": "\U000F0E32", + "format-text-variant-outline": "\U000F150F", + "format-text-wrapping-clip": "\U000F0D0E", + "format-text-wrapping-overflow": "\U000F0D0F", + "format-text-wrapping-wrap": "\U000F0D10", + "format-textbox": "\U000F0D11", + "format-textdirection-l-to-r": "\U000F0285", + "format-textdirection-r-to-l": "\U000F0286", + "format-title": "\U000F05F4", + "format-underline": "\U000F0287", + "format-vertical-align-bottom": "\U000F0621", + "format-vertical-align-center": "\U000F0622", + "format-vertical-align-top": "\U000F0623", + "format-wrap-inline": "\U000F0288", + "format-wrap-square": "\U000F0289", + "format-wrap-tight": "\U000F028A", + "format-wrap-top-bottom": "\U000F028B", + "forum": "\U000F028C", + "forum-outline": "\U000F0822", + "forward": "\U000F028D", + "forwardburger": "\U000F0D75", + "fountain": "\U000F096B", + "fountain-pen": "\U000F0D12", + "fountain-pen-tip": "\U000F0D13", + "freebsd": "\U000F08E0", + "frequently-asked-questions": "\U000F0EB4", + "fridge": "\U000F0290", + "fridge-alert": "\U000F11B1", + "fridge-alert-outline": "\U000F11B2", + "fridge-bottom": "\U000F0292", + "fridge-industrial": "\U000F15EE", + "fridge-industrial-alert": "\U000F15EF", + "fridge-industrial-alert-outline": "\U000F15F0", + "fridge-industrial-off": "\U000F15F1", + "fridge-industrial-off-outline": "\U000F15F2", + "fridge-industrial-outline": "\U000F15F3", + "fridge-off": "\U000F11AF", + "fridge-off-outline": "\U000F11B0", + "fridge-outline": "\U000F028F", + "fridge-top": "\U000F0291", + "fridge-variant": "\U000F15F4", + "fridge-variant-alert": "\U000F15F5", + "fridge-variant-alert-outline": "\U000F15F6", + "fridge-variant-off": "\U000F15F7", + "fridge-variant-off-outline": "\U000F15F8", + "fridge-variant-outline": "\U000F15F9", + "fruit-cherries": "\U000F1042", + "fruit-cherries-off": "\U000F13F8", + "fruit-citrus": "\U000F1043", + "fruit-citrus-off": "\U000F13F9", + "fruit-grapes": "\U000F1044", + "fruit-grapes-outline": "\U000F1045", + "fruit-pineapple": "\U000F1046", + "fruit-watermelon": "\U000F1047", + "fuel": "\U000F07CA", + "fullscreen": "\U000F0293", + "fullscreen-exit": "\U000F0294", + "function": "\U000F0295", + "function-variant": "\U000F0871", + "furigana-horizontal": "\U000F1081", + "furigana-vertical": "\U000F1082", + "fuse": "\U000F0C85", + "fuse-alert": "\U000F142D", + "fuse-blade": "\U000F0C86", + "fuse-off": "\U000F142C", + "gamepad": "\U000F0296", + "gamepad-circle": "\U000F0E33", + "gamepad-circle-down": "\U000F0E34", + "gamepad-circle-left": "\U000F0E35", + "gamepad-circle-outline": "\U000F0E36", + "gamepad-circle-right": "\U000F0E37", + "gamepad-circle-up": "\U000F0E38", + "gamepad-down": "\U000F0E39", + "gamepad-left": "\U000F0E3A", + "gamepad-right": "\U000F0E3B", + "gamepad-round": "\U000F0E3C", + "gamepad-round-down": "\U000F0E3D", + "gamepad-round-left": "\U000F0E3E", + "gamepad-round-outline": "\U000F0E3F", + "gamepad-round-right": "\U000F0E40", + "gamepad-round-up": "\U000F0E41", + "gamepad-square": "\U000F0EB5", + "gamepad-square-outline": "\U000F0EB6", + "gamepad-up": "\U000F0E42", + "gamepad-variant": "\U000F0297", + "gamepad-variant-outline": "\U000F0EB7", + "gamma": "\U000F10EE", + "gantry-crane": "\U000F0DD1", + "garage": "\U000F06D9", + "garage-alert": "\U000F0872", + "garage-alert-variant": "\U000F12D5", + "garage-open": "\U000F06DA", + "garage-open-variant": "\U000F12D4", + "garage-variant": "\U000F12D3", + "gas-cylinder": "\U000F0647", + "gas-station": "\U000F0298", + "gas-station-off": "\U000F1409", + "gas-station-off-outline": "\U000F140A", + "gas-station-outline": "\U000F0EB8", + "gate": "\U000F0299", + "gate-and": "\U000F08E1", + "gate-arrow-right": "\U000F1169", + "gate-nand": "\U000F08E2", + "gate-nor": "\U000F08E3", + "gate-not": "\U000F08E4", + "gate-open": "\U000F116A", + "gate-or": "\U000F08E5", + "gate-xnor": "\U000F08E6", + "gate-xor": "\U000F08E7", + "gatsby": "\U000F0E43", + "gauge": "\U000F029A", + "gauge-empty": "\U000F0873", + "gauge-full": "\U000F0874", + "gauge-low": "\U000F0875", + "gavel": "\U000F029B", + "gender-female": "\U000F029C", + "gender-male": "\U000F029D", + "gender-male-female": "\U000F029E", + "gender-male-female-variant": "\U000F113F", + "gender-non-binary": "\U000F1140", + "gender-transgender": "\U000F029F", + "gentoo": "\U000F08E8", + "gesture": "\U000F07CB", + "gesture-double-tap": "\U000F073C", + "gesture-pinch": "\U000F0ABD", + "gesture-spread": "\U000F0ABE", + "gesture-swipe": "\U000F0D76", + "gesture-swipe-down": "\U000F073D", + "gesture-swipe-horizontal": "\U000F0ABF", + "gesture-swipe-left": "\U000F073E", + "gesture-swipe-right": "\U000F073F", + "gesture-swipe-up": "\U000F0740", + "gesture-swipe-vertical": "\U000F0AC0", + "gesture-tap": "\U000F0741", + "gesture-tap-box": "\U000F12A9", + "gesture-tap-button": "\U000F12A8", + "gesture-tap-hold": "\U000F0D77", + "gesture-two-double-tap": "\U000F0742", + "gesture-two-tap": "\U000F0743", + "ghost": "\U000F02A0", + "ghost-off": "\U000F09F5", + "gif": "\U000F0D78", + "gift": "\U000F0E44", + "gift-outline": "\U000F02A1", + "git": "\U000F02A2", + "github": "\U000F02A4", + "gitlab": "\U000F0BA0", + "glass-cocktail": "\U000F0356", + "glass-cocktail-off": "\U000F15E6", + "glass-flute": "\U000F02A5", + "glass-mug": "\U000F02A6", + "glass-mug-off": "\U000F15E7", + "glass-mug-variant": "\U000F1116", + "glass-mug-variant-off": "\U000F15E8", + "glass-pint-outline": "\U000F130D", + "glass-stange": "\U000F02A7", + "glass-tulip": "\U000F02A8", + "glass-wine": "\U000F0876", + "glasses": "\U000F02AA", + "globe-light": "\U000F12D7", + "globe-model": "\U000F08E9", + "gmail": "\U000F02AB", + "gnome": "\U000F02AC", + "go-kart": "\U000F0D79", + "go-kart-track": "\U000F0D7A", + "gog": "\U000F0BA1", + "gold": "\U000F124F", + "golf": "\U000F0823", + "golf-cart": "\U000F11A4", + "golf-tee": "\U000F1083", + "gondola": "\U000F0686", + "goodreads": "\U000F0D7B", + "google": "\U000F02AD", + "google-ads": "\U000F0C87", + "google-analytics": "\U000F07CC", + "google-assistant": "\U000F07CD", + "google-cardboard": "\U000F02AE", + "google-chrome": "\U000F02AF", + "google-circles": "\U000F02B0", + "google-circles-communities": "\U000F02B1", + "google-circles-extended": "\U000F02B2", + "google-circles-group": "\U000F02B3", + "google-classroom": "\U000F02C0", + "google-cloud": "\U000F11F6", + "google-controller": "\U000F02B4", + "google-controller-off": "\U000F02B5", + "google-downasaur": "\U000F1362", + "google-drive": "\U000F02B6", + "google-earth": "\U000F02B7", + "google-fit": "\U000F096C", + "google-glass": "\U000F02B8", + "google-hangouts": "\U000F02C9", + "google-home": "\U000F0824", + "google-keep": "\U000F06DC", + "google-lens": "\U000F09F6", + "google-maps": "\U000F05F5", + "google-my-business": "\U000F1048", + "google-nearby": "\U000F02B9", + "google-photos": "\U000F06DD", + "google-play": "\U000F02BC", + "google-plus": "\U000F02BD", + "google-podcast": "\U000F0EB9", + "google-spreadsheet": "\U000F09F7", + "google-street-view": "\U000F0C88", + "google-translate": "\U000F02BF", + "gradient": "\U000F06A0", + "grain": "\U000F0D7C", + "graph": "\U000F1049", + "graph-outline": "\U000F104A", + "graphql": "\U000F0877", + "grass": "\U000F1510", + "grave-stone": "\U000F0BA2", + "grease-pencil": "\U000F0648", + "greater-than": "\U000F096D", + "greater-than-or-equal": "\U000F096E", + "grid": "\U000F02C1", + "grid-large": "\U000F0758", + "grid-off": "\U000F02C2", + "grill": "\U000F0E45", + "grill-outline": "\U000F118A", + "group": "\U000F02C3", + "guitar-acoustic": "\U000F0771", + "guitar-electric": "\U000F02C4", + "guitar-pick": "\U000F02C5", + "guitar-pick-outline": "\U000F02C6", + "guy-fawkes-mask": "\U000F0825", + "hail": "\U000F0AC1", + "hair-dryer": "\U000F10EF", + "hair-dryer-outline": "\U000F10F0", + "halloween": "\U000F0BA3", + "hamburger": "\U000F0685", + "hammer": "\U000F08EA", + "hammer-screwdriver": "\U000F1322", + "hammer-wrench": "\U000F1323", + "hand": "\U000F0A4F", + "hand-heart": "\U000F10F1", + "hand-heart-outline": "\U000F157E", + "hand-left": "\U000F0E46", + "hand-okay": "\U000F0A50", + "hand-peace": "\U000F0A51", + "hand-peace-variant": "\U000F0A52", + "hand-pointing-down": "\U000F0A53", + "hand-pointing-left": "\U000F0A54", + "hand-pointing-right": "\U000F02C7", + "hand-pointing-up": "\U000F0A55", + "hand-right": "\U000F0E47", + "hand-saw": "\U000F0E48", + "hand-wash": "\U000F157F", + "hand-wash-outline": "\U000F1580", + "hand-water": "\U000F139F", + "handball": "\U000F0F53", + "handcuffs": "\U000F113E", + "handshake": "\U000F1218", + "handshake-outline": "\U000F15A1", + "hanger": "\U000F02C8", + "hard-hat": "\U000F096F", + "harddisk": "\U000F02CA", + "harddisk-plus": "\U000F104B", + "harddisk-remove": "\U000F104C", + "hat-fedora": "\U000F0BA4", + "hazard-lights": "\U000F0C89", + "hdr": "\U000F0D7D", + "hdr-off": "\U000F0D7E", + "head": "\U000F135E", + "head-alert": "\U000F1338", + "head-alert-outline": "\U000F1339", + "head-check": "\U000F133A", + "head-check-outline": "\U000F133B", + "head-cog": "\U000F133C", + "head-cog-outline": "\U000F133D", + "head-dots-horizontal": "\U000F133E", + "head-dots-horizontal-outline": "\U000F133F", + "head-flash": "\U000F1340", + "head-flash-outline": "\U000F1341", + "head-heart": "\U000F1342", + "head-heart-outline": "\U000F1343", + "head-lightbulb": "\U000F1344", + "head-lightbulb-outline": "\U000F1345", + "head-minus": "\U000F1346", + "head-minus-outline": "\U000F1347", + "head-outline": "\U000F135F", + "head-plus": "\U000F1348", + "head-plus-outline": "\U000F1349", + "head-question": "\U000F134A", + "head-question-outline": "\U000F134B", + "head-remove": "\U000F134C", + "head-remove-outline": "\U000F134D", + "head-snowflake": "\U000F134E", + "head-snowflake-outline": "\U000F134F", + "head-sync": "\U000F1350", + "head-sync-outline": "\U000F1351", + "headphones": "\U000F02CB", + "headphones-bluetooth": "\U000F0970", + "headphones-box": "\U000F02CC", + "headphones-off": "\U000F07CE", + "headphones-settings": "\U000F02CD", + "headset": "\U000F02CE", + "headset-dock": "\U000F02CF", + "headset-off": "\U000F02D0", + "heart": "\U000F02D1", + "heart-box": "\U000F02D2", + "heart-box-outline": "\U000F02D3", + "heart-broken": "\U000F02D4", + "heart-broken-outline": "\U000F0D14", + "heart-circle": "\U000F0971", + "heart-circle-outline": "\U000F0972", + "heart-flash": "\U000F0EF9", + "heart-half": "\U000F06DF", + "heart-half-full": "\U000F06DE", + "heart-half-outline": "\U000F06E0", + "heart-minus": "\U000F142F", + "heart-minus-outline": "\U000F1432", + "heart-multiple": "\U000F0A56", + "heart-multiple-outline": "\U000F0A57", + "heart-off": "\U000F0759", + "heart-off-outline": "\U000F1434", + "heart-outline": "\U000F02D5", + "heart-plus": "\U000F142E", + "heart-plus-outline": "\U000F1431", + "heart-pulse": "\U000F05F6", + "heart-remove": "\U000F1430", + "heart-remove-outline": "\U000F1433", + "helicopter": "\U000F0AC2", + "help": "\U000F02D6", + "help-box": "\U000F078B", + "help-circle": "\U000F02D7", + "help-circle-outline": "\U000F0625", + "help-network": "\U000F06F5", + "help-network-outline": "\U000F0C8A", + "help-rhombus": "\U000F0BA5", + "help-rhombus-outline": "\U000F0BA6", + "hexadecimal": "\U000F12A7", + "hexagon": "\U000F02D8", + "hexagon-multiple": "\U000F06E1", + "hexagon-multiple-outline": "\U000F10F2", + "hexagon-outline": "\U000F02D9", + "hexagon-slice-1": "\U000F0AC3", + "hexagon-slice-2": "\U000F0AC4", + "hexagon-slice-3": "\U000F0AC5", + "hexagon-slice-4": "\U000F0AC6", + "hexagon-slice-5": "\U000F0AC7", + "hexagon-slice-6": "\U000F0AC8", + "hexagram": "\U000F0AC9", + "hexagram-outline": "\U000F0ACA", + "high-definition": "\U000F07CF", + "high-definition-box": "\U000F0878", + "highway": "\U000F05F7", + "hiking": "\U000F0D7F", + "hinduism": "\U000F0973", + "history": "\U000F02DA", + "hockey-puck": "\U000F0879", + "hockey-sticks": "\U000F087A", + "hololens": "\U000F02DB", + "home": "\U000F02DC", + "home-account": "\U000F0826", + "home-alert": "\U000F087B", + "home-alert-outline": "\U000F15D0", + "home-analytics": "\U000F0EBA", + "home-assistant": "\U000F07D0", + "home-automation": "\U000F07D1", + "home-circle": "\U000F07D2", + "home-circle-outline": "\U000F104D", + "home-city": "\U000F0D15", + "home-city-outline": "\U000F0D16", + "home-currency-usd": "\U000F08AF", + "home-edit": "\U000F1159", + "home-edit-outline": "\U000F115A", + "home-export-outline": "\U000F0F9B", + "home-flood": "\U000F0EFA", + "home-floor-0": "\U000F0DD2", + "home-floor-1": "\U000F0D80", + "home-floor-2": "\U000F0D81", + "home-floor-3": "\U000F0D82", + "home-floor-a": "\U000F0D83", + "home-floor-b": "\U000F0D84", + "home-floor-g": "\U000F0D85", + "home-floor-l": "\U000F0D86", + "home-floor-negative-1": "\U000F0DD3", + "home-group": "\U000F0DD4", + "home-heart": "\U000F0827", + "home-import-outline": "\U000F0F9C", + "home-lightbulb": "\U000F1251", + "home-lightbulb-outline": "\U000F1252", + "home-lock": "\U000F08EB", + "home-lock-open": "\U000F08EC", + "home-map-marker": "\U000F05F8", + "home-minus": "\U000F0974", + "home-minus-outline": "\U000F13D5", + "home-modern": "\U000F02DD", + "home-outline": "\U000F06A1", + "home-plus": "\U000F0975", + "home-plus-outline": "\U000F13D6", + "home-remove": "\U000F1247", + "home-remove-outline": "\U000F13D7", + "home-roof": "\U000F112B", + "home-search": "\U000F13B0", + "home-search-outline": "\U000F13B1", + "home-thermometer": "\U000F0F54", + "home-thermometer-outline": "\U000F0F55", + "home-variant": "\U000F02DE", + "home-variant-outline": "\U000F0BA7", + "hook": "\U000F06E2", + "hook-off": "\U000F06E3", + "hops": "\U000F02DF", + "horizontal-rotate-clockwise": "\U000F10F3", + "horizontal-rotate-counterclockwise": "\U000F10F4", + "horse": "\U000F15BF", + "horse-human": "\U000F15C0", + "horse-variant": "\U000F15C1", + "horseshoe": "\U000F0A58", + "hospital": "\U000F0FF6", + "hospital-box": "\U000F02E0", + "hospital-box-outline": "\U000F0FF7", + "hospital-building": "\U000F02E1", + "hospital-marker": "\U000F02E2", + "hot-tub": "\U000F0828", + "hours-24": "\U000F1478", + "hubspot": "\U000F0D17", + "hulu": "\U000F0829", + "human": "\U000F02E6", + "human-baby-changing-table": "\U000F138B", + "human-cane": "\U000F1581", + "human-capacity-decrease": "\U000F159B", + "human-capacity-increase": "\U000F159C", + "human-child": "\U000F02E7", + "human-edit": "\U000F14E8", + "human-female": "\U000F0649", + "human-female-boy": "\U000F0A59", + "human-female-dance": "\U000F15C9", + "human-female-female": "\U000F0A5A", + "human-female-girl": "\U000F0A5B", + "human-greeting": "\U000F064A", + "human-greeting-proximity": "\U000F159D", + "human-handsdown": "\U000F064B", + "human-handsup": "\U000F064C", + "human-male": "\U000F064D", + "human-male-boy": "\U000F0A5C", + "human-male-child": "\U000F138C", + "human-male-female": "\U000F02E8", + "human-male-girl": "\U000F0A5D", + "human-male-height": "\U000F0EFB", + "human-male-height-variant": "\U000F0EFC", + "human-male-male": "\U000F0A5E", + "human-pregnant": "\U000F05CF", + "human-queue": "\U000F1571", + "human-scooter": "\U000F11E9", + "human-wheelchair": "\U000F138D", + "humble-bundle": "\U000F0744", + "hvac": "\U000F1352", + "hvac-off": "\U000F159E", + "hydraulic-oil-level": "\U000F1324", + "hydraulic-oil-temperature": "\U000F1325", + "hydro-power": "\U000F12E5", + "ice-cream": "\U000F082A", + "ice-cream-off": "\U000F0E52", + "ice-pop": "\U000F0EFD", + "id-card": "\U000F0FC0", + "identifier": "\U000F0EFE", + "ideogram-cjk": "\U000F1331", + "ideogram-cjk-variant": "\U000F1332", + "iframe": "\U000F0C8B", + "iframe-array": "\U000F10F5", + "iframe-array-outline": "\U000F10F6", + "iframe-braces": "\U000F10F7", + "iframe-braces-outline": "\U000F10F8", + "iframe-outline": "\U000F0C8C", + "iframe-parentheses": "\U000F10F9", + "iframe-parentheses-outline": "\U000F10FA", + "iframe-variable": "\U000F10FB", + "iframe-variable-outline": "\U000F10FC", + "image": "\U000F02E9", + "image-album": "\U000F02EA", + "image-area": "\U000F02EB", + "image-area-close": "\U000F02EC", + "image-auto-adjust": "\U000F0FC1", + "image-broken": "\U000F02ED", + "image-broken-variant": "\U000F02EE", + "image-edit": "\U000F11E3", + "image-edit-outline": "\U000F11E4", + "image-filter-black-white": "\U000F02F0", + "image-filter-center-focus": "\U000F02F1", + "image-filter-center-focus-strong": "\U000F0EFF", + "image-filter-center-focus-strong-outline": "\U000F0F00", + "image-filter-center-focus-weak": "\U000F02F2", + "image-filter-drama": "\U000F02F3", + "image-filter-frames": "\U000F02F4", + "image-filter-hdr": "\U000F02F5", + "image-filter-none": "\U000F02F6", + "image-filter-tilt-shift": "\U000F02F7", + "image-filter-vintage": "\U000F02F8", + "image-frame": "\U000F0E49", + "image-minus": "\U000F1419", + "image-move": "\U000F09F8", + "image-multiple": "\U000F02F9", + "image-multiple-outline": "\U000F02EF", + "image-off": "\U000F082B", + "image-off-outline": "\U000F11D1", + "image-outline": "\U000F0976", + "image-plus": "\U000F087C", + "image-remove": "\U000F1418", + "image-search": "\U000F0977", + "image-search-outline": "\U000F0978", + "image-size-select-actual": "\U000F0C8D", + "image-size-select-large": "\U000F0C8E", + "image-size-select-small": "\U000F0C8F", + "image-text": "\U000F160D", + "import": "\U000F02FA", + "inbox": "\U000F0687", + "inbox-arrow-down": "\U000F02FB", + "inbox-arrow-down-outline": "\U000F1270", + "inbox-arrow-up": "\U000F03D1", + "inbox-arrow-up-outline": "\U000F1271", + "inbox-full": "\U000F1272", + "inbox-full-outline": "\U000F1273", + "inbox-multiple": "\U000F08B0", + "inbox-multiple-outline": "\U000F0BA8", + "inbox-outline": "\U000F1274", + "inbox-remove": "\U000F159F", + "inbox-remove-outline": "\U000F15A0", + "incognito": "\U000F05F9", + "incognito-circle": "\U000F1421", + "incognito-circle-off": "\U000F1422", + "incognito-off": "\U000F0075", + "infinity": "\U000F06E4", + "information": "\U000F02FC", + "information-outline": "\U000F02FD", + "information-variant": "\U000F064E", + "instagram": "\U000F02FE", + "instrument-triangle": "\U000F104E", + "invert-colors": "\U000F0301", + "invert-colors-off": "\U000F0E4A", + "iobroker": "\U000F12E8", + "ip": "\U000F0A5F", + "ip-network": "\U000F0A60", + "ip-network-outline": "\U000F0C90", + "ipod": "\U000F0C91", + "islam": "\U000F0979", + "island": "\U000F104F", + "iv-bag": "\U000F10B9", + "jabber": "\U000F0DD5", + "jeepney": "\U000F0302", + "jellyfish": "\U000F0F01", + "jellyfish-outline": "\U000F0F02", + "jira": "\U000F0303", + "jquery": "\U000F087D", + "jsfiddle": "\U000F0304", + "judaism": "\U000F097A", + "jump-rope": "\U000F12FF", + "kabaddi": "\U000F0D87", + "kangaroo": "\U000F1558", + "karate": "\U000F082C", + "keg": "\U000F0305", + "kettle": "\U000F05FA", + "kettle-alert": "\U000F1317", + "kettle-alert-outline": "\U000F1318", + "kettle-off": "\U000F131B", + "kettle-off-outline": "\U000F131C", + "kettle-outline": "\U000F0F56", + "kettle-steam": "\U000F1319", + "kettle-steam-outline": "\U000F131A", + "kettlebell": "\U000F1300", + "key": "\U000F0306", + "key-arrow-right": "\U000F1312", + "key-chain": "\U000F1574", + "key-chain-variant": "\U000F1575", + "key-change": "\U000F0307", + "key-link": "\U000F119F", + "key-minus": "\U000F0308", + "key-outline": "\U000F0DD6", + "key-plus": "\U000F0309", + "key-remove": "\U000F030A", + "key-star": "\U000F119E", + "key-variant": "\U000F030B", + "key-wireless": "\U000F0FC2", + "keyboard": "\U000F030C", + "keyboard-backspace": "\U000F030D", + "keyboard-caps": "\U000F030E", + "keyboard-close": "\U000F030F", + "keyboard-esc": "\U000F12B7", + "keyboard-f1": "\U000F12AB", + "keyboard-f10": "\U000F12B4", + "keyboard-f11": "\U000F12B5", + "keyboard-f12": "\U000F12B6", + "keyboard-f2": "\U000F12AC", + "keyboard-f3": "\U000F12AD", + "keyboard-f4": "\U000F12AE", + "keyboard-f5": "\U000F12AF", + "keyboard-f6": "\U000F12B0", + "keyboard-f7": "\U000F12B1", + "keyboard-f8": "\U000F12B2", + "keyboard-f9": "\U000F12B3", + "keyboard-off": "\U000F0310", + "keyboard-off-outline": "\U000F0E4B", + "keyboard-outline": "\U000F097B", + "keyboard-return": "\U000F0311", + "keyboard-settings": "\U000F09F9", + "keyboard-settings-outline": "\U000F09FA", + "keyboard-space": "\U000F1050", + "keyboard-tab": "\U000F0312", + "keyboard-variant": "\U000F0313", + "khanda": "\U000F10FD", + "kickstarter": "\U000F0745", + "klingon": "\U000F135B", + "knife": "\U000F09FB", + "knife-military": "\U000F09FC", + "kodi": "\U000F0314", + "kubernetes": "\U000F10FE", + "label": "\U000F0315", + "label-multiple": "\U000F1375", + "label-multiple-outline": "\U000F1376", + "label-off": "\U000F0ACB", + "label-off-outline": "\U000F0ACC", + "label-outline": "\U000F0316", + "label-percent": "\U000F12EA", + "label-percent-outline": "\U000F12EB", + "label-variant": "\U000F0ACD", + "label-variant-outline": "\U000F0ACE", + "ladder": "\U000F15A2", + "ladybug": "\U000F082D", + "lambda": "\U000F0627", + "lamp": "\U000F06B5", + "lamps": "\U000F1576", + "lan": "\U000F0317", + "lan-check": "\U000F12AA", + "lan-connect": "\U000F0318", + "lan-disconnect": "\U000F0319", + "lan-pending": "\U000F031A", + "language-c": "\U000F0671", + "language-cpp": "\U000F0672", + "language-csharp": "\U000F031B", + "language-css3": "\U000F031C", + "language-fortran": "\U000F121A", + "language-go": "\U000F07D3", + "language-haskell": "\U000F0C92", + "language-html5": "\U000F031D", + "language-java": "\U000F0B37", + "language-javascript": "\U000F031E", + "language-kotlin": "\U000F1219", + "language-lua": "\U000F08B1", + "language-markdown": "\U000F0354", + "language-markdown-outline": "\U000F0F5B", + "language-php": "\U000F031F", + "language-python": "\U000F0320", + "language-r": "\U000F07D4", + "language-ruby": "\U000F0D2D", + "language-ruby-on-rails": "\U000F0ACF", + "language-rust": "\U000F1617", + "language-swift": "\U000F06E5", + "language-typescript": "\U000F06E6", + "language-xaml": "\U000F0673", + "laptop": "\U000F0322", + "laptop-chromebook": "\U000F0323", + "laptop-mac": "\U000F0324", + "laptop-off": "\U000F06E7", + "laptop-windows": "\U000F0325", + "laravel": "\U000F0AD0", + "laser-pointer": "\U000F1484", + "lasso": "\U000F0F03", + "lastpass": "\U000F0446", + "latitude": "\U000F0F57", + "launch": "\U000F0327", + "lava-lamp": "\U000F07D5", + "layers": "\U000F0328", + "layers-minus": "\U000F0E4C", + "layers-off": "\U000F0329", + "layers-off-outline": "\U000F09FD", + "layers-outline": "\U000F09FE", + "layers-plus": "\U000F0E4D", + "layers-remove": "\U000F0E4E", + "layers-search": "\U000F1206", + "layers-search-outline": "\U000F1207", + "layers-triple": "\U000F0F58", + "layers-triple-outline": "\U000F0F59", + "lead-pencil": "\U000F064F", + "leaf": "\U000F032A", + "leaf-maple": "\U000F0C93", + "leaf-maple-off": "\U000F12DA", + "leaf-off": "\U000F12D9", + "leak": "\U000F0DD7", + "leak-off": "\U000F0DD8", + "led-off": "\U000F032B", + "led-on": "\U000F032C", + "led-outline": "\U000F032D", + "led-strip": "\U000F07D6", + "led-strip-variant": "\U000F1051", + "led-variant-off": "\U000F032E", + "led-variant-on": "\U000F032F", + "led-variant-outline": "\U000F0330", + "leek": "\U000F117D", + "less-than": "\U000F097C", + "less-than-or-equal": "\U000F097D", + "library": "\U000F0331", + "library-shelves": "\U000F0BA9", + "license": "\U000F0FC3", + "lifebuoy": "\U000F087E", + "light-switch": "\U000F097E", + "lightbulb": "\U000F0335", + "lightbulb-cfl": "\U000F1208", + "lightbulb-cfl-off": "\U000F1209", + "lightbulb-cfl-spiral": "\U000F1275", + "lightbulb-cfl-spiral-off": "\U000F12C3", + "lightbulb-group": "\U000F1253", + "lightbulb-group-off": "\U000F12CD", + "lightbulb-group-off-outline": "\U000F12CE", + "lightbulb-group-outline": "\U000F1254", + "lightbulb-multiple": "\U000F1255", + "lightbulb-multiple-off": "\U000F12CF", + "lightbulb-multiple-off-outline": "\U000F12D0", + "lightbulb-multiple-outline": "\U000F1256", + "lightbulb-off": "\U000F0E4F", + "lightbulb-off-outline": "\U000F0E50", + "lightbulb-on": "\U000F06E8", + "lightbulb-on-outline": "\U000F06E9", + "lightbulb-outline": "\U000F0336", + "lighthouse": "\U000F09FF", + "lighthouse-on": "\U000F0A00", + "lightning-bolt": "\U000F140B", + "lightning-bolt-outline": "\U000F140C", + "lingerie": "\U000F1476", + "link": "\U000F0337", + "link-box": "\U000F0D1A", + "link-box-outline": "\U000F0D1B", + "link-box-variant": "\U000F0D1C", + "link-box-variant-outline": "\U000F0D1D", + "link-lock": "\U000F10BA", + "link-off": "\U000F0338", + "link-plus": "\U000F0C94", + "link-variant": "\U000F0339", + "link-variant-minus": "\U000F10FF", + "link-variant-off": "\U000F033A", + "link-variant-plus": "\U000F1100", + "link-variant-remove": "\U000F1101", + "linkedin": "\U000F033B", + "linux": "\U000F033D", + "linux-mint": "\U000F08ED", + "lipstick": "\U000F13B5", + "list-status": "\U000F15AB", + "litecoin": "\U000F0A61", + "loading": "\U000F0772", + "location-enter": "\U000F0FC4", + "location-exit": "\U000F0FC5", + "lock": "\U000F033E", + "lock-alert": "\U000F08EE", + "lock-alert-outline": "\U000F15D1", + "lock-check": "\U000F139A", + "lock-clock": "\U000F097F", + "lock-open": "\U000F033F", + "lock-open-alert": "\U000F139B", + "lock-open-alert-outline": "\U000F15D2", + "lock-open-check": "\U000F139C", + "lock-open-outline": "\U000F0340", + "lock-open-variant": "\U000F0FC6", + "lock-open-variant-outline": "\U000F0FC7", + "lock-outline": "\U000F0341", + "lock-pattern": "\U000F06EA", + "lock-plus": "\U000F05FB", + "lock-question": "\U000F08EF", + "lock-reset": "\U000F0773", + "lock-smart": "\U000F08B2", + "locker": "\U000F07D7", + "locker-multiple": "\U000F07D8", + "login": "\U000F0342", + "login-variant": "\U000F05FC", + "logout": "\U000F0343", + "logout-variant": "\U000F05FD", + "longitude": "\U000F0F5A", + "looks": "\U000F0344", + "lotion": "\U000F1582", + "lotion-outline": "\U000F1583", + "lotion-plus": "\U000F1584", + "lotion-plus-outline": "\U000F1585", + "loupe": "\U000F0345", + "lumx": "\U000F0346", + "lungs": "\U000F1084", + "magnet": "\U000F0347", + "magnet-on": "\U000F0348", + "magnify": "\U000F0349", + "magnify-close": "\U000F0980", + "magnify-minus": "\U000F034A", + "magnify-minus-cursor": "\U000F0A62", + "magnify-minus-outline": "\U000F06EC", + "magnify-plus": "\U000F034B", + "magnify-plus-cursor": "\U000F0A63", + "magnify-plus-outline": "\U000F06ED", + "magnify-remove-cursor": "\U000F120C", + "magnify-remove-outline": "\U000F120D", + "magnify-scan": "\U000F1276", + "mail": "\U000F0EBB", + "mailbox": "\U000F06EE", + "mailbox-open": "\U000F0D88", + "mailbox-open-outline": "\U000F0D89", + "mailbox-open-up": "\U000F0D8A", + "mailbox-open-up-outline": "\U000F0D8B", + "mailbox-outline": "\U000F0D8C", + "mailbox-up": "\U000F0D8D", + "mailbox-up-outline": "\U000F0D8E", + "manjaro": "\U000F160A", + "map": "\U000F034D", + "map-check": "\U000F0EBC", + "map-check-outline": "\U000F0EBD", + "map-clock": "\U000F0D1E", + "map-clock-outline": "\U000F0D1F", + "map-legend": "\U000F0A01", + "map-marker": "\U000F034E", + "map-marker-alert": "\U000F0F05", + "map-marker-alert-outline": "\U000F0F06", + "map-marker-check": "\U000F0C95", + "map-marker-check-outline": "\U000F12FB", + "map-marker-circle": "\U000F034F", + "map-marker-distance": "\U000F08F0", + "map-marker-down": "\U000F1102", + "map-marker-left": "\U000F12DB", + "map-marker-left-outline": "\U000F12DD", + "map-marker-minus": "\U000F0650", + "map-marker-minus-outline": "\U000F12F9", + "map-marker-multiple": "\U000F0350", + "map-marker-multiple-outline": "\U000F1277", + "map-marker-off": "\U000F0351", + "map-marker-off-outline": "\U000F12FD", + "map-marker-outline": "\U000F07D9", + "map-marker-path": "\U000F0D20", + "map-marker-plus": "\U000F0651", + "map-marker-plus-outline": "\U000F12F8", + "map-marker-question": "\U000F0F07", + "map-marker-question-outline": "\U000F0F08", + "map-marker-radius": "\U000F0352", + "map-marker-radius-outline": "\U000F12FC", + "map-marker-remove": "\U000F0F09", + "map-marker-remove-outline": "\U000F12FA", + "map-marker-remove-variant": "\U000F0F0A", + "map-marker-right": "\U000F12DC", + "map-marker-right-outline": "\U000F12DE", + "map-marker-star": "\U000F1608", + "map-marker-star-outline": "\U000F1609", + "map-marker-up": "\U000F1103", + "map-minus": "\U000F0981", + "map-outline": "\U000F0982", + "map-plus": "\U000F0983", + "map-search": "\U000F0984", + "map-search-outline": "\U000F0985", + "mapbox": "\U000F0BAA", + "margin": "\U000F0353", + "marker": "\U000F0652", + "marker-cancel": "\U000F0DD9", + "marker-check": "\U000F0355", + "mastodon": "\U000F0AD1", + "material-design": "\U000F0986", + "material-ui": "\U000F0357", + "math-compass": "\U000F0358", + "math-cos": "\U000F0C96", + "math-integral": "\U000F0FC8", + "math-integral-box": "\U000F0FC9", + "math-log": "\U000F1085", + "math-norm": "\U000F0FCA", + "math-norm-box": "\U000F0FCB", + "math-sin": "\U000F0C97", + "math-tan": "\U000F0C98", + "matrix": "\U000F0628", + "medal": "\U000F0987", + "medal-outline": "\U000F1326", + "medical-bag": "\U000F06EF", + "meditation": "\U000F117B", + "memory": "\U000F035B", + "menu": "\U000F035C", + "menu-down": "\U000F035D", + "menu-down-outline": "\U000F06B6", + "menu-left": "\U000F035E", + "menu-left-outline": "\U000F0A02", + "menu-open": "\U000F0BAB", + "menu-right": "\U000F035F", + "menu-right-outline": "\U000F0A03", + "menu-swap": "\U000F0A64", + "menu-swap-outline": "\U000F0A65", + "menu-up": "\U000F0360", + "menu-up-outline": "\U000F06B7", + "merge": "\U000F0F5C", + "message": "\U000F0361", + "message-alert": "\U000F0362", + "message-alert-outline": "\U000F0A04", + "message-arrow-left": "\U000F12F2", + "message-arrow-left-outline": "\U000F12F3", + "message-arrow-right": "\U000F12F4", + "message-arrow-right-outline": "\U000F12F5", + "message-bookmark": "\U000F15AC", + "message-bookmark-outline": "\U000F15AD", + "message-bulleted": "\U000F06A2", + "message-bulleted-off": "\U000F06A3", + "message-cog": "\U000F06F1", + "message-cog-outline": "\U000F1172", + "message-draw": "\U000F0363", + "message-flash": "\U000F15A9", + "message-flash-outline": "\U000F15AA", + "message-image": "\U000F0364", + "message-image-outline": "\U000F116C", + "message-lock": "\U000F0FCC", + "message-lock-outline": "\U000F116D", + "message-minus": "\U000F116E", + "message-minus-outline": "\U000F116F", + "message-outline": "\U000F0365", + "message-plus": "\U000F0653", + "message-plus-outline": "\U000F10BB", + "message-processing": "\U000F0366", + "message-processing-outline": "\U000F1170", + "message-reply": "\U000F0367", + "message-reply-text": "\U000F0368", + "message-settings": "\U000F06F0", + "message-settings-outline": "\U000F1171", + "message-text": "\U000F0369", + "message-text-clock": "\U000F1173", + "message-text-clock-outline": "\U000F1174", + "message-text-lock": "\U000F0FCD", + "message-text-lock-outline": "\U000F1175", + "message-text-outline": "\U000F036A", + "message-video": "\U000F036B", + "meteor": "\U000F0629", + "metronome": "\U000F07DA", + "metronome-tick": "\U000F07DB", + "micro-sd": "\U000F07DC", + "microphone": "\U000F036C", + "microphone-minus": "\U000F08B3", + "microphone-off": "\U000F036D", + "microphone-outline": "\U000F036E", + "microphone-plus": "\U000F08B4", + "microphone-settings": "\U000F036F", + "microphone-variant": "\U000F0370", + "microphone-variant-off": "\U000F0371", + "microscope": "\U000F0654", + "microsoft": "\U000F0372", + "microsoft-access": "\U000F138E", + "microsoft-azure": "\U000F0805", + "microsoft-azure-devops": "\U000F0FD5", + "microsoft-bing": "\U000F00A4", + "microsoft-dynamics-365": "\U000F0988", + "microsoft-edge": "\U000F01E9", + "microsoft-edge-legacy": "\U000F1250", + "microsoft-excel": "\U000F138F", + "microsoft-internet-explorer": "\U000F0300", + "microsoft-office": "\U000F03C6", + "microsoft-onedrive": "\U000F03CA", + "microsoft-onenote": "\U000F0747", + "microsoft-outlook": "\U000F0D22", + "microsoft-powerpoint": "\U000F1390", + "microsoft-sharepoint": "\U000F1391", + "microsoft-teams": "\U000F02BB", + "microsoft-visual-studio": "\U000F0610", + "microsoft-visual-studio-code": "\U000F0A1E", + "microsoft-windows": "\U000F05B3", + "microsoft-windows-classic": "\U000F0A21", + "microsoft-word": "\U000F1392", + "microsoft-xbox": "\U000F05B9", + "microsoft-xbox-controller": "\U000F05BA", + "microsoft-xbox-controller-battery-alert": "\U000F074B", + "microsoft-xbox-controller-battery-charging": "\U000F0A22", + "microsoft-xbox-controller-battery-empty": "\U000F074C", + "microsoft-xbox-controller-battery-full": "\U000F074D", + "microsoft-xbox-controller-battery-low": "\U000F074E", + "microsoft-xbox-controller-battery-medium": "\U000F074F", + "microsoft-xbox-controller-battery-unknown": "\U000F0750", + "microsoft-xbox-controller-menu": "\U000F0E6F", + "microsoft-xbox-controller-off": "\U000F05BB", + "microsoft-xbox-controller-view": "\U000F0E70", + "microsoft-yammer": "\U000F0789", + "microwave": "\U000F0C99", + "microwave-off": "\U000F1423", + "middleware": "\U000F0F5D", + "middleware-outline": "\U000F0F5E", + "midi": "\U000F08F1", + "midi-port": "\U000F08F2", + "mine": "\U000F0DDA", + "minecraft": "\U000F0373", + "mini-sd": "\U000F0A05", + "minidisc": "\U000F0A06", + "minus": "\U000F0374", + "minus-box": "\U000F0375", + "minus-box-multiple": "\U000F1141", + "minus-box-multiple-outline": "\U000F1142", + "minus-box-outline": "\U000F06F2", + "minus-circle": "\U000F0376", + "minus-circle-multiple": "\U000F035A", + "minus-circle-multiple-outline": "\U000F0AD3", + "minus-circle-off": "\U000F1459", + "minus-circle-off-outline": "\U000F145A", + "minus-circle-outline": "\U000F0377", + "minus-network": "\U000F0378", + "minus-network-outline": "\U000F0C9A", + "mirror": "\U000F11FD", + "mixed-martial-arts": "\U000F0D8F", + "mixed-reality": "\U000F087F", + "molecule": "\U000F0BAC", + "molecule-co": "\U000F12FE", + "molecule-co2": "\U000F07E4", + "monitor": "\U000F0379", + "monitor-cellphone": "\U000F0989", + "monitor-cellphone-star": "\U000F098A", + "monitor-clean": "\U000F1104", + "monitor-dashboard": "\U000F0A07", + "monitor-edit": "\U000F12C6", + "monitor-eye": "\U000F13B4", + "monitor-lock": "\U000F0DDB", + "monitor-multiple": "\U000F037A", + "monitor-off": "\U000F0D90", + "monitor-screenshot": "\U000F0E51", + "monitor-share": "\U000F1483", + "monitor-speaker": "\U000F0F5F", + "monitor-speaker-off": "\U000F0F60", + "monitor-star": "\U000F0DDC", + "moon-first-quarter": "\U000F0F61", + "moon-full": "\U000F0F62", + "moon-last-quarter": "\U000F0F63", + "moon-new": "\U000F0F64", + "moon-waning-crescent": "\U000F0F65", + "moon-waning-gibbous": "\U000F0F66", + "moon-waxing-crescent": "\U000F0F67", + "moon-waxing-gibbous": "\U000F0F68", + "moped": "\U000F1086", + "moped-electric": "\U000F15B7", + "moped-electric-outline": "\U000F15B8", + "moped-outline": "\U000F15B9", + "more": "\U000F037B", + "mother-heart": "\U000F1314", + "mother-nurse": "\U000F0D21", + "motion": "\U000F15B2", + "motion-outline": "\U000F15B3", + "motion-pause": "\U000F1590", + "motion-pause-outline": "\U000F1592", + "motion-play": "\U000F158F", + "motion-play-outline": "\U000F1591", + "motion-sensor": "\U000F0D91", + "motion-sensor-off": "\U000F1435", + "motorbike": "\U000F037C", + "motorbike-electric": "\U000F15BA", + "mouse": "\U000F037D", + "mouse-bluetooth": "\U000F098B", + "mouse-move-down": "\U000F1550", + "mouse-move-up": "\U000F1551", + "mouse-move-vertical": "\U000F1552", + "mouse-off": "\U000F037E", + "mouse-variant": "\U000F037F", + "mouse-variant-off": "\U000F0380", + "move-resize": "\U000F0655", + "move-resize-variant": "\U000F0656", + "movie": "\U000F0381", + "movie-edit": "\U000F1122", + "movie-edit-outline": "\U000F1123", + "movie-filter": "\U000F1124", + "movie-filter-outline": "\U000F1125", + "movie-open": "\U000F0FCE", + "movie-open-outline": "\U000F0FCF", + "movie-outline": "\U000F0DDD", + "movie-roll": "\U000F07DE", + "movie-search": "\U000F11D2", + "movie-search-outline": "\U000F11D3", + "muffin": "\U000F098C", + "multiplication": "\U000F0382", + "multiplication-box": "\U000F0383", + "mushroom": "\U000F07DF", + "mushroom-off": "\U000F13FA", + "mushroom-off-outline": "\U000F13FB", + "mushroom-outline": "\U000F07E0", + "music": "\U000F075A", + "music-accidental-double-flat": "\U000F0F69", + "music-accidental-double-sharp": "\U000F0F6A", + "music-accidental-flat": "\U000F0F6B", + "music-accidental-natural": "\U000F0F6C", + "music-accidental-sharp": "\U000F0F6D", + "music-box": "\U000F0384", + "music-box-multiple": "\U000F0333", + "music-box-multiple-outline": "\U000F0F04", + "music-box-outline": "\U000F0385", + "music-circle": "\U000F0386", + "music-circle-outline": "\U000F0AD4", + "music-clef-alto": "\U000F0F6E", + "music-clef-bass": "\U000F0F6F", + "music-clef-treble": "\U000F0F70", + "music-note": "\U000F0387", + "music-note-bluetooth": "\U000F05FE", + "music-note-bluetooth-off": "\U000F05FF", + "music-note-eighth": "\U000F0388", + "music-note-eighth-dotted": "\U000F0F71", + "music-note-half": "\U000F0389", + "music-note-half-dotted": "\U000F0F72", + "music-note-off": "\U000F038A", + "music-note-off-outline": "\U000F0F73", + "music-note-outline": "\U000F0F74", + "music-note-plus": "\U000F0DDE", + "music-note-quarter": "\U000F038B", + "music-note-quarter-dotted": "\U000F0F75", + "music-note-sixteenth": "\U000F038C", + "music-note-sixteenth-dotted": "\U000F0F76", + "music-note-whole": "\U000F038D", + "music-note-whole-dotted": "\U000F0F77", + "music-off": "\U000F075B", + "music-rest-eighth": "\U000F0F78", + "music-rest-half": "\U000F0F79", + "music-rest-quarter": "\U000F0F7A", + "music-rest-sixteenth": "\U000F0F7B", + "music-rest-whole": "\U000F0F7C", + "mustache": "\U000F15DE", + "nail": "\U000F0DDF", + "nas": "\U000F08F3", + "nativescript": "\U000F0880", + "nature": "\U000F038E", + "nature-people": "\U000F038F", + "navigation": "\U000F0390", + "navigation-outline": "\U000F1607", + "near-me": "\U000F05CD", + "necklace": "\U000F0F0B", + "needle": "\U000F0391", + "netflix": "\U000F0746", + "network": "\U000F06F3", + "network-off": "\U000F0C9B", + "network-off-outline": "\U000F0C9C", + "network-outline": "\U000F0C9D", + "network-strength-1": "\U000F08F4", + "network-strength-1-alert": "\U000F08F5", + "network-strength-2": "\U000F08F6", + "network-strength-2-alert": "\U000F08F7", + "network-strength-3": "\U000F08F8", + "network-strength-3-alert": "\U000F08F9", + "network-strength-4": "\U000F08FA", + "network-strength-4-alert": "\U000F08FB", + "network-strength-off": "\U000F08FC", + "network-strength-off-outline": "\U000F08FD", + "network-strength-outline": "\U000F08FE", + "new-box": "\U000F0394", + "newspaper": "\U000F0395", + "newspaper-minus": "\U000F0F0C", + "newspaper-plus": "\U000F0F0D", + "newspaper-variant": "\U000F1001", + "newspaper-variant-multiple": "\U000F1002", + "newspaper-variant-multiple-outline": "\U000F1003", + "newspaper-variant-outline": "\U000F1004", + "nfc": "\U000F0396", + "nfc-search-variant": "\U000F0E53", + "nfc-tap": "\U000F0397", + "nfc-variant": "\U000F0398", + "nfc-variant-off": "\U000F0E54", + "ninja": "\U000F0774", + "nintendo-game-boy": "\U000F1393", + "nintendo-switch": "\U000F07E1", + "nintendo-wii": "\U000F05AB", + "nintendo-wiiu": "\U000F072D", + "nix": "\U000F1105", + "nodejs": "\U000F0399", + "noodles": "\U000F117E", + "not-equal": "\U000F098D", + "not-equal-variant": "\U000F098E", + "note": "\U000F039A", + "note-multiple": "\U000F06B8", + "note-multiple-outline": "\U000F06B9", + "note-outline": "\U000F039B", + "note-plus": "\U000F039C", + "note-plus-outline": "\U000F039D", + "note-text": "\U000F039E", + "note-text-outline": "\U000F11D7", + "notebook": "\U000F082E", + "notebook-check": "\U000F14F5", + "notebook-check-outline": "\U000F14F6", + "notebook-edit": "\U000F14E7", + "notebook-edit-outline": "\U000F14E9", + "notebook-minus": "\U000F1610", + "notebook-minus-outline": "\U000F1611", + "notebook-multiple": "\U000F0E55", + "notebook-outline": "\U000F0EBF", + "notebook-plus": "\U000F1612", + "notebook-plus-outline": "\U000F1613", + "notebook-remove": "\U000F1614", + "notebook-remove-outline": "\U000F1615", + "notification-clear-all": "\U000F039F", + "npm": "\U000F06F7", + "nuke": "\U000F06A4", + "null": "\U000F07E2", + "numeric": "\U000F03A0", + "numeric-0": "\U000F0B39", + "numeric-0-box": "\U000F03A1", + "numeric-0-box-multiple": "\U000F0F0E", + "numeric-0-box-multiple-outline": "\U000F03A2", + "numeric-0-box-outline": "\U000F03A3", + "numeric-0-circle": "\U000F0C9E", + "numeric-0-circle-outline": "\U000F0C9F", + "numeric-1": "\U000F0B3A", + "numeric-1-box": "\U000F03A4", + "numeric-1-box-multiple": "\U000F0F0F", + "numeric-1-box-multiple-outline": "\U000F03A5", + "numeric-1-box-outline": "\U000F03A6", + "numeric-1-circle": "\U000F0CA0", + "numeric-1-circle-outline": "\U000F0CA1", + "numeric-10": "\U000F0FE9", + "numeric-10-box": "\U000F0F7D", + "numeric-10-box-multiple": "\U000F0FEA", + "numeric-10-box-multiple-outline": "\U000F0FEB", + "numeric-10-box-outline": "\U000F0F7E", + "numeric-10-circle": "\U000F0FEC", + "numeric-10-circle-outline": "\U000F0FED", + "numeric-2": "\U000F0B3B", + "numeric-2-box": "\U000F03A7", + "numeric-2-box-multiple": "\U000F0F10", + "numeric-2-box-multiple-outline": "\U000F03A8", + "numeric-2-box-outline": "\U000F03A9", + "numeric-2-circle": "\U000F0CA2", + "numeric-2-circle-outline": "\U000F0CA3", + "numeric-3": "\U000F0B3C", + "numeric-3-box": "\U000F03AA", + "numeric-3-box-multiple": "\U000F0F11", + "numeric-3-box-multiple-outline": "\U000F03AB", + "numeric-3-box-outline": "\U000F03AC", + "numeric-3-circle": "\U000F0CA4", + "numeric-3-circle-outline": "\U000F0CA5", + "numeric-4": "\U000F0B3D", + "numeric-4-box": "\U000F03AD", + "numeric-4-box-multiple": "\U000F0F12", + "numeric-4-box-multiple-outline": "\U000F03B2", + "numeric-4-box-outline": "\U000F03AE", + "numeric-4-circle": "\U000F0CA6", + "numeric-4-circle-outline": "\U000F0CA7", + "numeric-5": "\U000F0B3E", + "numeric-5-box": "\U000F03B1", + "numeric-5-box-multiple": "\U000F0F13", + "numeric-5-box-multiple-outline": "\U000F03AF", + "numeric-5-box-outline": "\U000F03B0", + "numeric-5-circle": "\U000F0CA8", + "numeric-5-circle-outline": "\U000F0CA9", + "numeric-6": "\U000F0B3F", + "numeric-6-box": "\U000F03B3", + "numeric-6-box-multiple": "\U000F0F14", + "numeric-6-box-multiple-outline": "\U000F03B4", + "numeric-6-box-outline": "\U000F03B5", + "numeric-6-circle": "\U000F0CAA", + "numeric-6-circle-outline": "\U000F0CAB", + "numeric-7": "\U000F0B40", + "numeric-7-box": "\U000F03B6", + "numeric-7-box-multiple": "\U000F0F15", + "numeric-7-box-multiple-outline": "\U000F03B7", + "numeric-7-box-outline": "\U000F03B8", + "numeric-7-circle": "\U000F0CAC", + "numeric-7-circle-outline": "\U000F0CAD", + "numeric-8": "\U000F0B41", + "numeric-8-box": "\U000F03B9", + "numeric-8-box-multiple": "\U000F0F16", + "numeric-8-box-multiple-outline": "\U000F03BA", + "numeric-8-box-outline": "\U000F03BB", + "numeric-8-circle": "\U000F0CAE", + "numeric-8-circle-outline": "\U000F0CAF", + "numeric-9": "\U000F0B42", + "numeric-9-box": "\U000F03BC", + "numeric-9-box-multiple": "\U000F0F17", + "numeric-9-box-multiple-outline": "\U000F03BD", + "numeric-9-box-outline": "\U000F03BE", + "numeric-9-circle": "\U000F0CB0", + "numeric-9-circle-outline": "\U000F0CB1", + "numeric-9-plus": "\U000F0FEE", + "numeric-9-plus-box": "\U000F03BF", + "numeric-9-plus-box-multiple": "\U000F0F18", + "numeric-9-plus-box-multiple-outline": "\U000F03C0", + "numeric-9-plus-box-outline": "\U000F03C1", + "numeric-9-plus-circle": "\U000F0CB2", + "numeric-9-plus-circle-outline": "\U000F0CB3", + "numeric-negative-1": "\U000F1052", + "numeric-positive-1": "\U000F15CB", + "nut": "\U000F06F8", + "nutrition": "\U000F03C2", + "nuxt": "\U000F1106", + "oar": "\U000F067C", + "ocarina": "\U000F0DE0", + "oci": "\U000F12E9", + "ocr": "\U000F113A", + "octagon": "\U000F03C3", + "octagon-outline": "\U000F03C4", + "octagram": "\U000F06F9", + "octagram-outline": "\U000F0775", + "odnoklassniki": "\U000F03C5", + "offer": "\U000F121B", + "office-building": "\U000F0991", + "office-building-marker": "\U000F1520", + "office-building-marker-outline": "\U000F1521", + "office-building-outline": "\U000F151F", + "oil": "\U000F03C7", + "oil-lamp": "\U000F0F19", + "oil-level": "\U000F1053", + "oil-temperature": "\U000F0FF8", + "omega": "\U000F03C9", + "one-up": "\U000F0BAD", + "onepassword": "\U000F0881", + "opacity": "\U000F05CC", + "open-in-app": "\U000F03CB", + "open-in-new": "\U000F03CC", + "open-source-initiative": "\U000F0BAE", + "openid": "\U000F03CD", + "opera": "\U000F03CE", + "orbit": "\U000F0018", + "orbit-variant": "\U000F15DB", + "order-alphabetical-ascending": "\U000F020D", + "order-alphabetical-descending": "\U000F0D07", + "order-bool-ascending": "\U000F02BE", + "order-bool-ascending-variant": "\U000F098F", + "order-bool-descending": "\U000F1384", + "order-bool-descending-variant": "\U000F0990", + "order-numeric-ascending": "\U000F0545", + "order-numeric-descending": "\U000F0546", + "origin": "\U000F0B43", + "ornament": "\U000F03CF", + "ornament-variant": "\U000F03D0", + "outdoor-lamp": "\U000F1054", + "overscan": "\U000F1005", + "owl": "\U000F03D2", + "pac-man": "\U000F0BAF", + "package": "\U000F03D3", + "package-down": "\U000F03D4", + "package-up": "\U000F03D5", + "package-variant": "\U000F03D6", + "package-variant-closed": "\U000F03D7", + "page-first": "\U000F0600", + "page-last": "\U000F0601", + "page-layout-body": "\U000F06FA", + "page-layout-footer": "\U000F06FB", + "page-layout-header": "\U000F06FC", + "page-layout-header-footer": "\U000F0F7F", + "page-layout-sidebar-left": "\U000F06FD", + "page-layout-sidebar-right": "\U000F06FE", + "page-next": "\U000F0BB0", + "page-next-outline": "\U000F0BB1", + "page-previous": "\U000F0BB2", + "page-previous-outline": "\U000F0BB3", + "pail": "\U000F1417", + "pail-minus": "\U000F1437", + "pail-minus-outline": "\U000F143C", + "pail-off": "\U000F1439", + "pail-off-outline": "\U000F143E", + "pail-outline": "\U000F143A", + "pail-plus": "\U000F1436", + "pail-plus-outline": "\U000F143B", + "pail-remove": "\U000F1438", + "pail-remove-outline": "\U000F143D", + "palette": "\U000F03D8", + "palette-advanced": "\U000F03D9", + "palette-outline": "\U000F0E0C", + "palette-swatch": "\U000F08B5", + "palette-swatch-outline": "\U000F135C", + "palm-tree": "\U000F1055", + "pan": "\U000F0BB4", + "pan-bottom-left": "\U000F0BB5", + "pan-bottom-right": "\U000F0BB6", + "pan-down": "\U000F0BB7", + "pan-horizontal": "\U000F0BB8", + "pan-left": "\U000F0BB9", + "pan-right": "\U000F0BBA", + "pan-top-left": "\U000F0BBB", + "pan-top-right": "\U000F0BBC", + "pan-up": "\U000F0BBD", + "pan-vertical": "\U000F0BBE", + "panda": "\U000F03DA", + "pandora": "\U000F03DB", + "panorama": "\U000F03DC", + "panorama-fisheye": "\U000F03DD", + "panorama-horizontal": "\U000F03DE", + "panorama-vertical": "\U000F03DF", + "panorama-wide-angle": "\U000F03E0", + "paper-cut-vertical": "\U000F03E1", + "paper-roll": "\U000F1157", + "paper-roll-outline": "\U000F1158", + "paperclip": "\U000F03E2", + "parachute": "\U000F0CB4", + "parachute-outline": "\U000F0CB5", + "parking": "\U000F03E3", + "party-popper": "\U000F1056", + "passport": "\U000F07E3", + "passport-biometric": "\U000F0DE1", + "pasta": "\U000F1160", + "patio-heater": "\U000F0F80", + "patreon": "\U000F0882", + "pause": "\U000F03E4", + "pause-circle": "\U000F03E5", + "pause-circle-outline": "\U000F03E6", + "pause-octagon": "\U000F03E7", + "pause-octagon-outline": "\U000F03E8", + "paw": "\U000F03E9", + "paw-off": "\U000F0657", + "pdf-box": "\U000F0E56", + "peace": "\U000F0884", + "peanut": "\U000F0FFC", + "peanut-off": "\U000F0FFD", + "peanut-off-outline": "\U000F0FFF", + "peanut-outline": "\U000F0FFE", + "pen": "\U000F03EA", + "pen-lock": "\U000F0DE2", + "pen-minus": "\U000F0DE3", + "pen-off": "\U000F0DE4", + "pen-plus": "\U000F0DE5", + "pen-remove": "\U000F0DE6", + "pencil": "\U000F03EB", + "pencil-box": "\U000F03EC", + "pencil-box-multiple": "\U000F1144", + "pencil-box-multiple-outline": "\U000F1145", + "pencil-box-outline": "\U000F03ED", + "pencil-circle": "\U000F06FF", + "pencil-circle-outline": "\U000F0776", + "pencil-lock": "\U000F03EE", + "pencil-lock-outline": "\U000F0DE7", + "pencil-minus": "\U000F0DE8", + "pencil-minus-outline": "\U000F0DE9", + "pencil-off": "\U000F03EF", + "pencil-off-outline": "\U000F0DEA", + "pencil-outline": "\U000F0CB6", + "pencil-plus": "\U000F0DEB", + "pencil-plus-outline": "\U000F0DEC", + "pencil-remove": "\U000F0DED", + "pencil-remove-outline": "\U000F0DEE", + "pencil-ruler": "\U000F1353", + "penguin": "\U000F0EC0", + "pentagon": "\U000F0701", + "pentagon-outline": "\U000F0700", + "percent": "\U000F03F0", + "percent-outline": "\U000F1278", + "periodic-table": "\U000F08B6", + "perspective-less": "\U000F0D23", + "perspective-more": "\U000F0D24", + "pharmacy": "\U000F03F1", + "phone": "\U000F03F2", + "phone-alert": "\U000F0F1A", + "phone-alert-outline": "\U000F118E", + "phone-bluetooth": "\U000F03F3", + "phone-bluetooth-outline": "\U000F118F", + "phone-cancel": "\U000F10BC", + "phone-cancel-outline": "\U000F1190", + "phone-check": "\U000F11A9", + "phone-check-outline": "\U000F11AA", + "phone-classic": "\U000F0602", + "phone-classic-off": "\U000F1279", + "phone-dial": "\U000F1559", + "phone-dial-outline": "\U000F155A", + "phone-forward": "\U000F03F4", + "phone-forward-outline": "\U000F1191", + "phone-hangup": "\U000F03F5", + "phone-hangup-outline": "\U000F1192", + "phone-in-talk": "\U000F03F6", + "phone-in-talk-outline": "\U000F1182", + "phone-incoming": "\U000F03F7", + "phone-incoming-outline": "\U000F1193", + "phone-lock": "\U000F03F8", + "phone-lock-outline": "\U000F1194", + "phone-log": "\U000F03F9", + "phone-log-outline": "\U000F1195", + "phone-message": "\U000F1196", + "phone-message-outline": "\U000F1197", + "phone-minus": "\U000F0658", + "phone-minus-outline": "\U000F1198", + "phone-missed": "\U000F03FA", + "phone-missed-outline": "\U000F11A5", + "phone-off": "\U000F0DEF", + "phone-off-outline": "\U000F11A6", + "phone-outgoing": "\U000F03FB", + "phone-outgoing-outline": "\U000F1199", + "phone-outline": "\U000F0DF0", + "phone-paused": "\U000F03FC", + "phone-paused-outline": "\U000F119A", + "phone-plus": "\U000F0659", + "phone-plus-outline": "\U000F119B", + "phone-remove": "\U000F152F", + "phone-remove-outline": "\U000F1530", + "phone-return": "\U000F082F", + "phone-return-outline": "\U000F119C", + "phone-ring": "\U000F11AB", + "phone-ring-outline": "\U000F11AC", + "phone-rotate-landscape": "\U000F0885", + "phone-rotate-portrait": "\U000F0886", + "phone-settings": "\U000F03FD", + "phone-settings-outline": "\U000F119D", + "phone-voip": "\U000F03FE", + "pi": "\U000F03FF", + "pi-box": "\U000F0400", + "pi-hole": "\U000F0DF1", + "piano": "\U000F067D", + "pickaxe": "\U000F08B7", + "picture-in-picture-bottom-right": "\U000F0E57", + "picture-in-picture-bottom-right-outline": "\U000F0E58", + "picture-in-picture-top-right": "\U000F0E59", + "picture-in-picture-top-right-outline": "\U000F0E5A", + "pier": "\U000F0887", + "pier-crane": "\U000F0888", + "pig": "\U000F0401", + "pig-variant": "\U000F1006", + "piggy-bank": "\U000F1007", + "pill": "\U000F0402", + "pillar": "\U000F0702", + "pin": "\U000F0403", + "pin-off": "\U000F0404", + "pin-off-outline": "\U000F0930", + "pin-outline": "\U000F0931", + "pine-tree": "\U000F0405", + "pine-tree-box": "\U000F0406", + "pine-tree-fire": "\U000F141A", + "pinterest": "\U000F0407", + "pinwheel": "\U000F0AD5", + "pinwheel-outline": "\U000F0AD6", + "pipe": "\U000F07E5", + "pipe-disconnected": "\U000F07E6", + "pipe-leak": "\U000F0889", + "pipe-wrench": "\U000F1354", + "pirate": "\U000F0A08", + "pistol": "\U000F0703", + "piston": "\U000F088A", + "pitchfork": "\U000F1553", + "pizza": "\U000F0409", + "play": "\U000F040A", + "play-box": "\U000F127A", + "play-box-multiple": "\U000F0D19", + "play-box-multiple-outline": "\U000F13E6", + "play-box-outline": "\U000F040B", + "play-circle": "\U000F040C", + "play-circle-outline": "\U000F040D", + "play-network": "\U000F088B", + "play-network-outline": "\U000F0CB7", + "play-outline": "\U000F0F1B", + "play-pause": "\U000F040E", + "play-protected-content": "\U000F040F", + "play-speed": "\U000F08FF", + "playlist-check": "\U000F05C7", + "playlist-edit": "\U000F0900", + "playlist-minus": "\U000F0410", + "playlist-music": "\U000F0CB8", + "playlist-music-outline": "\U000F0CB9", + "playlist-play": "\U000F0411", + "playlist-plus": "\U000F0412", + "playlist-remove": "\U000F0413", + "playlist-star": "\U000F0DF2", + "plex": "\U000F06BA", + "plus": "\U000F0415", + "plus-box": "\U000F0416", + "plus-box-multiple": "\U000F0334", + "plus-box-multiple-outline": "\U000F1143", + "plus-box-outline": "\U000F0704", + "plus-circle": "\U000F0417", + "plus-circle-multiple": "\U000F034C", + "plus-circle-multiple-outline": "\U000F0418", + "plus-circle-outline": "\U000F0419", + "plus-minus": "\U000F0992", + "plus-minus-box": "\U000F0993", + "plus-minus-variant": "\U000F14C9", + "plus-network": "\U000F041A", + "plus-network-outline": "\U000F0CBA", + "plus-one": "\U000F041B", + "plus-outline": "\U000F0705", + "plus-thick": "\U000F11EC", + "podcast": "\U000F0994", + "podium": "\U000F0D25", + "podium-bronze": "\U000F0D26", + "podium-gold": "\U000F0D27", + "podium-silver": "\U000F0D28", + "point-of-sale": "\U000F0D92", + "pokeball": "\U000F041D", + "pokemon-go": "\U000F0A09", + "poker-chip": "\U000F0830", + "polaroid": "\U000F041E", + "police-badge": "\U000F1167", + "police-badge-outline": "\U000F1168", + "poll": "\U000F041F", + "poll-box": "\U000F0420", + "poll-box-outline": "\U000F127B", + "polo": "\U000F14C3", + "polymer": "\U000F0421", + "pool": "\U000F0606", + "popcorn": "\U000F0422", + "post": "\U000F1008", + "post-outline": "\U000F1009", + "postage-stamp": "\U000F0CBB", + "pot": "\U000F02E5", + "pot-mix": "\U000F065B", + "pot-mix-outline": "\U000F0677", + "pot-outline": "\U000F02FF", + "pot-steam": "\U000F065A", + "pot-steam-outline": "\U000F0326", + "pound": "\U000F0423", + "pound-box": "\U000F0424", + "pound-box-outline": "\U000F117F", + "power": "\U000F0425", + "power-cycle": "\U000F0901", + "power-off": "\U000F0902", + "power-on": "\U000F0903", + "power-plug": "\U000F06A5", + "power-plug-off": "\U000F06A6", + "power-plug-off-outline": "\U000F1424", + "power-plug-outline": "\U000F1425", + "power-settings": "\U000F0426", + "power-sleep": "\U000F0904", + "power-socket": "\U000F0427", + "power-socket-au": "\U000F0905", + "power-socket-de": "\U000F1107", + "power-socket-eu": "\U000F07E7", + "power-socket-fr": "\U000F1108", + "power-socket-it": "\U000F14FF", + "power-socket-jp": "\U000F1109", + "power-socket-uk": "\U000F07E8", + "power-socket-us": "\U000F07E9", + "power-standby": "\U000F0906", + "powershell": "\U000F0A0A", + "prescription": "\U000F0706", + "presentation": "\U000F0428", + "presentation-play": "\U000F0429", + "pretzel": "\U000F1562", + "printer": "\U000F042A", + "printer-3d": "\U000F042B", + "printer-3d-nozzle": "\U000F0E5B", + "printer-3d-nozzle-alert": "\U000F11C0", + "printer-3d-nozzle-alert-outline": "\U000F11C1", + "printer-3d-nozzle-outline": "\U000F0E5C", + "printer-alert": "\U000F042C", + "printer-check": "\U000F1146", + "printer-eye": "\U000F1458", + "printer-off": "\U000F0E5D", + "printer-pos": "\U000F1057", + "printer-search": "\U000F1457", + "printer-settings": "\U000F0707", + "printer-wireless": "\U000F0A0B", + "priority-high": "\U000F0603", + "priority-low": "\U000F0604", + "professional-hexagon": "\U000F042D", + "progress-alert": "\U000F0CBC", + "progress-check": "\U000F0995", + "progress-clock": "\U000F0996", + "progress-close": "\U000F110A", + "progress-download": "\U000F0997", + "progress-question": "\U000F1522", + "progress-upload": "\U000F0998", + "progress-wrench": "\U000F0CBD", + "projector": "\U000F042E", + "projector-screen": "\U000F042F", + "propane-tank": "\U000F1357", + "propane-tank-outline": "\U000F1358", + "protocol": "\U000F0FD8", + "publish": "\U000F06A7", + "pulse": "\U000F0430", + "pump": "\U000F1402", + "pumpkin": "\U000F0BBF", + "purse": "\U000F0F1C", + "purse-outline": "\U000F0F1D", + "puzzle": "\U000F0431", + "puzzle-check": "\U000F1426", + "puzzle-check-outline": "\U000F1427", + "puzzle-edit": "\U000F14D3", + "puzzle-edit-outline": "\U000F14D9", + "puzzle-heart": "\U000F14D4", + "puzzle-heart-outline": "\U000F14DA", + "puzzle-minus": "\U000F14D1", + "puzzle-minus-outline": "\U000F14D7", + "puzzle-outline": "\U000F0A66", + "puzzle-plus": "\U000F14D0", + "puzzle-plus-outline": "\U000F14D6", + "puzzle-remove": "\U000F14D2", + "puzzle-remove-outline": "\U000F14D8", + "puzzle-star": "\U000F14D5", + "puzzle-star-outline": "\U000F14DB", + "qi": "\U000F0999", + "qqchat": "\U000F0605", + "qrcode": "\U000F0432", + "qrcode-edit": "\U000F08B8", + "qrcode-minus": "\U000F118C", + "qrcode-plus": "\U000F118B", + "qrcode-remove": "\U000F118D", + "qrcode-scan": "\U000F0433", + "quadcopter": "\U000F0434", + "quality-high": "\U000F0435", + "quality-low": "\U000F0A0C", + "quality-medium": "\U000F0A0D", + "quora": "\U000F0D29", + "rabbit": "\U000F0907", + "racing-helmet": "\U000F0D93", + "racquetball": "\U000F0D94", + "radar": "\U000F0437", + "radiator": "\U000F0438", + "radiator-disabled": "\U000F0AD7", + "radiator-off": "\U000F0AD8", + "radio": "\U000F0439", + "radio-am": "\U000F0CBE", + "radio-fm": "\U000F0CBF", + "radio-handheld": "\U000F043A", + "radio-off": "\U000F121C", + "radio-tower": "\U000F043B", + "radioactive": "\U000F043C", + "radioactive-off": "\U000F0EC1", + "radiobox-blank": "\U000F043D", + "radiobox-marked": "\U000F043E", + "radiology-box": "\U000F14C5", + "radiology-box-outline": "\U000F14C6", + "radius": "\U000F0CC0", + "radius-outline": "\U000F0CC1", + "railroad-light": "\U000F0F1E", + "rake": "\U000F1544", + "raspberry-pi": "\U000F043F", + "ray-end": "\U000F0440", + "ray-end-arrow": "\U000F0441", + "ray-start": "\U000F0442", + "ray-start-arrow": "\U000F0443", + "ray-start-end": "\U000F0444", + "ray-start-vertex-end": "\U000F15D8", + "ray-vertex": "\U000F0445", + "react": "\U000F0708", + "read": "\U000F0447", + "receipt": "\U000F0449", + "record": "\U000F044A", + "record-circle": "\U000F0EC2", + "record-circle-outline": "\U000F0EC3", + "record-player": "\U000F099A", + "record-rec": "\U000F044B", + "rectangle": "\U000F0E5E", + "rectangle-outline": "\U000F0E5F", + "recycle": "\U000F044C", + "recycle-variant": "\U000F139D", + "reddit": "\U000F044D", + "redhat": "\U000F111B", + "redo": "\U000F044E", + "redo-variant": "\U000F044F", + "reflect-horizontal": "\U000F0A0E", + "reflect-vertical": "\U000F0A0F", + "refresh": "\U000F0450", + "refresh-circle": "\U000F1377", + "regex": "\U000F0451", + "registered-trademark": "\U000F0A67", + "reiterate": "\U000F1588", + "relation-many-to-many": "\U000F1496", + "relation-many-to-one": "\U000F1497", + "relation-many-to-one-or-many": "\U000F1498", + "relation-many-to-only-one": "\U000F1499", + "relation-many-to-zero-or-many": "\U000F149A", + "relation-many-to-zero-or-one": "\U000F149B", + "relation-one-or-many-to-many": "\U000F149C", + "relation-one-or-many-to-one": "\U000F149D", + "relation-one-or-many-to-one-or-many": "\U000F149E", + "relation-one-or-many-to-only-one": "\U000F149F", + "relation-one-or-many-to-zero-or-many": "\U000F14A0", + "relation-one-or-many-to-zero-or-one": "\U000F14A1", + "relation-one-to-many": "\U000F14A2", + "relation-one-to-one": "\U000F14A3", + "relation-one-to-one-or-many": "\U000F14A4", + "relation-one-to-only-one": "\U000F14A5", + "relation-one-to-zero-or-many": "\U000F14A6", + "relation-one-to-zero-or-one": "\U000F14A7", + "relation-only-one-to-many": "\U000F14A8", + "relation-only-one-to-one": "\U000F14A9", + "relation-only-one-to-one-or-many": "\U000F14AA", + "relation-only-one-to-only-one": "\U000F14AB", + "relation-only-one-to-zero-or-many": "\U000F14AC", + "relation-only-one-to-zero-or-one": "\U000F14AD", + "relation-zero-or-many-to-many": "\U000F14AE", + "relation-zero-or-many-to-one": "\U000F14AF", + "relation-zero-or-many-to-one-or-many": "\U000F14B0", + "relation-zero-or-many-to-only-one": "\U000F14B1", + "relation-zero-or-many-to-zero-or-many": "\U000F14B2", + "relation-zero-or-many-to-zero-or-one": "\U000F14B3", + "relation-zero-or-one-to-many": "\U000F14B4", + "relation-zero-or-one-to-one": "\U000F14B5", + "relation-zero-or-one-to-one-or-many": "\U000F14B6", + "relation-zero-or-one-to-only-one": "\U000F14B7", + "relation-zero-or-one-to-zero-or-many": "\U000F14B8", + "relation-zero-or-one-to-zero-or-one": "\U000F14B9", + "relative-scale": "\U000F0452", + "reload": "\U000F0453", + "reload-alert": "\U000F110B", + "reminder": "\U000F088C", + "remote": "\U000F0454", + "remote-desktop": "\U000F08B9", + "remote-off": "\U000F0EC4", + "remote-tv": "\U000F0EC5", + "remote-tv-off": "\U000F0EC6", + "rename-box": "\U000F0455", + "reorder-horizontal": "\U000F0688", + "reorder-vertical": "\U000F0689", + "repeat": "\U000F0456", + "repeat-off": "\U000F0457", + "repeat-once": "\U000F0458", + "replay": "\U000F0459", + "reply": "\U000F045A", + "reply-all": "\U000F045B", + "reply-all-outline": "\U000F0F1F", + "reply-circle": "\U000F11AE", + "reply-outline": "\U000F0F20", + "reproduction": "\U000F045C", + "resistor": "\U000F0B44", + "resistor-nodes": "\U000F0B45", + "resize": "\U000F0A68", + "resize-bottom-right": "\U000F045D", + "responsive": "\U000F045E", + "restart": "\U000F0709", + "restart-alert": "\U000F110C", + "restart-off": "\U000F0D95", + "restore": "\U000F099B", + "restore-alert": "\U000F110D", + "rewind": "\U000F045F", + "rewind-10": "\U000F0D2A", + "rewind-30": "\U000F0D96", + "rewind-5": "\U000F11F9", + "rewind-60": "\U000F160C", + "rewind-outline": "\U000F070A", + "rhombus": "\U000F070B", + "rhombus-medium": "\U000F0A10", + "rhombus-medium-outline": "\U000F14DC", + "rhombus-outline": "\U000F070C", + "rhombus-split": "\U000F0A11", + "rhombus-split-outline": "\U000F14DD", + "ribbon": "\U000F0460", + "rice": "\U000F07EA", + "rickshaw": "\U000F15BB", + "rickshaw-electric": "\U000F15BC", + "ring": "\U000F07EB", + "rivet": "\U000F0E60", + "road": "\U000F0461", + "road-variant": "\U000F0462", + "robber": "\U000F1058", + "robot": "\U000F06A9", + "robot-industrial": "\U000F0B46", + "robot-mower": "\U000F11F7", + "robot-mower-outline": "\U000F11F3", + "robot-vacuum": "\U000F070D", + "robot-vacuum-variant": "\U000F0908", + "rocket": "\U000F0463", + "rocket-launch": "\U000F14DE", + "rocket-launch-outline": "\U000F14DF", + "rocket-outline": "\U000F13AF", + "rodent": "\U000F1327", + "roller-skate": "\U000F0D2B", + "roller-skate-off": "\U000F0145", + "rollerblade": "\U000F0D2C", + "rollerblade-off": "\U000F002E", + "rollupjs": "\U000F0BC0", + "roman-numeral-1": "\U000F1088", + "roman-numeral-10": "\U000F1091", + "roman-numeral-2": "\U000F1089", + "roman-numeral-3": "\U000F108A", + "roman-numeral-4": "\U000F108B", + "roman-numeral-5": "\U000F108C", + "roman-numeral-6": "\U000F108D", + "roman-numeral-7": "\U000F108E", + "roman-numeral-8": "\U000F108F", + "roman-numeral-9": "\U000F1090", + "room-service": "\U000F088D", + "room-service-outline": "\U000F0D97", + "rotate-3d": "\U000F0EC7", + "rotate-3d-variant": "\U000F0464", + "rotate-left": "\U000F0465", + "rotate-left-variant": "\U000F0466", + "rotate-orbit": "\U000F0D98", + "rotate-right": "\U000F0467", + "rotate-right-variant": "\U000F0468", + "rounded-corner": "\U000F0607", + "router": "\U000F11E2", + "router-network": "\U000F1087", + "router-wireless": "\U000F0469", + "router-wireless-off": "\U000F15A3", + "router-wireless-settings": "\U000F0A69", + "routes": "\U000F046A", + "routes-clock": "\U000F1059", + "rowing": "\U000F0608", + "rss": "\U000F046B", + "rss-box": "\U000F046C", + "rss-off": "\U000F0F21", + "rug": "\U000F1475", + "rugby": "\U000F0D99", + "ruler": "\U000F046D", + "ruler-square": "\U000F0CC2", + "ruler-square-compass": "\U000F0EBE", + "run": "\U000F070E", + "run-fast": "\U000F046E", + "rv-truck": "\U000F11D4", + "sack": "\U000F0D2E", + "sack-percent": "\U000F0D2F", + "safe": "\U000F0A6A", + "safe-square": "\U000F127C", + "safe-square-outline": "\U000F127D", + "safety-goggles": "\U000F0D30", + "sail-boat": "\U000F0EC8", + "sale": "\U000F046F", + "salesforce": "\U000F088E", + "sass": "\U000F07EC", + "satellite": "\U000F0470", + "satellite-uplink": "\U000F0909", + "satellite-variant": "\U000F0471", + "sausage": "\U000F08BA", + "saw-blade": "\U000F0E61", + "sawtooth-wave": "\U000F147A", + "saxophone": "\U000F0609", + "scale": "\U000F0472", + "scale-balance": "\U000F05D1", + "scale-bathroom": "\U000F0473", + "scale-off": "\U000F105A", + "scan-helper": "\U000F13D8", + "scanner": "\U000F06AB", + "scanner-off": "\U000F090A", + "scatter-plot": "\U000F0EC9", + "scatter-plot-outline": "\U000F0ECA", + "school": "\U000F0474", + "school-outline": "\U000F1180", + "scissors-cutting": "\U000F0A6B", + "scooter": "\U000F15BD", + "scooter-electric": "\U000F15BE", + "scoreboard": "\U000F127E", + "scoreboard-outline": "\U000F127F", + "screen-rotation": "\U000F0475", + "screen-rotation-lock": "\U000F0478", + "screw-flat-top": "\U000F0DF3", + "screw-lag": "\U000F0DF4", + "screw-machine-flat-top": "\U000F0DF5", + "screw-machine-round-top": "\U000F0DF6", + "screw-round-top": "\U000F0DF7", + "screwdriver": "\U000F0476", + "script": "\U000F0BC1", + "script-outline": "\U000F0477", + "script-text": "\U000F0BC2", + "script-text-outline": "\U000F0BC3", + "sd": "\U000F0479", + "seal": "\U000F047A", + "seal-variant": "\U000F0FD9", + "search-web": "\U000F070F", + "seat": "\U000F0CC3", + "seat-flat": "\U000F047B", + "seat-flat-angled": "\U000F047C", + "seat-individual-suite": "\U000F047D", + "seat-legroom-extra": "\U000F047E", + "seat-legroom-normal": "\U000F047F", + "seat-legroom-reduced": "\U000F0480", + "seat-outline": "\U000F0CC4", + "seat-passenger": "\U000F1249", + "seat-recline-extra": "\U000F0481", + "seat-recline-normal": "\U000F0482", + "seatbelt": "\U000F0CC5", + "security": "\U000F0483", + "security-network": "\U000F0484", + "seed": "\U000F0E62", + "seed-off": "\U000F13FD", + "seed-off-outline": "\U000F13FE", + "seed-outline": "\U000F0E63", + "seesaw": "\U000F15A4", + "segment": "\U000F0ECB", + "select": "\U000F0485", + "select-all": "\U000F0486", + "select-color": "\U000F0D31", + "select-compare": "\U000F0AD9", + "select-drag": "\U000F0A6C", + "select-group": "\U000F0F82", + "select-inverse": "\U000F0487", + "select-marker": "\U000F1280", + "select-multiple": "\U000F1281", + "select-multiple-marker": "\U000F1282", + "select-off": "\U000F0488", + "select-place": "\U000F0FDA", + "select-search": "\U000F1204", + "selection": "\U000F0489", + "selection-drag": "\U000F0A6D", + "selection-ellipse": "\U000F0D32", + "selection-ellipse-arrow-inside": "\U000F0F22", + "selection-marker": "\U000F1283", + "selection-multiple": "\U000F1285", + "selection-multiple-marker": "\U000F1284", + "selection-off": "\U000F0777", + "selection-search": "\U000F1205", + "semantic-web": "\U000F1316", + "send": "\U000F048A", + "send-check": "\U000F1161", + "send-check-outline": "\U000F1162", + "send-circle": "\U000F0DF8", + "send-circle-outline": "\U000F0DF9", + "send-clock": "\U000F1163", + "send-clock-outline": "\U000F1164", + "send-lock": "\U000F07ED", + "send-lock-outline": "\U000F1166", + "send-outline": "\U000F1165", + "serial-port": "\U000F065C", + "server": "\U000F048B", + "server-minus": "\U000F048C", + "server-network": "\U000F048D", + "server-network-off": "\U000F048E", + "server-off": "\U000F048F", + "server-plus": "\U000F0490", + "server-remove": "\U000F0491", + "server-security": "\U000F0492", + "set-all": "\U000F0778", + "set-center": "\U000F0779", + "set-center-right": "\U000F077A", + "set-left": "\U000F077B", + "set-left-center": "\U000F077C", + "set-left-right": "\U000F077D", + "set-merge": "\U000F14E0", + "set-none": "\U000F077E", + "set-right": "\U000F077F", + "set-split": "\U000F14E1", + "set-square": "\U000F145D", + "set-top-box": "\U000F099F", + "settings-helper": "\U000F0A6E", + "shaker": "\U000F110E", + "shaker-outline": "\U000F110F", + "shape": "\U000F0831", + "shape-circle-plus": "\U000F065D", + "shape-outline": "\U000F0832", + "shape-oval-plus": "\U000F11FA", + "shape-plus": "\U000F0495", + "shape-polygon-plus": "\U000F065E", + "shape-rectangle-plus": "\U000F065F", + "shape-square-plus": "\U000F0660", + "shape-square-rounded-plus": "\U000F14FA", + "share": "\U000F0496", + "share-all": "\U000F11F4", + "share-all-outline": "\U000F11F5", + "share-circle": "\U000F11AD", + "share-off": "\U000F0F23", + "share-off-outline": "\U000F0F24", + "share-outline": "\U000F0932", + "share-variant": "\U000F0497", + "share-variant-outline": "\U000F1514", + "sheep": "\U000F0CC6", + "shield": "\U000F0498", + "shield-account": "\U000F088F", + "shield-account-outline": "\U000F0A12", + "shield-account-variant": "\U000F15A7", + "shield-account-variant-outline": "\U000F15A8", + "shield-airplane": "\U000F06BB", + "shield-airplane-outline": "\U000F0CC7", + "shield-alert": "\U000F0ECC", + "shield-alert-outline": "\U000F0ECD", + "shield-bug": "\U000F13DA", + "shield-bug-outline": "\U000F13DB", + "shield-car": "\U000F0F83", + "shield-check": "\U000F0565", + "shield-check-outline": "\U000F0CC8", + "shield-cross": "\U000F0CC9", + "shield-cross-outline": "\U000F0CCA", + "shield-edit": "\U000F11A0", + "shield-edit-outline": "\U000F11A1", + "shield-half": "\U000F1360", + "shield-half-full": "\U000F0780", + "shield-home": "\U000F068A", + "shield-home-outline": "\U000F0CCB", + "shield-key": "\U000F0BC4", + "shield-key-outline": "\U000F0BC5", + "shield-link-variant": "\U000F0D33", + "shield-link-variant-outline": "\U000F0D34", + "shield-lock": "\U000F099D", + "shield-lock-outline": "\U000F0CCC", + "shield-off": "\U000F099E", + "shield-off-outline": "\U000F099C", + "shield-outline": "\U000F0499", + "shield-plus": "\U000F0ADA", + "shield-plus-outline": "\U000F0ADB", + "shield-refresh": "\U000F00AA", + "shield-refresh-outline": "\U000F01E0", + "shield-remove": "\U000F0ADC", + "shield-remove-outline": "\U000F0ADD", + "shield-search": "\U000F0D9A", + "shield-star": "\U000F113B", + "shield-star-outline": "\U000F113C", + "shield-sun": "\U000F105D", + "shield-sun-outline": "\U000F105E", + "shield-sync": "\U000F11A2", + "shield-sync-outline": "\U000F11A3", + "ship-wheel": "\U000F0833", + "shoe-ballet": "\U000F15CA", + "shoe-cleat": "\U000F15C7", + "shoe-formal": "\U000F0B47", + "shoe-heel": "\U000F0B48", + "shoe-print": "\U000F0DFA", + "shoe-sneaker": "\U000F15C8", + "shopping": "\U000F049A", + "shopping-music": "\U000F049B", + "shopping-outline": "\U000F11D5", + "shopping-search": "\U000F0F84", + "shore": "\U000F14F9", + "shovel": "\U000F0710", + "shovel-off": "\U000F0711", + "shower": "\U000F09A0", + "shower-head": "\U000F09A1", + "shredder": "\U000F049C", + "shuffle": "\U000F049D", + "shuffle-disabled": "\U000F049E", + "shuffle-variant": "\U000F049F", + "shuriken": "\U000F137F", + "sigma": "\U000F04A0", + "sigma-lower": "\U000F062B", + "sign-caution": "\U000F04A1", + "sign-direction": "\U000F0781", + "sign-direction-minus": "\U000F1000", + "sign-direction-plus": "\U000F0FDC", + "sign-direction-remove": "\U000F0FDD", + "sign-pole": "\U000F14F8", + "sign-real-estate": "\U000F1118", + "sign-text": "\U000F0782", + "signal": "\U000F04A2", + "signal-2g": "\U000F0712", + "signal-3g": "\U000F0713", + "signal-4g": "\U000F0714", + "signal-5g": "\U000F0A6F", + "signal-cellular-1": "\U000F08BC", + "signal-cellular-2": "\U000F08BD", + "signal-cellular-3": "\U000F08BE", + "signal-cellular-outline": "\U000F08BF", + "signal-distance-variant": "\U000F0E64", + "signal-hspa": "\U000F0715", + "signal-hspa-plus": "\U000F0716", + "signal-off": "\U000F0783", + "signal-variant": "\U000F060A", + "signature": "\U000F0DFB", + "signature-freehand": "\U000F0DFC", + "signature-image": "\U000F0DFD", + "signature-text": "\U000F0DFE", + "silo": "\U000F0B49", + "silverware": "\U000F04A3", + "silverware-clean": "\U000F0FDE", + "silverware-fork": "\U000F04A4", + "silverware-fork-knife": "\U000F0A70", + "silverware-spoon": "\U000F04A5", + "silverware-variant": "\U000F04A6", + "sim": "\U000F04A7", + "sim-alert": "\U000F04A8", + "sim-alert-outline": "\U000F15D3", + "sim-off": "\U000F04A9", + "sim-off-outline": "\U000F15D4", + "sim-outline": "\U000F15D5", + "simple-icons": "\U000F131D", + "sina-weibo": "\U000F0ADF", + "sine-wave": "\U000F095B", + "sitemap": "\U000F04AA", + "size-l": "\U000F13A6", + "size-m": "\U000F13A5", + "size-s": "\U000F13A4", + "size-xl": "\U000F13A7", + "size-xs": "\U000F13A3", + "size-xxl": "\U000F13A8", + "size-xxs": "\U000F13A2", + "size-xxxl": "\U000F13A9", + "skate": "\U000F0D35", + "skateboard": "\U000F14C2", + "skew-less": "\U000F0D36", + "skew-more": "\U000F0D37", + "ski": "\U000F1304", + "ski-cross-country": "\U000F1305", + "ski-water": "\U000F1306", + "skip-backward": "\U000F04AB", + "skip-backward-outline": "\U000F0F25", + "skip-forward": "\U000F04AC", + "skip-forward-outline": "\U000F0F26", + "skip-next": "\U000F04AD", + "skip-next-circle": "\U000F0661", + "skip-next-circle-outline": "\U000F0662", + "skip-next-outline": "\U000F0F27", + "skip-previous": "\U000F04AE", + "skip-previous-circle": "\U000F0663", + "skip-previous-circle-outline": "\U000F0664", + "skip-previous-outline": "\U000F0F28", + "skull": "\U000F068C", + "skull-crossbones": "\U000F0BC6", + "skull-crossbones-outline": "\U000F0BC7", + "skull-outline": "\U000F0BC8", + "skull-scan": "\U000F14C7", + "skull-scan-outline": "\U000F14C8", + "skype": "\U000F04AF", + "skype-business": "\U000F04B0", + "slack": "\U000F04B1", + "slash-forward": "\U000F0FDF", + "slash-forward-box": "\U000F0FE0", + "sleep": "\U000F04B2", + "sleep-off": "\U000F04B3", + "slide": "\U000F15A5", + "slope-downhill": "\U000F0DFF", + "slope-uphill": "\U000F0E00", + "slot-machine": "\U000F1114", + "slot-machine-outline": "\U000F1115", + "smart-card": "\U000F10BD", + "smart-card-outline": "\U000F10BE", + "smart-card-reader": "\U000F10BF", + "smart-card-reader-outline": "\U000F10C0", + "smog": "\U000F0A71", + "smoke-detector": "\U000F0392", + "smoking": "\U000F04B4", + "smoking-off": "\U000F04B5", + "smoking-pipe": "\U000F140D", + "smoking-pipe-off": "\U000F1428", + "snake": "\U000F150E", + "snapchat": "\U000F04B6", + "snowboard": "\U000F1307", + "snowflake": "\U000F0717", + "snowflake-alert": "\U000F0F29", + "snowflake-melt": "\U000F12CB", + "snowflake-off": "\U000F14E3", + "snowflake-variant": "\U000F0F2A", + "snowman": "\U000F04B7", + "soccer": "\U000F04B8", + "soccer-field": "\U000F0834", + "social-distance-2-meters": "\U000F1579", + "social-distance-6-feet": "\U000F157A", + "sofa": "\U000F04B9", + "sofa-outline": "\U000F156D", + "sofa-single": "\U000F156E", + "sofa-single-outline": "\U000F156F", + "solar-panel": "\U000F0D9B", + "solar-panel-large": "\U000F0D9C", + "solar-power": "\U000F0A72", + "soldering-iron": "\U000F1092", + "solid": "\U000F068D", + "sony-playstation": "\U000F0414", + "sort": "\U000F04BA", + "sort-alphabetical-ascending": "\U000F05BD", + "sort-alphabetical-ascending-variant": "\U000F1148", + "sort-alphabetical-descending": "\U000F05BF", + "sort-alphabetical-descending-variant": "\U000F1149", + "sort-alphabetical-variant": "\U000F04BB", + "sort-ascending": "\U000F04BC", + "sort-bool-ascending": "\U000F1385", + "sort-bool-ascending-variant": "\U000F1386", + "sort-bool-descending": "\U000F1387", + "sort-bool-descending-variant": "\U000F1388", + "sort-calendar-ascending": "\U000F1547", + "sort-calendar-descending": "\U000F1548", + "sort-clock-ascending": "\U000F1549", + "sort-clock-ascending-outline": "\U000F154A", + "sort-clock-descending": "\U000F154B", + "sort-clock-descending-outline": "\U000F154C", + "sort-descending": "\U000F04BD", + "sort-numeric-ascending": "\U000F1389", + "sort-numeric-ascending-variant": "\U000F090D", + "sort-numeric-descending": "\U000F138A", + "sort-numeric-descending-variant": "\U000F0AD2", + "sort-numeric-variant": "\U000F04BE", + "sort-reverse-variant": "\U000F033C", + "sort-variant": "\U000F04BF", + "sort-variant-lock": "\U000F0CCD", + "sort-variant-lock-open": "\U000F0CCE", + "sort-variant-remove": "\U000F1147", + "soundcloud": "\U000F04C0", + "source-branch": "\U000F062C", + "source-branch-check": "\U000F14CF", + "source-branch-minus": "\U000F14CB", + "source-branch-plus": "\U000F14CA", + "source-branch-refresh": "\U000F14CD", + "source-branch-remove": "\U000F14CC", + "source-branch-sync": "\U000F14CE", + "source-commit": "\U000F0718", + "source-commit-end": "\U000F0719", + "source-commit-end-local": "\U000F071A", + "source-commit-local": "\U000F071B", + "source-commit-next-local": "\U000F071C", + "source-commit-start": "\U000F071D", + "source-commit-start-next-local": "\U000F071E", + "source-fork": "\U000F04C1", + "source-merge": "\U000F062D", + "source-pull": "\U000F04C2", + "source-repository": "\U000F0CCF", + "source-repository-multiple": "\U000F0CD0", + "soy-sauce": "\U000F07EE", + "soy-sauce-off": "\U000F13FC", + "spa": "\U000F0CD1", + "spa-outline": "\U000F0CD2", + "space-invaders": "\U000F0BC9", + "space-station": "\U000F1383", + "spade": "\U000F0E65", + "sparkles": "\U000F1545", + "speaker": "\U000F04C3", + "speaker-bluetooth": "\U000F09A2", + "speaker-multiple": "\U000F0D38", + "speaker-off": "\U000F04C4", + "speaker-wireless": "\U000F071F", + "speedometer": "\U000F04C5", + "speedometer-medium": "\U000F0F85", + "speedometer-slow": "\U000F0F86", + "spellcheck": "\U000F04C6", + "spider": "\U000F11EA", + "spider-thread": "\U000F11EB", + "spider-web": "\U000F0BCA", + "spirit-level": "\U000F14F1", + "spoon-sugar": "\U000F1429", + "spotify": "\U000F04C7", + "spotlight": "\U000F04C8", + "spotlight-beam": "\U000F04C9", + "spray": "\U000F0665", + "spray-bottle": "\U000F0AE0", + "sprinkler": "\U000F105F", + "sprinkler-variant": "\U000F1060", + "sprout": "\U000F0E66", + "sprout-outline": "\U000F0E67", + "square": "\U000F0764", + "square-circle": "\U000F1500", + "square-edit-outline": "\U000F090C", + "square-medium": "\U000F0A13", + "square-medium-outline": "\U000F0A14", + "square-off": "\U000F12EE", + "square-off-outline": "\U000F12EF", + "square-outline": "\U000F0763", + "square-root": "\U000F0784", + "square-root-box": "\U000F09A3", + "square-rounded": "\U000F14FB", + "square-rounded-outline": "\U000F14FC", + "square-small": "\U000F0A15", + "square-wave": "\U000F147B", + "squeegee": "\U000F0AE1", + "ssh": "\U000F08C0", + "stack-exchange": "\U000F060B", + "stack-overflow": "\U000F04CC", + "stackpath": "\U000F0359", + "stadium": "\U000F0FF9", + "stadium-variant": "\U000F0720", + "stairs": "\U000F04CD", + "stairs-box": "\U000F139E", + "stairs-down": "\U000F12BE", + "stairs-up": "\U000F12BD", + "stamper": "\U000F0D39", + "standard-definition": "\U000F07EF", + "star": "\U000F04CE", + "star-box": "\U000F0A73", + "star-box-multiple": "\U000F1286", + "star-box-multiple-outline": "\U000F1287", + "star-box-outline": "\U000F0A74", + "star-check": "\U000F1566", + "star-check-outline": "\U000F156A", + "star-circle": "\U000F04CF", + "star-circle-outline": "\U000F09A4", + "star-face": "\U000F09A5", + "star-four-points": "\U000F0AE2", + "star-four-points-outline": "\U000F0AE3", + "star-half": "\U000F0246", + "star-half-full": "\U000F04D0", + "star-minus": "\U000F1564", + "star-minus-outline": "\U000F1568", + "star-off": "\U000F04D1", + "star-off-outline": "\U000F155B", + "star-outline": "\U000F04D2", + "star-plus": "\U000F1563", + "star-plus-outline": "\U000F1567", + "star-remove": "\U000F1565", + "star-remove-outline": "\U000F1569", + "star-three-points": "\U000F0AE4", + "star-three-points-outline": "\U000F0AE5", + "state-machine": "\U000F11EF", + "steam": "\U000F04D3", + "steering": "\U000F04D4", + "steering-off": "\U000F090E", + "step-backward": "\U000F04D5", + "step-backward-2": "\U000F04D6", + "step-forward": "\U000F04D7", + "step-forward-2": "\U000F04D8", + "stethoscope": "\U000F04D9", + "sticker": "\U000F1364", + "sticker-alert": "\U000F1365", + "sticker-alert-outline": "\U000F1366", + "sticker-check": "\U000F1367", + "sticker-check-outline": "\U000F1368", + "sticker-circle-outline": "\U000F05D0", + "sticker-emoji": "\U000F0785", + "sticker-minus": "\U000F1369", + "sticker-minus-outline": "\U000F136A", + "sticker-outline": "\U000F136B", + "sticker-plus": "\U000F136C", + "sticker-plus-outline": "\U000F136D", + "sticker-remove": "\U000F136E", + "sticker-remove-outline": "\U000F136F", + "stocking": "\U000F04DA", + "stomach": "\U000F1093", + "stop": "\U000F04DB", + "stop-circle": "\U000F0666", + "stop-circle-outline": "\U000F0667", + "store": "\U000F04DC", + "store-24-hour": "\U000F04DD", + "store-outline": "\U000F1361", + "storefront": "\U000F07C7", + "storefront-outline": "\U000F10C1", + "stove": "\U000F04DE", + "strategy": "\U000F11D6", + "stretch-to-page": "\U000F0F2B", + "stretch-to-page-outline": "\U000F0F2C", + "string-lights": "\U000F12BA", + "string-lights-off": "\U000F12BB", + "subdirectory-arrow-left": "\U000F060C", + "subdirectory-arrow-right": "\U000F060D", + "submarine": "\U000F156C", + "subtitles": "\U000F0A16", + "subtitles-outline": "\U000F0A17", + "subway": "\U000F06AC", + "subway-alert-variant": "\U000F0D9D", + "subway-variant": "\U000F04DF", + "summit": "\U000F0786", + "sunglasses": "\U000F04E0", + "surround-sound": "\U000F05C5", + "surround-sound-2-0": "\U000F07F0", + "surround-sound-3-1": "\U000F07F1", + "surround-sound-5-1": "\U000F07F2", + "surround-sound-7-1": "\U000F07F3", + "svg": "\U000F0721", + "swap-horizontal": "\U000F04E1", + "swap-horizontal-bold": "\U000F0BCD", + "swap-horizontal-circle": "\U000F0FE1", + "swap-horizontal-circle-outline": "\U000F0FE2", + "swap-horizontal-variant": "\U000F08C1", + "swap-vertical": "\U000F04E2", + "swap-vertical-bold": "\U000F0BCE", + "swap-vertical-circle": "\U000F0FE3", + "swap-vertical-circle-outline": "\U000F0FE4", + "swap-vertical-variant": "\U000F08C2", + "swim": "\U000F04E3", + "switch": "\U000F04E4", + "sword": "\U000F04E5", + "sword-cross": "\U000F0787", + "syllabary-hangul": "\U000F1333", + "syllabary-hiragana": "\U000F1334", + "syllabary-katakana": "\U000F1335", + "syllabary-katakana-halfwidth": "\U000F1336", + "symbol": "\U000F1501", + "symfony": "\U000F0AE6", + "sync": "\U000F04E6", + "sync-alert": "\U000F04E7", + "sync-circle": "\U000F1378", + "sync-off": "\U000F04E8", + "tab": "\U000F04E9", + "tab-minus": "\U000F0B4B", + "tab-plus": "\U000F075C", + "tab-remove": "\U000F0B4C", + "tab-unselected": "\U000F04EA", + "table": "\U000F04EB", + "table-account": "\U000F13B9", + "table-alert": "\U000F13BA", + "table-arrow-down": "\U000F13BB", + "table-arrow-left": "\U000F13BC", + "table-arrow-right": "\U000F13BD", + "table-arrow-up": "\U000F13BE", + "table-border": "\U000F0A18", + "table-cancel": "\U000F13BF", + "table-chair": "\U000F1061", + "table-check": "\U000F13C0", + "table-clock": "\U000F13C1", + "table-cog": "\U000F13C2", + "table-column": "\U000F0835", + "table-column-plus-after": "\U000F04EC", + "table-column-plus-before": "\U000F04ED", + "table-column-remove": "\U000F04EE", + "table-column-width": "\U000F04EF", + "table-edit": "\U000F04F0", + "table-eye": "\U000F1094", + "table-eye-off": "\U000F13C3", + "table-furniture": "\U000F05BC", + "table-headers-eye": "\U000F121D", + "table-headers-eye-off": "\U000F121E", + "table-heart": "\U000F13C4", + "table-key": "\U000F13C5", + "table-large": "\U000F04F1", + "table-large-plus": "\U000F0F87", + "table-large-remove": "\U000F0F88", + "table-lock": "\U000F13C6", + "table-merge-cells": "\U000F09A6", + "table-minus": "\U000F13C7", + "table-multiple": "\U000F13C8", + "table-network": "\U000F13C9", + "table-of-contents": "\U000F0836", + "table-off": "\U000F13CA", + "table-plus": "\U000F0A75", + "table-refresh": "\U000F13A0", + "table-remove": "\U000F0A76", + "table-row": "\U000F0837", + "table-row-height": "\U000F04F2", + "table-row-plus-after": "\U000F04F3", + "table-row-plus-before": "\U000F04F4", + "table-row-remove": "\U000F04F5", + "table-search": "\U000F090F", + "table-settings": "\U000F0838", + "table-split-cell": "\U000F142A", + "table-star": "\U000F13CB", + "table-sync": "\U000F13A1", + "table-tennis": "\U000F0E68", + "tablet": "\U000F04F6", + "tablet-android": "\U000F04F7", + "tablet-cellphone": "\U000F09A7", + "tablet-dashboard": "\U000F0ECE", + "tablet-ipad": "\U000F04F8", + "taco": "\U000F0762", + "tag": "\U000F04F9", + "tag-faces": "\U000F04FA", + "tag-heart": "\U000F068B", + "tag-heart-outline": "\U000F0BCF", + "tag-minus": "\U000F0910", + "tag-minus-outline": "\U000F121F", + "tag-multiple": "\U000F04FB", + "tag-multiple-outline": "\U000F12F7", + "tag-off": "\U000F1220", + "tag-off-outline": "\U000F1221", + "tag-outline": "\U000F04FC", + "tag-plus": "\U000F0722", + "tag-plus-outline": "\U000F1222", + "tag-remove": "\U000F0723", + "tag-remove-outline": "\U000F1223", + "tag-text": "\U000F1224", + "tag-text-outline": "\U000F04FD", + "tailwind": "\U000F13FF", + "tank": "\U000F0D3A", + "tanker-truck": "\U000F0FE5", + "tape-measure": "\U000F0B4D", + "target": "\U000F04FE", + "target-account": "\U000F0BD0", + "target-variant": "\U000F0A77", + "taxi": "\U000F04FF", + "tea": "\U000F0D9E", + "tea-outline": "\U000F0D9F", + "teach": "\U000F0890", + "teamviewer": "\U000F0500", + "telegram": "\U000F0501", + "telescope": "\U000F0B4E", + "television": "\U000F0502", + "television-ambient-light": "\U000F1356", + "television-box": "\U000F0839", + "television-classic": "\U000F07F4", + "television-classic-off": "\U000F083A", + "television-clean": "\U000F1110", + "television-guide": "\U000F0503", + "television-off": "\U000F083B", + "television-pause": "\U000F0F89", + "television-play": "\U000F0ECF", + "television-stop": "\U000F0F8A", + "temperature-celsius": "\U000F0504", + "temperature-fahrenheit": "\U000F0505", + "temperature-kelvin": "\U000F0506", + "tennis": "\U000F0DA0", + "tennis-ball": "\U000F0507", + "tent": "\U000F0508", + "terraform": "\U000F1062", + "terrain": "\U000F0509", + "test-tube": "\U000F0668", + "test-tube-empty": "\U000F0911", + "test-tube-off": "\U000F0912", + "text": "\U000F09A8", + "text-account": "\U000F1570", + "text-box": "\U000F021A", + "text-box-check": "\U000F0EA6", + "text-box-check-outline": "\U000F0EA7", + "text-box-minus": "\U000F0EA8", + "text-box-minus-outline": "\U000F0EA9", + "text-box-multiple": "\U000F0AB7", + "text-box-multiple-outline": "\U000F0AB8", + "text-box-outline": "\U000F09ED", + "text-box-plus": "\U000F0EAA", + "text-box-plus-outline": "\U000F0EAB", + "text-box-remove": "\U000F0EAC", + "text-box-remove-outline": "\U000F0EAD", + "text-box-search": "\U000F0EAE", + "text-box-search-outline": "\U000F0EAF", + "text-recognition": "\U000F113D", + "text-search": "\U000F13B8", + "text-shadow": "\U000F0669", + "text-short": "\U000F09A9", + "text-subject": "\U000F09AA", + "text-to-speech": "\U000F050A", + "text-to-speech-off": "\U000F050B", + "texture": "\U000F050C", + "texture-box": "\U000F0FE6", + "theater": "\U000F050D", + "theme-light-dark": "\U000F050E", + "thermometer": "\U000F050F", + "thermometer-alert": "\U000F0E01", + "thermometer-chevron-down": "\U000F0E02", + "thermometer-chevron-up": "\U000F0E03", + "thermometer-high": "\U000F10C2", + "thermometer-lines": "\U000F0510", + "thermometer-low": "\U000F10C3", + "thermometer-minus": "\U000F0E04", + "thermometer-off": "\U000F1531", + "thermometer-plus": "\U000F0E05", + "thermostat": "\U000F0393", + "thermostat-box": "\U000F0891", + "thought-bubble": "\U000F07F6", + "thought-bubble-outline": "\U000F07F7", + "thumb-down": "\U000F0511", + "thumb-down-outline": "\U000F0512", + "thumb-up": "\U000F0513", + "thumb-up-outline": "\U000F0514", + "thumbs-up-down": "\U000F0515", + "ticket": "\U000F0516", + "ticket-account": "\U000F0517", + "ticket-confirmation": "\U000F0518", + "ticket-confirmation-outline": "\U000F13AA", + "ticket-outline": "\U000F0913", + "ticket-percent": "\U000F0724", + "ticket-percent-outline": "\U000F142B", + "tie": "\U000F0519", + "tilde": "\U000F0725", + "timelapse": "\U000F051A", + "timeline": "\U000F0BD1", + "timeline-alert": "\U000F0F95", + "timeline-alert-outline": "\U000F0F98", + "timeline-check": "\U000F1532", + "timeline-check-outline": "\U000F1533", + "timeline-clock": "\U000F11FB", + "timeline-clock-outline": "\U000F11FC", + "timeline-help": "\U000F0F99", + "timeline-help-outline": "\U000F0F9A", + "timeline-minus": "\U000F1534", + "timeline-minus-outline": "\U000F1535", + "timeline-outline": "\U000F0BD2", + "timeline-plus": "\U000F0F96", + "timeline-plus-outline": "\U000F0F97", + "timeline-remove": "\U000F1536", + "timeline-remove-outline": "\U000F1537", + "timeline-text": "\U000F0BD3", + "timeline-text-outline": "\U000F0BD4", + "timer": "\U000F13AB", + "timer-10": "\U000F051C", + "timer-3": "\U000F051D", + "timer-off": "\U000F13AC", + "timer-off-outline": "\U000F051E", + "timer-outline": "\U000F051B", + "timer-sand": "\U000F051F", + "timer-sand-empty": "\U000F06AD", + "timer-sand-full": "\U000F078C", + "timetable": "\U000F0520", + "toaster": "\U000F1063", + "toaster-off": "\U000F11B7", + "toaster-oven": "\U000F0CD3", + "toggle-switch": "\U000F0521", + "toggle-switch-off": "\U000F0522", + "toggle-switch-off-outline": "\U000F0A19", + "toggle-switch-outline": "\U000F0A1A", + "toilet": "\U000F09AB", + "toolbox": "\U000F09AC", + "toolbox-outline": "\U000F09AD", + "tools": "\U000F1064", + "tooltip": "\U000F0523", + "tooltip-account": "\U000F000C", + "tooltip-check": "\U000F155C", + "tooltip-check-outline": "\U000F155D", + "tooltip-edit": "\U000F0524", + "tooltip-edit-outline": "\U000F12C5", + "tooltip-image": "\U000F0525", + "tooltip-image-outline": "\U000F0BD5", + "tooltip-minus": "\U000F155E", + "tooltip-minus-outline": "\U000F155F", + "tooltip-outline": "\U000F0526", + "tooltip-plus": "\U000F0BD6", + "tooltip-plus-outline": "\U000F0527", + "tooltip-remove": "\U000F1560", + "tooltip-remove-outline": "\U000F1561", + "tooltip-text": "\U000F0528", + "tooltip-text-outline": "\U000F0BD7", + "tooth": "\U000F08C3", + "tooth-outline": "\U000F0529", + "toothbrush": "\U000F1129", + "toothbrush-electric": "\U000F112C", + "toothbrush-paste": "\U000F112A", + "torch": "\U000F1606", + "tortoise": "\U000F0D3B", + "toslink": "\U000F12B8", + "tournament": "\U000F09AE", + "tow-truck": "\U000F083C", + "tower-beach": "\U000F0681", + "tower-fire": "\U000F0682", + "toy-brick": "\U000F1288", + "toy-brick-marker": "\U000F1289", + "toy-brick-marker-outline": "\U000F128A", + "toy-brick-minus": "\U000F128B", + "toy-brick-minus-outline": "\U000F128C", + "toy-brick-outline": "\U000F128D", + "toy-brick-plus": "\U000F128E", + "toy-brick-plus-outline": "\U000F128F", + "toy-brick-remove": "\U000F1290", + "toy-brick-remove-outline": "\U000F1291", + "toy-brick-search": "\U000F1292", + "toy-brick-search-outline": "\U000F1293", + "track-light": "\U000F0914", + "trackpad": "\U000F07F8", + "trackpad-lock": "\U000F0933", + "tractor": "\U000F0892", + "tractor-variant": "\U000F14C4", + "trademark": "\U000F0A78", + "traffic-cone": "\U000F137C", + "traffic-light": "\U000F052B", + "train": "\U000F052C", + "train-car": "\U000F0BD8", + "train-variant": "\U000F08C4", + "tram": "\U000F052D", + "tram-side": "\U000F0FE7", + "transcribe": "\U000F052E", + "transcribe-close": "\U000F052F", + "transfer": "\U000F1065", + "transfer-down": "\U000F0DA1", + "transfer-left": "\U000F0DA2", + "transfer-right": "\U000F0530", + "transfer-up": "\U000F0DA3", + "transit-connection": "\U000F0D3C", + "transit-connection-horizontal": "\U000F1546", + "transit-connection-variant": "\U000F0D3D", + "transit-detour": "\U000F0F8B", + "transit-skip": "\U000F1515", + "transit-transfer": "\U000F06AE", + "transition": "\U000F0915", + "transition-masked": "\U000F0916", + "translate": "\U000F05CA", + "translate-off": "\U000F0E06", + "transmission-tower": "\U000F0D3E", + "trash-can": "\U000F0A79", + "trash-can-outline": "\U000F0A7A", + "tray": "\U000F1294", + "tray-alert": "\U000F1295", + "tray-full": "\U000F1296", + "tray-minus": "\U000F1297", + "tray-plus": "\U000F1298", + "tray-remove": "\U000F1299", + "treasure-chest": "\U000F0726", + "tree": "\U000F0531", + "tree-outline": "\U000F0E69", + "trello": "\U000F0532", + "trending-down": "\U000F0533", + "trending-neutral": "\U000F0534", + "trending-up": "\U000F0535", + "triangle": "\U000F0536", + "triangle-outline": "\U000F0537", + "triangle-wave": "\U000F147C", + "triforce": "\U000F0BD9", + "trophy": "\U000F0538", + "trophy-award": "\U000F0539", + "trophy-broken": "\U000F0DA4", + "trophy-outline": "\U000F053A", + "trophy-variant": "\U000F053B", + "trophy-variant-outline": "\U000F053C", + "truck": "\U000F053D", + "truck-check": "\U000F0CD4", + "truck-check-outline": "\U000F129A", + "truck-delivery": "\U000F053E", + "truck-delivery-outline": "\U000F129B", + "truck-fast": "\U000F0788", + "truck-fast-outline": "\U000F129C", + "truck-outline": "\U000F129D", + "truck-trailer": "\U000F0727", + "trumpet": "\U000F1096", + "tshirt-crew": "\U000F0A7B", + "tshirt-crew-outline": "\U000F053F", + "tshirt-v": "\U000F0A7C", + "tshirt-v-outline": "\U000F0540", + "tumble-dryer": "\U000F0917", + "tumble-dryer-alert": "\U000F11BA", + "tumble-dryer-off": "\U000F11BB", + "tune": "\U000F062E", + "tune-variant": "\U000F1542", + "tune-vertical": "\U000F066A", + "tune-vertical-variant": "\U000F1543", + "turnstile": "\U000F0CD5", + "turnstile-outline": "\U000F0CD6", + "turtle": "\U000F0CD7", + "twitch": "\U000F0543", + "twitter": "\U000F0544", + "twitter-retweet": "\U000F0547", + "two-factor-authentication": "\U000F09AF", + "typewriter": "\U000F0F2D", + "ubisoft": "\U000F0BDA", + "ubuntu": "\U000F0548", + "ufo": "\U000F10C4", + "ufo-outline": "\U000F10C5", + "ultra-high-definition": "\U000F07F9", + "umbraco": "\U000F0549", + "umbrella": "\U000F054A", + "umbrella-closed": "\U000F09B0", + "umbrella-closed-outline": "\U000F13E2", + "umbrella-closed-variant": "\U000F13E1", + "umbrella-outline": "\U000F054B", + "undo": "\U000F054C", + "undo-variant": "\U000F054D", + "unfold-less-horizontal": "\U000F054E", + "unfold-less-vertical": "\U000F0760", + "unfold-more-horizontal": "\U000F054F", + "unfold-more-vertical": "\U000F0761", + "ungroup": "\U000F0550", + "unicode": "\U000F0ED0", + "unicorn": "\U000F15C2", + "unicorn-variant": "\U000F15C3", + "unicycle": "\U000F15E5", + "unity": "\U000F06AF", + "unreal": "\U000F09B1", + "untappd": "\U000F0551", + "update": "\U000F06B0", + "upload": "\U000F0552", + "upload-lock": "\U000F1373", + "upload-lock-outline": "\U000F1374", + "upload-multiple": "\U000F083D", + "upload-network": "\U000F06F6", + "upload-network-outline": "\U000F0CD8", + "upload-off": "\U000F10C6", + "upload-off-outline": "\U000F10C7", + "upload-outline": "\U000F0E07", + "usb": "\U000F0553", + "usb-flash-drive": "\U000F129E", + "usb-flash-drive-outline": "\U000F129F", + "usb-port": "\U000F11F0", + "valve": "\U000F1066", + "valve-closed": "\U000F1067", + "valve-open": "\U000F1068", + "van-passenger": "\U000F07FA", + "van-utility": "\U000F07FB", + "vanish": "\U000F07FC", + "vanish-quarter": "\U000F1554", + "vanity-light": "\U000F11E1", + "variable": "\U000F0AE7", + "variable-box": "\U000F1111", + "vector-arrange-above": "\U000F0554", + "vector-arrange-below": "\U000F0555", + "vector-bezier": "\U000F0AE8", + "vector-circle": "\U000F0556", + "vector-circle-variant": "\U000F0557", + "vector-combine": "\U000F0558", + "vector-curve": "\U000F0559", + "vector-difference": "\U000F055A", + "vector-difference-ab": "\U000F055B", + "vector-difference-ba": "\U000F055C", + "vector-ellipse": "\U000F0893", + "vector-intersection": "\U000F055D", + "vector-line": "\U000F055E", + "vector-link": "\U000F0FE8", + "vector-point": "\U000F055F", + "vector-polygon": "\U000F0560", + "vector-polyline": "\U000F0561", + "vector-polyline-edit": "\U000F1225", + "vector-polyline-minus": "\U000F1226", + "vector-polyline-plus": "\U000F1227", + "vector-polyline-remove": "\U000F1228", + "vector-radius": "\U000F074A", + "vector-rectangle": "\U000F05C6", + "vector-selection": "\U000F0562", + "vector-square": "\U000F0001", + "vector-triangle": "\U000F0563", + "vector-union": "\U000F0564", + "vhs": "\U000F0A1B", + "vibrate": "\U000F0566", + "vibrate-off": "\U000F0CD9", + "video": "\U000F0567", + "video-3d": "\U000F07FD", + "video-3d-off": "\U000F13D9", + "video-3d-variant": "\U000F0ED1", + "video-4k-box": "\U000F083E", + "video-account": "\U000F0919", + "video-box": "\U000F00FD", + "video-box-off": "\U000F00FE", + "video-check": "\U000F1069", + "video-check-outline": "\U000F106A", + "video-high-definition": "\U000F152E", + "video-image": "\U000F091A", + "video-input-antenna": "\U000F083F", + "video-input-component": "\U000F0840", + "video-input-hdmi": "\U000F0841", + "video-input-scart": "\U000F0F8C", + "video-input-svideo": "\U000F0842", + "video-minus": "\U000F09B2", + "video-minus-outline": "\U000F02BA", + "video-off": "\U000F0568", + "video-off-outline": "\U000F0BDB", + "video-outline": "\U000F0BDC", + "video-plus": "\U000F09B3", + "video-plus-outline": "\U000F01D3", + "video-stabilization": "\U000F091B", + "video-switch": "\U000F0569", + "video-switch-outline": "\U000F0790", + "video-vintage": "\U000F0A1C", + "video-wireless": "\U000F0ED2", + "video-wireless-outline": "\U000F0ED3", + "view-agenda": "\U000F056A", + "view-agenda-outline": "\U000F11D8", + "view-array": "\U000F056B", + "view-array-outline": "\U000F1485", + "view-carousel": "\U000F056C", + "view-carousel-outline": "\U000F1486", + "view-column": "\U000F056D", + "view-column-outline": "\U000F1487", + "view-comfy": "\U000F0E6A", + "view-comfy-outline": "\U000F1488", + "view-compact": "\U000F0E6B", + "view-compact-outline": "\U000F0E6C", + "view-dashboard": "\U000F056E", + "view-dashboard-outline": "\U000F0A1D", + "view-dashboard-variant": "\U000F0843", + "view-dashboard-variant-outline": "\U000F1489", + "view-day": "\U000F056F", + "view-day-outline": "\U000F148A", + "view-grid": "\U000F0570", + "view-grid-outline": "\U000F11D9", + "view-grid-plus": "\U000F0F8D", + "view-grid-plus-outline": "\U000F11DA", + "view-headline": "\U000F0571", + "view-list": "\U000F0572", + "view-list-outline": "\U000F148B", + "view-module": "\U000F0573", + "view-module-outline": "\U000F148C", + "view-parallel": "\U000F0728", + "view-parallel-outline": "\U000F148D", + "view-quilt": "\U000F0574", + "view-quilt-outline": "\U000F148E", + "view-sequential": "\U000F0729", + "view-sequential-outline": "\U000F148F", + "view-split-horizontal": "\U000F0BCB", + "view-split-vertical": "\U000F0BCC", + "view-stream": "\U000F0575", + "view-stream-outline": "\U000F1490", + "view-week": "\U000F0576", + "view-week-outline": "\U000F1491", + "vimeo": "\U000F0577", + "violin": "\U000F060F", + "virtual-reality": "\U000F0894", + "virus": "\U000F13B6", + "virus-outline": "\U000F13B7", + "vk": "\U000F0579", + "vlc": "\U000F057C", + "voice-off": "\U000F0ED4", + "voicemail": "\U000F057D", + "volleyball": "\U000F09B4", + "volume-high": "\U000F057E", + "volume-low": "\U000F057F", + "volume-medium": "\U000F0580", + "volume-minus": "\U000F075E", + "volume-mute": "\U000F075F", + "volume-off": "\U000F0581", + "volume-plus": "\U000F075D", + "volume-source": "\U000F1120", + "volume-variant-off": "\U000F0E08", + "volume-vibrate": "\U000F1121", + "vote": "\U000F0A1F", + "vote-outline": "\U000F0A20", + "vpn": "\U000F0582", + "vuejs": "\U000F0844", + "vuetify": "\U000F0E6D", + "walk": "\U000F0583", + "wall": "\U000F07FE", + "wall-sconce": "\U000F091C", + "wall-sconce-flat": "\U000F091D", + "wall-sconce-flat-variant": "\U000F041C", + "wall-sconce-round": "\U000F0748", + "wall-sconce-round-variant": "\U000F091E", + "wallet": "\U000F0584", + "wallet-giftcard": "\U000F0585", + "wallet-membership": "\U000F0586", + "wallet-outline": "\U000F0BDD", + "wallet-plus": "\U000F0F8E", + "wallet-plus-outline": "\U000F0F8F", + "wallet-travel": "\U000F0587", + "wallpaper": "\U000F0E09", + "wan": "\U000F0588", + "wardrobe": "\U000F0F90", + "wardrobe-outline": "\U000F0F91", + "warehouse": "\U000F0F81", + "washing-machine": "\U000F072A", + "washing-machine-alert": "\U000F11BC", + "washing-machine-off": "\U000F11BD", + "watch": "\U000F0589", + "watch-export": "\U000F058A", + "watch-export-variant": "\U000F0895", + "watch-import": "\U000F058B", + "watch-import-variant": "\U000F0896", + "watch-variant": "\U000F0897", + "watch-vibrate": "\U000F06B1", + "watch-vibrate-off": "\U000F0CDA", + "water": "\U000F058C", + "water-alert": "\U000F1502", + "water-alert-outline": "\U000F1503", + "water-boiler": "\U000F0F92", + "water-boiler-alert": "\U000F11B3", + "water-boiler-off": "\U000F11B4", + "water-check": "\U000F1504", + "water-check-outline": "\U000F1505", + "water-minus": "\U000F1506", + "water-minus-outline": "\U000F1507", + "water-off": "\U000F058D", + "water-off-outline": "\U000F1508", + "water-outline": "\U000F0E0A", + "water-percent": "\U000F058E", + "water-percent-alert": "\U000F1509", + "water-plus": "\U000F150A", + "water-plus-outline": "\U000F150B", + "water-polo": "\U000F12A0", + "water-pump": "\U000F058F", + "water-pump-off": "\U000F0F93", + "water-remove": "\U000F150C", + "water-remove-outline": "\U000F150D", + "water-well": "\U000F106B", + "water-well-outline": "\U000F106C", + "watering-can": "\U000F1481", + "watering-can-outline": "\U000F1482", + "watermark": "\U000F0612", + "wave": "\U000F0F2E", + "waveform": "\U000F147D", + "waves": "\U000F078D", + "waze": "\U000F0BDE", + "weather-cloudy": "\U000F0590", + "weather-cloudy-alert": "\U000F0F2F", + "weather-cloudy-arrow-right": "\U000F0E6E", + "weather-fog": "\U000F0591", + "weather-hail": "\U000F0592", + "weather-hazy": "\U000F0F30", + "weather-hurricane": "\U000F0898", + "weather-lightning": "\U000F0593", + "weather-lightning-rainy": "\U000F067E", + "weather-night": "\U000F0594", + "weather-night-partly-cloudy": "\U000F0F31", + "weather-partly-cloudy": "\U000F0595", + "weather-partly-lightning": "\U000F0F32", + "weather-partly-rainy": "\U000F0F33", + "weather-partly-snowy": "\U000F0F34", + "weather-partly-snowy-rainy": "\U000F0F35", + "weather-pouring": "\U000F0596", + "weather-rainy": "\U000F0597", + "weather-snowy": "\U000F0598", + "weather-snowy-heavy": "\U000F0F36", + "weather-snowy-rainy": "\U000F067F", + "weather-sunny": "\U000F0599", + "weather-sunny-alert": "\U000F0F37", + "weather-sunny-off": "\U000F14E4", + "weather-sunset": "\U000F059A", + "weather-sunset-down": "\U000F059B", + "weather-sunset-up": "\U000F059C", + "weather-tornado": "\U000F0F38", + "weather-windy": "\U000F059D", + "weather-windy-variant": "\U000F059E", + "web": "\U000F059F", + "web-box": "\U000F0F94", + "web-clock": "\U000F124A", + "webcam": "\U000F05A0", + "webhook": "\U000F062F", + "webpack": "\U000F072B", + "webrtc": "\U000F1248", + "wechat": "\U000F0611", + "weight": "\U000F05A1", + "weight-gram": "\U000F0D3F", + "weight-kilogram": "\U000F05A2", + "weight-lifter": "\U000F115D", + "weight-pound": "\U000F09B5", + "whatsapp": "\U000F05A3", + "wheel-barrow": "\U000F14F2", + "wheelchair-accessibility": "\U000F05A4", + "whistle": "\U000F09B6", + "whistle-outline": "\U000F12BC", + "white-balance-auto": "\U000F05A5", + "white-balance-incandescent": "\U000F05A6", + "white-balance-iridescent": "\U000F05A7", + "white-balance-sunny": "\U000F05A8", + "widgets": "\U000F072C", + "widgets-outline": "\U000F1355", + "wifi": "\U000F05A9", + "wifi-off": "\U000F05AA", + "wifi-star": "\U000F0E0B", + "wifi-strength-1": "\U000F091F", + "wifi-strength-1-alert": "\U000F0920", + "wifi-strength-1-lock": "\U000F0921", + "wifi-strength-2": "\U000F0922", + "wifi-strength-2-alert": "\U000F0923", + "wifi-strength-2-lock": "\U000F0924", + "wifi-strength-3": "\U000F0925", + "wifi-strength-3-alert": "\U000F0926", + "wifi-strength-3-lock": "\U000F0927", + "wifi-strength-4": "\U000F0928", + "wifi-strength-4-alert": "\U000F0929", + "wifi-strength-4-lock": "\U000F092A", + "wifi-strength-alert-outline": "\U000F092B", + "wifi-strength-lock-outline": "\U000F092C", + "wifi-strength-off": "\U000F092D", + "wifi-strength-off-outline": "\U000F092E", + "wifi-strength-outline": "\U000F092F", + "wikipedia": "\U000F05AC", + "wind-turbine": "\U000F0DA5", + "window-close": "\U000F05AD", + "window-closed": "\U000F05AE", + "window-closed-variant": "\U000F11DB", + "window-maximize": "\U000F05AF", + "window-minimize": "\U000F05B0", + "window-open": "\U000F05B1", + "window-open-variant": "\U000F11DC", + "window-restore": "\U000F05B2", + "window-shutter": "\U000F111C", + "window-shutter-alert": "\U000F111D", + "window-shutter-open": "\U000F111E", + "windsock": "\U000F15FA", + "wiper": "\U000F0AE9", + "wiper-wash": "\U000F0DA6", + "wizard-hat": "\U000F1477", + "wordpress": "\U000F05B4", + "wrap": "\U000F05B6", + "wrap-disabled": "\U000F0BDF", + "wrench": "\U000F05B7", + "wrench-outline": "\U000F0BE0", + "xamarin": "\U000F0845", + "xamarin-outline": "\U000F0846", + "xing": "\U000F05BE", + "xml": "\U000F05C0", + "xmpp": "\U000F07FF", + "y-combinator": "\U000F0624", + "yahoo": "\U000F0B4F", + "yeast": "\U000F05C1", + "yin-yang": "\U000F0680", + "yoga": "\U000F117C", + "youtube": "\U000F05C3", + "youtube-gaming": "\U000F0848", + "youtube-studio": "\U000F0847", + "youtube-subscription": "\U000F0D40", + "youtube-tv": "\U000F0448", + "yurt": "\U000F1516", + "z-wave": "\U000F0AEA", + "zend": "\U000F0AEB", + "zigbee": "\U000F0D41", + "zip-box": "\U000F05C4", + "zip-box-outline": "\U000F0FFA", + "zip-disk": "\U000F0A23", + "zodiac-aquarius": "\U000F0A7D", + "zodiac-aries": "\U000F0A7E", + "zodiac-cancer": "\U000F0A7F", + "zodiac-capricorn": "\U000F0A80", + "zodiac-gemini": "\U000F0A81", + "zodiac-leo": "\U000F0A82", + "zodiac-libra": "\U000F0A83", + "zodiac-pisces": "\U000F0A84", + "zodiac-sagittarius": "\U000F0A85", + "zodiac-scorpio": "\U000F0A86", + "zodiac-taurus": "\U000F0A87", + "zodiac-virgo": "\U000F0A88", + "blank": " ", +} diff --git a/kivymd/images/folder.png b/kivymd/images/folder.png new file mode 100644 index 0000000..58fed42 Binary files /dev/null and b/kivymd/images/folder.png differ diff --git a/kivymd/images/quad_shadow-0.png b/kivymd/images/quad_shadow-0.png new file mode 100644 index 0000000..5d64fde Binary files /dev/null and b/kivymd/images/quad_shadow-0.png differ diff --git a/kivymd/images/quad_shadow-1.png b/kivymd/images/quad_shadow-1.png new file mode 100644 index 0000000..c0f1e22 Binary files /dev/null and b/kivymd/images/quad_shadow-1.png differ diff --git a/kivymd/images/quad_shadow-2.png b/kivymd/images/quad_shadow-2.png new file mode 100644 index 0000000..44619e5 Binary files /dev/null and b/kivymd/images/quad_shadow-2.png differ diff --git a/kivymd/images/quad_shadow.atlas b/kivymd/images/quad_shadow.atlas new file mode 100644 index 0000000..68e0aad --- /dev/null +++ b/kivymd/images/quad_shadow.atlas @@ -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]}} \ No newline at end of file diff --git a/kivymd/images/rec_shadow-0.png b/kivymd/images/rec_shadow-0.png new file mode 100644 index 0000000..f02b919 Binary files /dev/null and b/kivymd/images/rec_shadow-0.png differ diff --git a/kivymd/images/rec_shadow-1.png b/kivymd/images/rec_shadow-1.png new file mode 100644 index 0000000..f752fd2 Binary files /dev/null and b/kivymd/images/rec_shadow-1.png differ diff --git a/kivymd/images/rec_shadow.atlas b/kivymd/images/rec_shadow.atlas new file mode 100644 index 0000000..71b0e9d --- /dev/null +++ b/kivymd/images/rec_shadow.atlas @@ -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]}} \ No newline at end of file diff --git a/kivymd/images/rec_st_shadow-0.png b/kivymd/images/rec_st_shadow-0.png new file mode 100644 index 0000000..887327d Binary files /dev/null and b/kivymd/images/rec_st_shadow-0.png differ diff --git a/kivymd/images/rec_st_shadow-1.png b/kivymd/images/rec_st_shadow-1.png new file mode 100644 index 0000000..759ee65 Binary files /dev/null and b/kivymd/images/rec_st_shadow-1.png differ diff --git a/kivymd/images/rec_st_shadow-2.png b/kivymd/images/rec_st_shadow-2.png new file mode 100644 index 0000000..e9fdacc Binary files /dev/null and b/kivymd/images/rec_st_shadow-2.png differ diff --git a/kivymd/images/rec_st_shadow.atlas b/kivymd/images/rec_st_shadow.atlas new file mode 100644 index 0000000..d4c24ab --- /dev/null +++ b/kivymd/images/rec_st_shadow.atlas @@ -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]}} \ No newline at end of file diff --git a/kivymd/images/round_shadow-0.png b/kivymd/images/round_shadow-0.png new file mode 100644 index 0000000..26d9840 Binary files /dev/null and b/kivymd/images/round_shadow-0.png differ diff --git a/kivymd/images/round_shadow-1.png b/kivymd/images/round_shadow-1.png new file mode 100644 index 0000000..d0f4c0f Binary files /dev/null and b/kivymd/images/round_shadow-1.png differ diff --git a/kivymd/images/round_shadow-2.png b/kivymd/images/round_shadow-2.png new file mode 100644 index 0000000..d5feef2 Binary files /dev/null and b/kivymd/images/round_shadow-2.png differ diff --git a/kivymd/images/round_shadow.atlas b/kivymd/images/round_shadow.atlas new file mode 100644 index 0000000..f25016d --- /dev/null +++ b/kivymd/images/round_shadow.atlas @@ -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]}} \ No newline at end of file diff --git a/kivymd/images/transparent.png b/kivymd/images/transparent.png new file mode 100644 index 0000000..effe1c2 Binary files /dev/null and b/kivymd/images/transparent.png differ diff --git a/kivymd/material_resources.py b/kivymd/material_resources.py new file mode 100755 index 0000000..0a1b811 --- /dev/null +++ b/kivymd/material_resources.py @@ -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) diff --git a/kivymd/stiffscroll/LICENSE b/kivymd/stiffscroll/LICENSE new file mode 100644 index 0000000..a23e5d8 --- /dev/null +++ b/kivymd/stiffscroll/LICENSE @@ -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. diff --git a/kivymd/stiffscroll/README.md b/kivymd/stiffscroll/README.md new file mode 100644 index 0000000..6730324 --- /dev/null +++ b/kivymd/stiffscroll/README.md @@ -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 \ No newline at end of file diff --git a/kivymd/stiffscroll/__init__.py b/kivymd/stiffscroll/__init__.py new file mode 100644 index 0000000..72a63e9 --- /dev/null +++ b/kivymd/stiffscroll/__init__.py @@ -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) diff --git a/kivymd/tests/test_app.py b/kivymd/tests/test_app.py new file mode 100644 index 0000000..076e9dd --- /dev/null +++ b/kivymd/tests/test_app.py @@ -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) diff --git a/kivymd/tests/test_font_definitions.py b/kivymd/tests/test_font_definitions.py new file mode 100644 index 0000000..d39cee7 --- /dev/null +++ b/kivymd/tests/test_font_definitions.py @@ -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() diff --git a/kivymd/tests/test_icon_definitions.py b/kivymd/tests/test_icon_definitions.py new file mode 100644 index 0000000..6da858f --- /dev/null +++ b/kivymd/tests/test_icon_definitions.py @@ -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 diff --git a/kivymd/theming.py b/kivymd/theming.py new file mode 100755 index 0000000..fbcd6f0 --- /dev/null +++ b/kivymd/theming.py @@ -0,0 +1,908 @@ +""" +Themes/Theming +============== + +.. seealso:: + + `Material Design spec, 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 = ''' + : + 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) diff --git a/kivymd/theming_dynamic_text.py b/kivymd/theming_dynamic_text.py new file mode 100755 index 0000000..bc9050b --- /dev/null +++ b/kivymd/theming_dynamic_text.py @@ -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" + ) diff --git a/kivymd/toast/LICENSE b/kivymd/toast/LICENSE new file mode 100755 index 0000000..b18e56d --- /dev/null +++ b/kivymd/toast/LICENSE @@ -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. \ No newline at end of file diff --git a/kivymd/toast/README.md b/kivymd/toast/README.md new file mode 100755 index 0000000..4d8abab --- /dev/null +++ b/kivymd/toast/README.md @@ -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. + + + +The package modules are written using the framework for cross-platform development of . +Information about the 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 \ No newline at end of file diff --git a/kivymd/toast/__init__.py b/kivymd/toast/__init__.py new file mode 100755 index 0000000..2b823a6 --- /dev/null +++ b/kivymd/toast/__init__.py @@ -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 diff --git a/kivymd/toast/androidtoast/__init__.py b/kivymd/toast/androidtoast/__init__.py new file mode 100755 index 0000000..300dd2d --- /dev/null +++ b/kivymd/toast/androidtoast/__init__.py @@ -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 diff --git a/kivymd/toast/androidtoast/androidtoast.py b/kivymd/toast/androidtoast/androidtoast.py new file mode 100755 index 0000000..57e95a5 --- /dev/null +++ b/kivymd/toast/androidtoast/androidtoast.py @@ -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() diff --git a/kivymd/toast/kivytoast/__init__.py b/kivymd/toast/kivytoast/__init__.py new file mode 100755 index 0000000..0d9c204 --- /dev/null +++ b/kivymd/toast/kivytoast/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("toast",) + +from .kivytoast import toast diff --git a/kivymd/toast/kivytoast/kivytoast.py b/kivymd/toast/kivytoast/kivytoast.py new file mode 100755 index 0000000..9c020c0 --- /dev/null +++ b/kivymd/toast/kivytoast/kivytoast.py @@ -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( + """ +: + 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) diff --git a/kivymd/tools/__init__.py b/kivymd/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kivymd/tools/packaging/__init__.py b/kivymd/tools/packaging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kivymd/tools/packaging/pyinstaller/__init__.py b/kivymd/tools/packaging/pyinstaller/__init__.py new file mode 100644 index 0000000..00497b2 --- /dev/null +++ b/kivymd/tools/packaging/pyinstaller/__init__.py @@ -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"] diff --git a/kivymd/tools/packaging/pyinstaller/hook-kivymd.py b/kivymd/tools/packaging/pyinstaller/hook-kivymd.py new file mode 100644 index 0000000..bfad6f4 --- /dev/null +++ b/kivymd/tools/packaging/pyinstaller/hook-kivymd.py @@ -0,0 +1 @@ +from kivymd.tools.packaging.pyinstaller import * # NOQA diff --git a/kivymd/tools/release/__init__.py b/kivymd/tools/release/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kivymd/tools/release/argument_parser.py b/kivymd/tools/release/argument_parser.py new file mode 100644 index 0000000..3de6da9 --- /dev/null +++ b/kivymd/tools/release/argument_parser.py @@ -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() diff --git a/kivymd/tools/release/git_commands.py b/kivymd/tools/release/git_commands.py new file mode 100644 index 0000000..fa873a5 --- /dev/null +++ b/kivymd/tools/release/git_commands.py @@ -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) diff --git a/kivymd/tools/release/make_release.py b/kivymd/tools/release/make_release.py new file mode 100644 index 0000000..74f254c --- /dev/null +++ b/kivymd/tools/release/make_release.py @@ -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 .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 `_ | `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() diff --git a/kivymd/tools/release/update_icons.py b/kivymd/tools/release/update_icons.py new file mode 100644 index 0000000..1d0b003 --- /dev/null +++ b/kivymd/tools/release/update_icons.py @@ -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"(?<=)[\d.]+(?=)") +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() diff --git a/kivymd/uix/__init__.py b/kivymd/uix/__init__.py new file mode 100755 index 0000000..e85f876 --- /dev/null +++ b/kivymd/uix/__init__.py @@ -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")) diff --git a/kivymd/uix/backdrop.py b/kivymd/uix/backdrop.py new file mode 100644 index 0000000..d101908 --- /dev/null +++ b/kivymd/uix/backdrop.py @@ -0,0 +1,395 @@ +""" +Components/Backdrop +=================== + +.. seealso:: + + `Material Design spec, 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 + + : + + 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 + + + + icon: "android" + + IconLeftWidget: + icon: root.icon + + + + 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 + + + + 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( + ''' + + + 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 `_ +""" + +__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( + """ + + + 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 diff --git a/kivymd/uix/banner.py b/kivymd/uix/banner.py new file mode 100644 index 0000000..d17706f --- /dev/null +++ b/kivymd/uix/banner.py @@ -0,0 +1,455 @@ +""" +Components/Banner +================= + +.. seealso:: + + `Material Design spec, Banner `_ + +.. 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(''' + + + 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 `_ +""" + +__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 + + + + 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 + + + + text: root.text_message[0] + secondary_text: root.text_message[1] + divider: None + _no_ripple_effect: True + + ImageLeftWidget: + source: root.icon + + + + text: root.text_message[0] + divider: None + _no_ripple_effect: True + + ImageLeftWidget: + source: root.icon + + + + text: root.text_message[0] + secondary_text: root.text_message[1] + tertiary_text: root.text_message[2] + divider: None + _no_ripple_effect: True + + + + text: root.text_message[0] + secondary_text: root.text_message[1] + divider: None + _no_ripple_effect: True + + + + text: root.text_message[0] + divider: None + _no_ripple_effect: True + + + + 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 diff --git a/kivymd/uix/behaviors/__init__.py b/kivymd/uix/behaviors/__init__.py new file mode 100755 index 0000000..96fb377 --- /dev/null +++ b/kivymd/uix/behaviors/__init__.py @@ -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 diff --git a/kivymd/uix/behaviors/backgroundcolorbehavior.py b/kivymd/uix/behaviors/backgroundcolorbehavior.py new file mode 100755 index 0000000..14921a5 --- /dev/null +++ b/kivymd/uix/behaviors/backgroundcolorbehavior.py @@ -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 + + + + 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 + + + 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 diff --git a/kivymd/uix/behaviors/elevation.py b/kivymd/uix/behaviors/elevation.py new file mode 100755 index 0000000..6a2ea4b --- /dev/null +++ b/kivymd/uix/behaviors/elevation.py @@ -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 = ''' + : + 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 + + + : + 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( + """ + + 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 + + + + 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 diff --git a/kivymd/uix/behaviors/focus_behavior.py b/kivymd/uix/behaviors/focus_behavior.py new file mode 100644 index 0000000..c553ce8 --- /dev/null +++ b/kivymd/uix/behaviors/focus_behavior.py @@ -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 diff --git a/kivymd/uix/behaviors/hover_behavior.py b/kivymd/uix/behaviors/hover_behavior.py new file mode 100644 index 0000000..d1a2942 --- /dev/null +++ b/kivymd/uix/behaviors/hover_behavior.py @@ -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 + + + +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) diff --git a/kivymd/uix/behaviors/magic_behavior.py b/kivymd/uix/behaviors/magic_behavior.py new file mode 100644 index 0000000..e88e749 --- /dev/null +++ b/kivymd/uix/behaviors/magic_behavior.py @@ -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 + + + +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 + + + + + + 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( + """ + + 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) diff --git a/kivymd/uix/behaviors/ripplebehavior.py b/kivymd/uix/behaviors/ripplebehavior.py new file mode 100755 index 0000000..5e7aac5 --- /dev/null +++ b/kivymd/uix/behaviors/ripplebehavior.py @@ -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, + ) diff --git a/kivymd/uix/behaviors/toggle_behavior.py b/kivymd/uix/behaviors/toggle_behavior.py new file mode 100644 index 0000000..265496f --- /dev/null +++ b/kivymd/uix/behaviors/toggle_behavior.py @@ -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 diff --git a/kivymd/uix/behaviors/touch_behavior.py b/kivymd/uix/behaviors/touch_behavior.py new file mode 100644 index 0000000..ce15987 --- /dev/null +++ b/kivymd/uix/behaviors/touch_behavior.py @@ -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(" event") + + def on_double_tap(self, *args): + print(" event") + + def on_triple_tap(self, *args): + print(" 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.""" diff --git a/kivymd/uix/bottomnavigation.py b/kivymd/uix/bottomnavigation.py new file mode 100755 index 0000000..e756289 --- /dev/null +++ b/kivymd/uix/bottomnavigation.py @@ -0,0 +1,646 @@ +""" +Components/Bottom Navigation +============================ + +.. seealso:: + + `Material Design spec, 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 + + >: + + 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 + + >: + + 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 `_ + + `See full example `_ +""" + +__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 + + + + 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 + + + + 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} + + + + 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"" + + +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 diff --git a/kivymd/uix/bottomsheet.py b/kivymd/uix/bottomsheet.py new file mode 100755 index 0000000..03b88dc --- /dev/null +++ b/kivymd/uix/bottomsheet.py @@ -0,0 +1,564 @@ +""" +Components/Bottom Sheet +======================= + +.. seealso:: + + `Material Design spec, 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 = ''' + + on_press: app.custom_sheet.dismiss() + icon: "" + + IconLeftWidget: + icon: root.icon + + + : + 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 + + : + 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 + + +: + + MDGridLayout: + id: box_sheet_list + cols: 1 + adaptive_height: True + padding: 0, 0, 0, "96dp" + + + + 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( + """ + + 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( + """ + + 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) diff --git a/kivymd/uix/boxlayout.py b/kivymd/uix/boxlayout.py new file mode 100644 index 0000000..b34edc4 --- /dev/null +++ b/kivymd/uix/boxlayout.py @@ -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 diff --git a/kivymd/uix/button.py b/kivymd/uix/button.py new file mode 100755 index 0000000..6b311d5 --- /dev/null +++ b/kivymd/uix/button.py @@ -0,0 +1,1842 @@ +""" +Components/Button +================= + +.. seealso:: + + `Material Design spec, Buttons `_ + + `Material Design spec, Buttons: floating action button `_ + +.. rubric:: Buttons allow users to take actions, and make choices, + with a single tap. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/buttons.png + :align: center + +`KivyMD` provides the following button classes for use: + +- MDIconButton_ +- MDFloatingActionButton_ +- MDFlatButton_ +- MDRaisedButton_ +- MDRectangleFlatButton_ +- MDRectangleFlatIconButton_ +- MDRoundFlatButton_ +- MDRoundFlatIconButton_ +- MDFillRoundFlatButton_ +- MDFillRoundFlatIconButton_ +- MDTextButton_ +- MDFloatingActionButtonSpeedDial_ + +.. MDIconButton: +MDIconButton +------------ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-button.gif + :align: center + +.. code-block:: python + +.. MDIconButton: +MDIconButton +------------ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-button.gif + :align: center + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + Screen: + + MDIconButton: + icon: "language-python" + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + + Example().run() + +The :class:`~MDIconButton.icon` parameter must have the name of the icon +from ``kivymd/icon_definitions.py`` file. + +You can also use custom icons: + +.. code-block:: kv + + MDIconButton: + icon: "data/logo/kivy-icon-256.png" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-custom-button.gif + :align: center + +By default, :class:`~MDIconButton` button has a size ``(dp(48), dp (48))``. +Use :class:`~BaseButton.user_font_size` attribute to resize the button: + +.. code-block:: kv + + MDIconButton: + icon: "android" + user_font_size: "64sp" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-button-user-font-size.gif + :align: center + +By default, the color of :class:`~MDIconButton` +(depending on the style of the application) is black or white. +You can change the color of :class:`~MDIconButton` as the text color +of :class:`~kivymd.uix.label.MDLabel`: + +.. code-block:: kv + + MDIconButton: + icon: "android" + theme_text_color: "Custom" + text_color: app.theme_cls.primary_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-button-theme-text-color.png + :align: center + +.. MDFloatingActionButton: +MDFloatingActionButton +---------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-floating-action-button.png + :align: center + +The above parameters for :class:`~MDIconButton` apply +to :class:`~MDFloatingActionButton`. + +To change :class:`~MDFloatingActionButton` background, use the +``md_bg_color`` parameter: + +.. code-block:: kv + + MDFloatingActionButton: + icon: "android" + md_bg_color: app.theme_cls.primary_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-floating-action-button-md-bg-color.png + :align: center + +The length of the shadow is controlled by the ``elevation_normal`` parameter: + +.. code-block:: kv + + MDFloatingActionButton: + icon: "android" + elevation_normal: 12 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-floating-action-button-elevation-normal.png + :align: center + + +.. MDFlatButton: +MDFlatButton +------------ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-flat-button.gif + :align: center + +To change the text color of: class:`~MDFlatButton` use the ``text_color`` parameter: + +.. code-block:: kv + + MDFlatButton: + text: "MDFLATBUTTON" + text_color: 0, 0, 1, 1 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-flat-button-text-color.png + :align: center + +Or use markup: + +.. code-block:: kv + + MDFlatButton: + text: "[color=#00ffcc]MDFLATBUTTON[/color]" + markup: True + +To specify the font size and font name, use the parameters as in the usual +`Kivy` buttons: + +.. code-block:: kv + + MDFlatButton: + text: "MDFLATBUTTON" + font_size: "18sp" + font_name: "path/to/font" + +.. warning:: You cannot use the ``size_hint_x`` parameter for `KivyMD` buttons + (the width of the buttons is set automatically)! + +.. MDRaisedButton: +MDRaisedButton +-------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-raised-button.gif + :align: center + +This button is similar to the :class:`~MDFlatButton` button except that you +can set the background color for :class:`~MDRaisedButton`: + +.. code-block:: kv + + MDRaisedButton: + text: "MDRAISEDBUTTON" + md_bg_color: 1, 0, 1, 1 + + +.. MDRectangleFlatButton: +MDRectangleFlatButton +--------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-rectangle-flat-button.gif + :align: center + +Button parameters :class:`~MDRectangleFlatButton` are the same as +button :class:`~MDRaisedButton`: + +.. code-block:: kv + + MDRectangleFlatButton: + text: "MDRECTANGLEFLATBUTTON" + text_color: 0, 0, 1, 1 + md_bg_color: 1, 1, 0, 1 + +.. note:: Note that the frame color will be the same as the text color. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-rectangle-flat-button-md-bg-color.png + :align: center + +.. MDRectangleFlatIconButton: +MDRectangleFlatIconButton +--------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-rectangle-flat-icon-button.png + :align: center + +Button parameters :class:`~MDRectangleFlatButton` are the same as +button :class:`~MDRectangleFlatButton`: + +.. code-block:: kv + + MDRectangleFlatIconButton: + icon: "android" + text: "MDRECTANGLEFLATICONBUTTON" + +Without border +-------------- + +.. code-block:: python + + from kivy.uix.screenmanager import Screen + + from kivymd.app import MDApp + from kivymd.uix.button import MDRectangleFlatIconButton + + + class Example(MDApp): + def build(self): + screen = Screen() + screen.add_widget( + MDRectangleFlatIconButton( + text="MDRectangleFlatIconButton", + icon="language-python", + line_color=(0, 0, 0, 0), + pos_hint={"center_x": .5, "center_y": .5}, + ) + ) + return screen + + + Example().run() + +.. code-block:: kv + + MDRectangleFlatIconButton: + text: "MDRectangleFlatIconButton" + icon: "language-python" + line_color: 0, 0, 0, 0 + pos_hint: {"center_x": .5, "center_y": .5} + +.. MDRoundFlatButton: +MDRoundFlatButton +----------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-round-flat-button.png + :align: center + +Button parameters :class:`~MDRoundFlatButton` are the same as +button :class:`~MDRectangleFlatButton`: + +.. code-block:: kv + + MDRoundFlatButton: + text: "MDROUNDFLATBUTTON" + +.. warning:: The border color does change when using ``text_color`` parameter. + +.. code-block:: kv + + MDRoundFlatButton: + text: "MDROUNDFLATBUTTON" + text_color: 0, 1, 0, 1 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-round-flat-button-text-color.png + :align: center + +.. MDRoundFlatIconButton: +MDRoundFlatIconButton +--------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-round-flat-icon-button.png + :align: center + +Button parameters :class:`~MDRoundFlatIconButton` are the same as +button :class:`~MDRoundFlatButton`: + +.. code-block:: kv + + MDRoundFlatIconButton: + icon: "android" + text: "MDROUNDFLATICONBUTTON" + +.. MDFillRoundFlatButton: +MDFillRoundFlatButton +--------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-fill-round-flat-button.png + :align: center + +Button parameters :class:`~MDFillRoundFlatButton` are the same as +button :class:`~MDRaisedButton`. + +.. MDFillRoundFlatIconButton: +MDFillRoundFlatIconButton +--------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-fill-round-flat-icon-button.png + :align: center + +Button parameters :class:`~MDFillRoundFlatIconButton` are the same as +button :class:`~MDRaisedButton`. + +.. note:: Notice that the width of the :class:`~MDFillRoundFlatIconButton` + button matches the size of the button text. + +.. MDTextButton: +MDTextButton +------------ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-text-button.png + :align: center + +.. code-block:: kv + + MDTextButton: + text: "MDTEXTBUTTON" + custom_color: 0, 1, 0, 1 + +.. MDFloatingActionButtonSpeedDial: +MDFloatingActionButtonSpeedDial +------------------------------- + +.. Note:: See the full list of arguments in the class + :class:`~MDFloatingActionButtonSpeedDial`. + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + Screen: + + MDFloatingActionButtonSpeedDial: + data: app.data + rotation_root_button: True + ''' + + + class Example(MDApp): + data = { + 'language-python': 'Python', + 'language-php': 'PHP', + 'language-cpp': 'C++', + } + + def build(self): + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial.gif + :align: center + +Or without KV Language: + +.. code-block:: python + + from kivy.uix.screenmanager import Screen + + from kivymd.app import MDApp + from kivymd.uix.button import MDFloatingActionButtonSpeedDial + + + class Example(MDApp): + data = { + 'language-python': 'Python', + 'language-php': 'PHP', + 'language-cpp': 'C++', + } + + def build(self): + screen = Screen() + speed_dial = MDFloatingActionButtonSpeedDial() + speed_dial.data = self.data + speed_dial.rotation_root_button = True + screen.add_widget(speed_dial) + return screen + + + Example().run() + +You can use various types of animation of labels for buttons on the stack: + +.. code-block:: kv + + MDFloatingActionButtonSpeedDial: + hint_animation: True + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-hint.gif + :align: center + +You can set your color values ​​for background, text of buttons etc: + +.. code-block:: kv + + MDFloatingActionButtonSpeedDial: + bg_hint_color: app.theme_cls.primary_light + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-hint-color.png + :align: center + +.. seealso:: + + `See full example `_ +""" + +__all__ = ( + "MDIconButton", + "MDFloatingActionButton", + "MDFlatButton", + "MDRaisedButton", + "MDRectangleFlatButton", + "MDRectangleFlatIconButton", + "MDRoundFlatButton", + "MDRoundFlatIconButton", + "MDFillRoundFlatButton", + "MDFillRoundFlatIconButton", + "MDTextButton", + "MDFloatingActionButtonSpeedDial", +) + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.graphics.context_instructions import Color +from kivy.graphics.stencil_instructions import ( + StencilPop, + StencilPush, + StencilUnUse, + StencilUse, +) +from kivy.graphics.vertex_instructions import Ellipse, RoundedRectangle +from kivy.lang import Builder +from kivy.metrics import dp, sp +from kivy.properties import ( + BooleanProperty, + BoundedNumericProperty, + DictProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.button import Button +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.image import Image +from kivy.uix.widget import Widget + +from kivymd import images_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + CircularElevationBehavior, + CircularRippleBehavior, + CommonElevationBehavior, + RectangularElevationBehavior, + RectangularRippleBehavior, + SpecificBackgroundColorBehavior, +) +from kivymd.uix.tooltip import MDTooltip + +Builder.load_string( + """ +#:import images_path kivymd.images_path +#:import md_icons kivymd.icon_definitions.md_icons + + + + size_hint: (None, None) + anchor_x: 'center' + anchor_y: 'center' + + + + + + + + + + canvas: + Clear + Color: + rgba: self._current_button_color if root.icon in md_icons else (0, 0, 0, 0) + Ellipse: + size: self.size + pos: self.pos + source: self.source if hasattr(self, "source") else "" + + size: + (dp(48), dp(48)) \ + if not root.user_font_size \ + else (dp(root.user_font_size + 23), dp(root.user_font_size + 23)) + lbl_txt: lbl_txt + padding: (dp(12), dp(12), dp(12), dp(12)) if root.icon in md_icons else (0, 0, 0, 0) + + MDIcon: + id: lbl_txt + icon: root.icon + font_size: + root.user_font_size \ + if root.user_font_size \ + else self.font_size + font_name: root.font_name if root.font_name else self.font_name + theme_text_color: root.theme_text_color + text_color: root.text_color + disabled: root.disabled + valign: 'middle' + halign: 'center' + opposite_colors: root.opposite_colors + + + + canvas: + Clear + Color: + rgba: self._current_button_color + RoundedRectangle: + size: self.size + pos: self.pos + radius: (root._radius, ) + + lbl_txt: lbl_txt + height: dp(22) + sp(root.font_size) + width: lbl_txt.texture_size[0] + dp(24) + padding: (dp(8), 0) # For MDRectangleFlatIconButton + theme_text_color: 'Primary' if not root.text_color else 'Custom' + markup: False + + MDLabel: + id: lbl_txt + text: root.text if root.button_label else '' + font_size: sp(root.font_size) + font_name: root.font_name if root.font_name else self.font_name + size_hint_x: None + text_size: (None, root.height) + height: self.texture_size[1] + theme_text_color: root.theme_text_color + text_color: root._current_text_color + markup: root.markup + disabled: root.disabled + valign: 'middle' + halign: 'center' + opposite_colors: root.opposite_colors + + + + canvas.before: + Color: + rgba: + (root.theme_cls.primary_color if not root.text_color else root.text_color) \ + if not root.disabled else root.theme_cls.disabled_hint_text_color + Line: + width: root.line_width + rounded_rectangle: + (self.x, self.y, self.width, self.height,\ + root._radius, root._radius, root._radius, root._radius,\ + self.height) + + theme_text_color: 'Custom' + text_color: + (root.theme_cls.primary_color if not root.text_color else root.text_color) \ + if not root.disabled else root.theme_cls.disabled_hint_text_color + + + + canvas.before: + Color: + rgba: + (root.theme_cls.primary_color if root.md_bg_color == [0.0, 0.0, 0.0, 0.0] else root.md_bg_color) \ + if not root.disabled else root.theme_cls.disabled_hint_text_color + RoundedRectangle: + size: self.size + pos: self.pos + radius: [root._radius, ] + + + + md_bg_color: + root.theme_cls.primary_color if root._current_button_color == [0.0, 0.0, 0.0, 0.0] \ + else root._current_button_color + line_width: 0.001 + + + + canvas.before: + Color: + rgba: + root.theme_cls.primary_color if not root.text_color else root.text_color + Line: + width: root.line_width + rectangle: (self.x, self.y, self.width, self.height) + + theme_text_color: 'Custom' + text_color: root.theme_cls.primary_color if not root.text_color else root.text_color + + + + canvas.before: + Color: + rgba: + root.line_color if root.line_color else \ + (root.theme_cls.primary_color if not root.text_color else root.text_color) \ + if not root.disabled else root.theme_cls.disabled_hint_text_color + Line: + width: 1 + rectangle: (self.x, self.y, self.width, self.height) + + size_hint_x: None + width: lbl_txt.texture_size[0] + lbl_ic.texture_size[0] + box.spacing * 3 + markup: False + + BoxLayout: + id: box + spacing: dp(10) + + MDIcon: + id: lbl_ic + icon: root.icon + theme_text_color: 'Custom' + text_color: + (root.theme_cls.primary_color if not root.text_color else root.text_color) \ + if not root.disabled else root.theme_cls.disabled_hint_text_color + size_hint_x: None + width: self.texture_size[0] + + Label: + id: lbl_txt + text: root.text + font_size: sp(root.font_size) + font_name: root.font_name if root.font_name else self.font_name + shorten: True + width: self.texture_size[0] + color: + (root.theme_cls.primary_color if not root.text_color else root.text_color) \ + if not root.disabled else root.theme_cls.disabled_hint_text_color + markup: root.markup + + + + size_hint_x: None + width: lbl_txt.texture_size[0] + lbl_ic.texture_size[0] + box.spacing * 3 + markup: False + + BoxLayout: + id: box + spacing: dp(10) + + MDIcon: + id: lbl_ic + icon: root.icon + theme_text_color: 'Custom' + text_color: + root.theme_cls.primary_color \ + if not root.text_color else root.text_color + size_hint_x: None + width: self.texture_size[0] + + Label: + id: lbl_txt + text: root.text + font_size: sp(root.font_size) + font_name: root.font_name if root.font_name else self.font_name + shorten: True + size_hint_x: None + width: self.texture_size[0] + color: root.theme_cls.primary_color if not root.text_color else root.text_color + markup: root.markup + + + + md_bg_color: root.theme_cls.primary_color + theme_text_color: 'Custom' + text_color: root.specific_text_color + + + + # Defaults to 56-by-56 and a background of the accent color according to + # guidelines + size: (dp(56), dp(56)) + theme_text_color: 'Custom' + + + + size_hint: None, None + size: self.texture_size + color: + root.theme_cls.primary_color \ + if not len(root.custom_color) else root.custom_color + background_down: f'{images_path}transparent.png' + background_normal: f'{images_path}transparent.png' + opacity: 1 + + +# SpeedDial classes + + + + size_hint: None, None + size: dp(46), dp(46) + theme_text_color: "Custom" + md_bg_color: self.theme_cls.primary_color + + canvas.before: + Color: + rgba: + self.theme_cls.primary_color \ + if not self._bg_color else self._bg_color + RoundedRectangle: + pos: + (self.x - self._canvas_width + dp(1.5)) + self._padding_right / 2, \ + self.y - self._padding_right / 2 + dp(1.5) + size: + self.width + self._canvas_width - dp(3), \ + self.height + self._padding_right - dp(3) + radius: [self.height / 2] + + + + theme_text_color: "Custom" + md_bg_color: self.theme_cls.primary_color + + canvas.before: + PushMatrix + Rotate: + angle: self._angle + axis: (0, 0, 1) + origin: self.center + canvas.after: + PopMatrix + + + + size_hint: None, None + padding: "8dp", "4dp", "8dp", "4dp" + height: label.texture_size[1] + self.padding[1] * 2 + width: label.texture_size[0] + self.padding[0] * 2 + elevation: 10 + + canvas: + Color: + rgba: self.theme_cls.primary_color if not root.bg_color else root.bg_color + RoundedRectangle: + pos: self.pos + size: self.size + radius: [5] + + Label: + id: label + markup: True + text: root.text + size_hint: None, None + size: self.texture_size + color: root.theme_cls.text_color if not root.text_color else root.text_color +""" +) + + +class BaseButton( + ThemableBehavior, + ButtonBehavior, + SpecificBackgroundColorBehavior, + AnchorLayout, + Widget, +): + """ + Abstract base class for all MD buttons. This class handles the button's + colors (disabled/down colors handled in children classes as those depend on + type of button) as well as the disabled state. + """ + + theme_text_color = OptionProperty( + "Primary", + options=[ + "Primary", + "Secondary", + "Hint", + "Error", + "Custom", + "ContrastParentBackground", + ], + ) + """ + Button text type. Available options are: (`"Primary"`, `"Secondary"`, + `"Hint"`, `"Error"`, `"Custom"`, `"ContrastParentBackground"`). + + :attr:`theme_text_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Primary'`. + """ + + text_color = ListProperty() + """ + Text color in ``rgba`` format. + + :attr:`text_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `''`. + """ + + font_name = StringProperty() + """ + Font name. + + :attr:`font_name` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + font_size = NumericProperty(14) + """ + Font size. + + :attr:`font_size` is an :class:`~kivy.properties.NumericProperty` + and defaults to `14`. + """ + + user_font_size = NumericProperty() + """Custom font size for :class:`~MDIconButton`. + + :attr:`user_font_size` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + md_bg_color_disabled = ListProperty() + """Color disabled. + + :attr:`md_bg_color_disabled` is an :class:`~kivy.properties.ListProperty` + and defaults to ``. + """ + + line_width = NumericProperty(1) + + opposite_colors = BooleanProperty(False) + + _current_button_color = ListProperty([0.0, 0.0, 0.0, 0.0]) + _current_text_color = ListProperty([1.0, 1.0, 1.0, 1]) + _md_bg_color_down = ListProperty([0.0, 0.0, 0.0, 0.1]) + _md_bg_color_disabled = ListProperty([0.0, 0.0, 0.0, 0.0]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.theme_cls.bind(primary_palette=self.update_md_bg_color) + Clock.schedule_once(self.check_current_button_color) + + def update_md_bg_color(self, instance, value): + """Called when the application color palette changes.""" + + def check_current_button_color(self, interval): + if self.md_bg_color_disabled: + self._md_bg_color_disabled = self.md_bg_color_disabled + else: + self._md_bg_color_disabled = self.theme_cls.disabled_hint_text_color + self.on_disabled(self, self.disabled) + if self._current_button_color == [0.0, 0.0, 0.0, 0.0]: + self._current_button_color = self.md_bg_color + + def on_text_color(self, instance, value): + if value not in ([0, 0, 0, 0.87], [1.0, 1.0, 1.0, 1]): + self._current_text_color = value + + def on_md_bg_color(self, instance, value): + if value != self.theme_cls.primary_color: + self._current_button_color = value + + def on_disabled(self, instance, value): + if self.disabled: + self._current_button_color = self._md_bg_color_disabled + else: + self._current_button_color = self.md_bg_color + + def on_font_size(self, instance, value): + def _on_font_size(interval): + if "lbl_ic" in instance.ids: + instance.ids.lbl_ic.font_size = sp(value) + + Clock.schedule_once(_on_font_size) + + +class BasePressedButton(BaseButton): + """ + Abstract base class for those button which fade to a background color on + press. + """ + + _fade_bg = None + + def on_touch_down(self, touch): + if touch.is_mouse_scrolling: + return False + elif not self.collide_point(touch.x, touch.y): + return False + elif self in touch.ud: + return False + elif self.disabled: + return False + else: + # Button dimming animation. + if self.md_bg_color == [0.0, 0.0, 0.0, 0.0]: + self._fade_bg = Animation( + duration=0.5, _current_button_color=self._md_bg_color_down + ) + self._fade_bg.start(self) + return super().on_touch_down(touch) + + def on_touch_up(self, touch): + if touch.grab_current is self and self._fade_bg: + self._fade_bg.stop_property(self, "_current_button_color") + Animation( + duration=0.05, _current_button_color=self.md_bg_color + ).start(self) + return super().on_touch_up(touch) + + +class BaseFlatButton(BaseButton): + """ + Abstract base class for flat buttons which do not elevate from material. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.md_bg_color = (0.0, 0.0, 0.0, 0.0) + + +class BaseRaisedButton(CommonElevationBehavior, BaseButton): + """ + Abstract base class for raised buttons which elevate from material. + Raised buttons are to be used sparingly to emphasise primary/important + actions. + + Implements elevation behavior as well as the recommended down/disabled + colors for raised buttons. + """ + + _elevation_normal = NumericProperty(0) + _elevation_raised = NumericProperty(0) + _anim_raised = None + + def update_md_bg_color(self, instance, value): + """Called when the application color palette changes.""" + + self._current_button_color = self.theme_cls._get_primary_color() + + def on_elevation(self, instance, value): + self._elevation_normal = self.elevation + self._elevation_raised = self.elevation + self._anim_raised = Animation(_elevation=value + 2, d=0.2) + self._anim_raised.bind(on_progress=self._do_anim_raised) + self._update_elevation(instance, value) + + def on_disabled(self, instance, value): + if self.disabled: + self._elevation = 0 + self._update_shadow(instance, 0) + else: + self._update_elevation(instance, self._elevation_normal) + super().on_disabled(instance, value) + + def on_touch_down(self, touch): + if not self.disabled: + if touch.is_mouse_scrolling: + return False + if not self.collide_point(touch.x, touch.y): + return False + if self in touch.ud: + return False + if self._anim_raised: + self._anim_raised.start(self) + return super().on_touch_down(touch) + + def on_touch_up(self, touch): + if not self.disabled: + if touch.grab_current is not self: + return super().on_touch_up(touch) + Animation.cancel_all(self, "_elevation") + self._elevation = self._elevation_raised + self._elevation_normal = self._elevation_raised + self._update_shadow(self, self._elevation) + return super().on_touch_up(touch) + + def _do_anim_raised(self, animation, instance, value): + self._elevation += value + if self._elevation < self._elevation_raised + 2: + self._update_shadow(instance, self._elevation) + + +class BaseRoundButton(CircularRippleBehavior, BaseButton): + """ + Abstract base class for all round buttons, bringing in the appropriate + on-touch behavior + """ + + +class BaseRectangularButton(RectangularRippleBehavior, BaseButton): + """ + Abstract base class for all rectangular buttons, bringing in the + appropriate on-touch behavior. Also maintains the correct minimum width + as stated in guidelines. + """ + + width = BoundedNumericProperty( + 88, min=88, max=None, errorhandler=lambda x: 88 + ) + text = StringProperty("") + """Button text. + + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + button_label = BooleanProperty(True) + """ + If ``False`` the text on the button will not be displayed. + + :attr:`button_label` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + _radius = NumericProperty("2dp") + _height = NumericProperty(0) + + +class MDIconButton(BaseRoundButton, BaseFlatButton, BasePressedButton): + icon = StringProperty("checkbox-blank-circle") + """ + Button icon. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'checkbox-blank-circle'`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.md_bg_color_disabled = (0, 0, 0, 0) + + +class MDFlatButton(BaseRectangularButton, BaseFlatButton, BasePressedButton): + pass + + +class BaseFlatIconButton(MDFlatButton): + icon = StringProperty("android") + """ + Button icon. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'android'`. + """ + + text = StringProperty("") + """Button text. + + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + button_label = BooleanProperty(False) + + def update_md_bg_color(self, instance, value): + self.text_color = self.theme_cls._get_primary_color() + + +class MDRaisedButton( + BaseRectangularButton, + RectangularElevationBehavior, + BaseRaisedButton, + BasePressedButton, +): + pass + + +class MDFloatingActionButton( + BaseRoundButton, CircularElevationBehavior, BaseRaisedButton +): + icon = StringProperty("android") + """ + Button icon. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'android'`. + """ + + background_palette = StringProperty("Accent") + """ + The name of the palette used for the background color of the button. + + :attr:`background_palette` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Accent'`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if self.md_bg_color == [1.0, 1.0, 1.0, 0.0]: + self.md_bg_color = self.theme_cls.accent_color + + def on_md_bg_color(self, instance, value): + if value != self.theme_cls.accent_color: + self._current_button_color = value + + +class MDRoundImageButton(MDFloatingActionButton): + source = StringProperty() + """Path to button image. + + :attr:`source` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + _current_button_color = [1, 1, 1, 1] + + def on_source(self, instance, value): + self.source = value + + def on_size(self, instance, value): + self.remove_widget(self.ids.lbl_txt) + + +class MDRectangleFlatButton(MDFlatButton): + def update_md_bg_color(self, instance, value): + self.text_color = self.theme_cls._get_primary_color() + + def on_disabled(self, instance, value): + if self.disabled: + self.line_width = 0.001 + self._current_button_color = ( + self.theme_cls.disabled_hint_text_color + if not self.md_bg_color_disabled + else self.md_bg_color_disabled + ) + else: + self._current_button_color = self.md_bg_color + self.line_width = 1 + + +class MDRoundFlatButton(MDFlatButton): + _radius = NumericProperty("18dp") + + def update_md_bg_color(self, instance, value): + self.text_color = self.theme_cls._get_primary_color() + + def lay_canvas_instructions(self): + with self.canvas.after: + StencilPush() + RoundedRectangle( + size=self.size, pos=self.pos, radius=[self._radius] + ) + 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() + RoundedRectangle( + size=self.size, pos=self.pos, radius=[self._radius] + ) + StencilPop() + self.bind(ripple_color=self._set_color, _ripple_rad=self._set_ellipse) + + +class MDTextButton(ThemableBehavior, Button): + custom_color = ListProperty() + """Custom user button color in ``rgba`` format. + + :attr:`custom_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + def animation_label(self): + def set_default_state_label(*args): + Animation(opacity=1, d=0.1, t="in_out_cubic").start(self) + + anim = Animation(opacity=0.5, d=0.2, t="in_out_cubic") + anim.bind(on_complete=set_default_state_label) + anim.start(self) + + def on_press(self, *args): + self.animation_label() + return super().on_press(*args) + + def on_disabled(self, instance, value): + if value: + self.disabled_color = self.theme_cls.disabled_hint_text_color + self.background_disabled_normal = f"{images_path}transparent.png" + + +class MDCustomRoundIconButton(CircularRippleBehavior, ButtonBehavior, Image): + pass + + +class MDFillRoundFlatButton(CircularElevationBehavior, MDRoundFlatButton): + _elevation_normal = NumericProperty() + + def __init__(self, **kwargs): + self.text_color = (1, 1, 1, 1) + self.line_width = 0.001 + super().__init__(**kwargs) + + def update_md_bg_color(self, instance, value): + self.text_color = self.text_color + self.md_bg_color = self.theme_cls._get_primary_color() + + def on_md_bg_color(self, instance, value): + if value != [0.0, 0.0, 0.0, 0.0]: + self._current_button_color = value + + def on_elevation(self, instance, value): + if value: + self._elevation_normal = value + + def on_disabled(self, instance, value): + # FIXME:The elevation parameter is not restored. + ''' + from kivy.lang import Builder + + from kivymd.app import MDApp + + root_kv = """ + Screen: + + MDFillRoundFlatButton: + id: btn + text: "Click me!" + pos_hint: {"center_x": .5, "center_y": .6} + elevation: 8 + on_press: self.disabled = True + + MDFillRoundFlatButton: + text: "UNDISABLED" + pos_hint: {"center_x": .5, "center_y": .4} + on_press: btn.disabled = False + """ + + class MainApp(MDApp): + def build(self): + self.root = Builder.load_string(root_kv) + + MainApp().run() + ''' + + if self.disabled: + self.elevation = 0 + self._update_shadow(instance, 0) + else: + self.elevation = self._elevation_normal + self._update_elevation(instance, self.elevation) + super().on_disabled(instance, value) + + +class MDRectangleFlatIconButton(BaseFlatIconButton): + line_color = ListProperty() + """Button border color in ``rgba`` format. + + :attr:`line_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + +class MDRoundFlatIconButton(MDRoundFlatButton, BaseFlatIconButton): + pass + + +class MDFillRoundFlatIconButton(MDRoundFlatIconButton): + text_color = ListProperty((1, 1, 1, 1)) + + def on_md_bg_color(self, instance, value): + if value != [0.0, 0.0, 0.0, 0.0]: + self._current_button_color = value + + def update_md_bg_color(self, instance, value): + self._current_button_color = self.theme_cls.primary_color + + +# SpeedDial classes + + +class BaseFloatingRootButton(MDFloatingActionButton): + _angle = NumericProperty(0) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.elevation = 5 + + +class BaseFloatingBottomButton(MDFloatingActionButton, MDTooltip): + _canvas_width = NumericProperty(0) + _padding_right = NumericProperty(0) + _bg_color = ListProperty() + + +class BaseFloatingLabel( + ThemableBehavior, RectangularElevationBehavior, BoxLayout +): + text = StringProperty() + text_color = ListProperty() + bg_color = ListProperty() + + +class MDFloatingBottomButton(BaseFloatingBottomButton): + pass + + +class MDFloatingRootButton(BaseFloatingRootButton): + pass + + +class MDFloatingLabel(BaseFloatingLabel): + pass + + +class MDFloatingActionButtonSpeedDial(ThemableBehavior, FloatLayout): + """ + :Events: + :attr:`on_open` + Called when a stack is opened. + :attr:`on_close` + Called when a stack is closed. + """ + + icon = StringProperty("plus") + """ + Root button icon name. + + :attr:`icon` is a :class:`~kivy.properties.StringProperty` + and defaults to `'plus'`. + """ + + anchor = OptionProperty("right", option=["right"]) + """ + Stack anchor. Available options are: `'right'`. + + :attr:`anchor` is a :class:`~kivy.properties.OptionProperty` + and defaults to `'right'`. + """ + + callback = ObjectProperty(lambda x: None) + """ + Custom callback. + + .. code-block:: kv + + MDFloatingActionButtonSpeedDial: + callback: app.callback + + .. code-block:: python + + def callback(self, instance): + print(instance.icon) + + + :attr:`callback` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + label_text_color = ListProperty([0, 0, 0, 1]) + """ + Floating text color in ``rgba`` format. + + :attr:`label_text_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0, 0, 1]`. + """ + + data = DictProperty() + """ + Must be a dictionary + + .. code-block:: python + + { + 'name-icon': 'Text label', + ..., + ..., + } + """ + + right_pad = BooleanProperty(True) + """ + If `True`, the button will increase on the right side by 2.5 piesels + if the :attr:`~hint_animation` parameter equal to `True`. + + .. rubric:: False + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-right-pad.gif + :align: center + + .. rubric:: True + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDFloatingActionButtonSpeedDial-right-pad-true.gif + :align: center + + :attr:`right_pad` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + rotation_root_button = BooleanProperty(False) + """ + If ``True`` then the root button will rotate 45 degrees when the stack + is opened. + + :attr:`rotation_root_button` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + opening_transition = StringProperty("out_cubic") + """ + The name of the stack opening animation type. + + :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_cubic'`. + """ + + closing_transition = StringProperty("out_cubic") + """ + The name of the stack closing animation type. + + :attr:`closing_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_cubic'`. + """ + + opening_transition_button_rotation = StringProperty("out_cubic") + """ + The name of the animation type to rotate the root button when opening the + stack. + + :attr:`opening_transition_button_rotation` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_cubic'`. + """ + + closing_transition_button_rotation = StringProperty("out_cubic") + """ + The name of the animation type to rotate the root button when closing the + stack. + + :attr:`closing_transition_button_rotation` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_cubic'`. + """ + + opening_time = NumericProperty(0.5) + """ + Time required for the stack to go to: attr:`state` `'open'`. + + :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + closing_time = NumericProperty(0.2) + """ + Time required for the stack to go to: attr:`state` `'close'`. + + :attr:`closing_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + opening_time_button_rotation = NumericProperty(0.2) + """ + Time required to rotate the root button 45 degrees during the stack + opening animation. + + :attr:`opening_time_button_rotation` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + closing_time_button_rotation = NumericProperty(0.2) + """ + Time required to rotate the root button 0 degrees during the stack + closing animation. + + :attr:`closing_time_button_rotation` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + state = OptionProperty("close", options=("close", "open")) + """ + Indicates whether the stack is closed or open. + Available options are: `'close'`, `'open'`. + + :attr:`state` is a :class:`~kivy.properties.OptionProperty` + and defaults to `'close'`. + """ + + bg_color_root_button = ListProperty() + """ + Root button color in ``rgba`` format. + + :attr:`bg_color_root_button` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + bg_color_stack_button = ListProperty() + """ + The color of the buttons in the stack ``rgba`` format. + + :attr:`bg_color_stack_button` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + color_icon_stack_button = ListProperty() + """ + The color icon of the buttons in the stack ``rgba`` format. + + :attr:`color_icon_stack_button` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + color_icon_root_button = ListProperty() + """ + The color icon of the root button ``rgba`` format. + + :attr:`color_icon_root_button` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + bg_hint_color = ListProperty() + """ + Background color for the text of the buttons in the stack ``rgba`` format. + + :attr:`bg_hint_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + hint_animation = BooleanProperty(False) + """ + Whether to use button extension animation to display text labels. + + :attr:`hint_animation` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + _label_pos_y_set = False + _anim_buttons_data = {} + _anim_labels_data = {} + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_open") + self.register_event_type("on_close") + Window.bind(on_resize=self._update_pos_buttons) + + def on_open(self, *args): + """Called when a stack is opened.""" + + def on_close(self, *args): + """Called when a stack is closed.""" + + def on_leave(self, instance): + """Called when the mouse cursor goes outside the button of stack.""" + + if self.state == "open": + for widget in self.children: + if isinstance(widget, MDFloatingLabel) and self.hint_animation: + Animation.cancel_all(widget) + if self.data[instance.icon] == widget.text: + Animation( + _canvas_width=0, + _padding_right=0, + d=self.opening_time, + t=self.opening_transition, + ).start(instance) + if self.hint_animation: + Animation( + opacity=0, d=0.1, t=self.opening_transition + ).start(widget) + break + + def on_enter(self, instance): + """Called when the mouse cursor is over a button from the stack.""" + + if self.state == "open": + for widget in self.children: + if isinstance(widget, MDFloatingLabel) and self.hint_animation: + widget.elevation = 0 + if self.data[instance.icon] == widget.text: + Animation( + _canvas_width=widget.width + dp(24), + _padding_right=dp(5) if self.right_pad else 0, + d=self.opening_time, + t=self.opening_transition, + ).start(instance) + if self.hint_animation: + Animation( + opacity=1, + d=self.opening_time, + t=self.opening_transition, + ).start(widget) + break + + def on_data(self, instance, value): + """Creates a stack of buttons.""" + + # FIXME: Don't know how to fix AttributeError error: + # File "kivymd/uix/button.py", line 1597, in on_data + # self.add_widget(bottom_button) + # File "kivy/uix/floatlayout.py", line 140, in add_widget + # return super(FloatLayout, self).add_widget(widget, index, canvas) + # File "kivy/uix/layout.py", line 97, in add_widget + # return super(Layout, self).add_widget(widget, index, canvas) + # File "kivy/uix/widget.py", line 629, in add_widget + # canvas.add(widget.canvas) + # AttributeError: 'NoneType' object has no attribute 'add' + super().__init__() + + self.clear_widgets() + self._anim_buttons_data = {} + self._anim_labels_data = {} + self._label_pos_y_set = False + + # Bottom buttons. + for name_icon in value.keys(): + bottom_button = MDFloatingBottomButton( + icon=name_icon, + on_enter=self.on_enter, + on_leave=self.on_leave, + opacity=0, + ) + bottom_button.bind( + on_release=lambda x=bottom_button: self.callback(x) + ) + self.set_pos_bottom_buttons(bottom_button) + self.add_widget(bottom_button) + # Labels. + floating_text = value[name_icon] + if floating_text: + label = MDFloatingLabel(text=floating_text, opacity=0) + label.text_color = self.label_text_color + self.add_widget(label) + # Top root button. + root_button = MDFloatingRootButton(on_release=self.open_stack) + root_button.icon = self.icon + self.set_pos_root_button(root_button) + self.add_widget(root_button) + + def on_icon(self, instance, value): + self._get_count_widget(MDFloatingRootButton).icon = value + + def on_label_text_color(self, instance, value): + for widget in self.children: + if isinstance(widget, MDFloatingLabel): + widget.text_color = value + + def on_color_icon_stack_button(self, instance, value): + for widget in self.children: + if isinstance(widget, MDFloatingBottomButton): + widget.text_color = value + + def on_hint_animation(self, instance, value): + for widget in self.children: + if isinstance(widget, MDFloatingLabel): + widget.bg_color = (0, 0, 0, 0) + + def on_bg_hint_color(self, instance, value): + for widget in self.children: + if isinstance(widget, MDFloatingBottomButton): + widget._bg_color = value + + def on_color_icon_root_button(self, instance, value): + self._get_count_widget(MDFloatingRootButton).text_color = value + + def on_bg_color_stack_button(self, instance, value): + for widget in self.children: + if isinstance(widget, MDFloatingBottomButton): + widget.md_bg_color = value + + def on_bg_color_root_button(self, instance, value): + self._get_count_widget(MDFloatingRootButton).md_bg_color = value + + def set_pos_labels(self, widget): + """Sets the position of the floating labels.""" + + if self.anchor == "right": + widget.x = Window.width - widget.width - dp(86) + + def set_pos_root_button(self, instance): + """Sets the position of the root button.""" + + if self.anchor == "right": + instance.y = dp(20) + instance.x = Window.width - (dp(56) + dp(20)) + + def set_pos_bottom_buttons(self, instance): + """Sets the position of the bottom buttons in a stack.""" + + if self.anchor == "right": + if self.state != "open": + instance.y = instance.height / 2 + instance.x = Window.width - (instance.height + instance.width / 2) + + def open_stack(self, instance): + """Opens a button stack.""" + + for widget in self.children: + if isinstance(widget, MDFloatingLabel): + Animation.cancel_all(widget) + + if self.state != "open": + y = 0 + label_position = dp(56) + anim_buttons_data = {} + anim_labels_data = {} + + for widget in self.children: + if isinstance(widget, MDFloatingBottomButton): + # Sets new button positions. + y += dp(56) + widget.y = widget.y * 2 + y + if not self._anim_buttons_data: + anim_buttons_data[widget] = Animation( + opacity=1, + d=self.opening_time, + t=self.opening_transition, + ) + elif isinstance(widget, MDFloatingLabel): + # Sets new labels positions. + label_position += dp(56) + # Sets the position of signatures only once. + if not self._label_pos_y_set: + widget.y = widget.y * 2 + label_position + widget.x = Window.width - widget.width - dp(86) + if not self._anim_labels_data: + anim_labels_data[widget] = Animation( + opacity=1, d=self.opening_time + ) + elif ( + isinstance(widget, MDFloatingRootButton) + and self.rotation_root_button + ): + # Rotates the root button 45 degrees. + Animation( + _angle=-45, + d=self.opening_time_button_rotation, + t=self.opening_transition_button_rotation, + ).start(widget) + + if anim_buttons_data: + self._anim_buttons_data = anim_buttons_data + if anim_labels_data and not self.hint_animation: + self._anim_labels_data = anim_labels_data + + self.state = "open" + self.dispatch("on_open") + self.do_animation_open_stack(self._anim_buttons_data) + self.do_animation_open_stack(self._anim_labels_data) + if not self._label_pos_y_set: + self._label_pos_y_set = True + else: + self.close_stack() + + def do_animation_open_stack(self, anim_data): + def on_progress(animation, widget, value): + if value >= 0.1: + animation_open_stack() + + def animation_open_stack(*args): + try: + widget = next(widgets_list) + animation = anim_data[widget] + animation.bind(on_progress=on_progress) + animation.start(widget) + except StopIteration: + pass + + widgets_list = iter(list(anim_data.keys())) + animation_open_stack() + + def close_stack(self): + """Closes the button stack.""" + + for widget in self.children: + if isinstance(widget, MDFloatingBottomButton): + Animation( + y=widget.height / 2, + d=self.closing_time, + t=self.closing_transition, + opacity=0, + ).start(widget) + elif isinstance(widget, MDFloatingLabel): + Animation(opacity=0, d=0.1).start(widget) + elif ( + isinstance(widget, MDFloatingRootButton) + and self.rotation_root_button + ): + Animation( + _angle=0, + d=self.closing_time_button_rotation, + t=self.closing_transition_button_rotation, + ).start(widget) + self.state = "close" + self.dispatch("on_close") + + def _update_pos_buttons(self, instance, width, height): + # Updates button positions when resizing screen. + for widget in self.children: + if isinstance(widget, MDFloatingBottomButton): + self.set_pos_bottom_buttons(widget) + elif isinstance(widget, MDFloatingRootButton): + self.set_pos_root_button(widget) + elif isinstance(widget, MDFloatingLabel): + self.set_pos_labels(widget) + + def _get_count_widget(self, instance): + widget = None + for widget in self.children: + if isinstance(widget, instance): + break + return widget diff --git a/kivymd/uix/card.py b/kivymd/uix/card.py new file mode 100755 index 0000000..1b24c7d --- /dev/null +++ b/kivymd/uix/card.py @@ -0,0 +1,896 @@ +""" +Components/Card +=============== + +.. seealso:: + + `Material Design spec, 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 + + : + 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 = ''' + : + 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 + + : + # 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 + + : + # 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 + + : + 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 + + : + 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 = ''' + : + 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 + + : + + 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 = ''' + : + 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 = ''' + + 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( + """ +: + canvas: + Color: + rgba: app.theme_cls.divider_color + Rectangle: + size: self.size + pos: self.pos + + + + canvas: + RoundedRectangle: + size: self.size + pos: self.pos + source: root.background + + + + 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 diff --git a/kivymd/uix/carousel.py b/kivymd/uix/carousel.py new file mode 100644 index 0000000..5954e1f --- /dev/null +++ b/kivymd/uix/carousel.py @@ -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) diff --git a/kivymd/uix/chip.py b/kivymd/uix/chip.py new file mode 100755 index 0000000..9d15470 --- /dev/null +++ b/kivymd/uix/chip.py @@ -0,0 +1,275 @@ +""" +Components/Chip +=============== + +.. seealso:: + + `Material Design spec, 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 `_ +""" + +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 + + + + adaptive_height: True + spacing: "5dp" + + + + 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) diff --git a/kivymd/uix/datatables.py b/kivymd/uix/datatables.py new file mode 100644 index 0000000..1db4eaf --- /dev/null +++ b/kivymd/uix/datatables.py @@ -0,0 +1,971 @@ +""" +Components/DataTables +===================== + +.. seealso:: + + `Material Design spec, DataTables `_ + +.. 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 + + + + 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: + + + + 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 + + + + 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: + + + + 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 + + + + 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") + + + + + 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() # + 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() # + 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() # + pagination_menu = ObjectProperty() # + pagination = ObjectProperty() # + 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() # + + +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 diff --git a/kivymd/uix/dialog.py b/kivymd/uix/dialog.py new file mode 100755 index 0000000..25c49d3 --- /dev/null +++ b/kivymd/uix/dialog.py @@ -0,0 +1,603 @@ +""" +Components/Dialog +================= + +.. seealso:: + + `Material Design spec, 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 + + + + 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 + + + + 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 = ''' + + + 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 = ''' + + 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 = ''' + + 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) diff --git a/kivymd/uix/dialog2.py b/kivymd/uix/dialog2.py new file mode 100644 index 0000000..0513ddb --- /dev/null +++ b/kivymd/uix/dialog2.py @@ -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: + + +This file is distributed under the terms of the same license, +as the Kivy framework. + +`Material Design spec, Dialogs `_ + +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(''' + + 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 + + + + 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 +: + size_hint: 1, None + valign: 'middle' + height: self.texture_size[1] + +: + size_hint_y: None + valign: 'middle' + height: self.texture_size[1] + +: + size_hint_y: None + height: self.minimum_height + padding: dp(0), dp(0), dp(10), dp(0) + + + + 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 + + + + 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() diff --git a/kivymd/uix/dropdownitem.py b/kivymd/uix/dropdownitem.py new file mode 100644 index 0000000..f7a8f7d --- /dev/null +++ b/kivymd/uix/dropdownitem.py @@ -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 `_ +""" + +__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 \ + ] + + + + 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 diff --git a/kivymd/uix/expansionpanel.py b/kivymd/uix/expansionpanel.py new file mode 100755 index 0000000..95920e8 --- /dev/null +++ b/kivymd/uix/expansionpanel.py @@ -0,0 +1,395 @@ +""" +Components/Expansion Panel +========================== + +.. seealso:: + + `Material Design spec, Expansion panel `_ + +.. 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 = ''' + + 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 `_ + + `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( + """ +: + icon: 'chevron-right' + disabled: True + + canvas.before: + PushMatrix + Rotate: + angle: self._angle + axis: (0, 0, 1) + origin: self.center + canvas.after: + PopMatrix + + + + 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 diff --git a/kivymd/uix/filemanager.py b/kivymd/uix/filemanager.py new file mode 100755 index 0000000..3ec0051 --- /dev/null +++ b/kivymd/uix/filemanager.py @@ -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 + + + + 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: + + + + size_hint_y: None + height: self.texture_size[1] + shorten: True + shorten_from: "center" + halign: "center" + text_size: self.width, None + + + + 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 + + + + 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 + + + + 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" + + + + + 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) diff --git a/kivymd/uix/floatlayout.py b/kivymd/uix/floatlayout.py new file mode 100644 index 0000000..df67386 --- /dev/null +++ b/kivymd/uix/floatlayout.py @@ -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 diff --git a/kivymd/uix/gridlayout.py b/kivymd/uix/gridlayout.py new file mode 100644 index 0000000..5c059bf --- /dev/null +++ b/kivymd/uix/gridlayout.py @@ -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 diff --git a/kivymd/uix/imagelist.py b/kivymd/uix/imagelist.py new file mode 100755 index 0000000..24b4b38 --- /dev/null +++ b/kivymd/uix/imagelist.py @@ -0,0 +1,311 @@ +""" +Components/Image List +===================== + +.. seealso:: + + `Material Design spec, 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 = ''' + + 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 = ''' + + 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( + """ + + _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 + + + + _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 diff --git a/kivymd/uix/label.py b/kivymd/uix/label.py new file mode 100755 index 0000000..5b7df8d --- /dev/null +++ b/kivymd/uix/label.py @@ -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 `_ + + `Material Design Icon Names `_ + +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 + + + + disabled_color: self.theme_cls.disabled_hint_text_color + text_size: self.width, None + + +: + 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`. + """ diff --git a/kivymd/uix/list.py b/kivymd/uix/list.py new file mode 100755 index 0000000..2545163 --- /dev/null +++ b/kivymd/uix/list.py @@ -0,0 +1,1026 @@ +""" +Components/List +=============== + +.. seealso:: + + `Material Design spec, Lists `_ + +.. rubric:: Lists are continuous, vertical indexes of text or images. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/lists.png + :align: center + +The class :class:`~MDList` in combination with a :class:`~BaseListItem` like +:class:`~OneLineListItem` will create a list that expands as items are added to +it, working nicely with `Kivy's` :class:`~kivy.uix.scrollview.ScrollView`. + +Due to the variety in sizes and controls in the `Material Design spec`, +this module suffers from a certain level of complexity to keep the widgets +compliant, flexible and performant. + +For this `KivyMD` provides list items that try to cover the most common usecases, +when those are insufficient, there's a base class called :class:`~BaseListItem` +which you can use to create your own list items. This documentation will only +cover the provided ones, for custom implementations please refer to this +module's source code. + +`KivyMD` provides the following list items classes for use: + +Text only ListItems +------------------- + +- OneLineListItem_ +- TwoLineListItem_ +- ThreeLineListItem_ + +ListItems with widget containers +-------------------------------- + +These widgets will take other widgets that inherit from :class:`~ILeftBody`, +:class:`ILeftBodyTouch`, :class:`~IRightBody` or :class:`~IRightBodyTouch` and +put them in their corresponding container. + +As the name implies, :class:`~ILeftBody` and :class:`~IRightBody` will signal +that the widget goes into the left or right container, respectively. + +:class:`~ILeftBodyTouch` and :class:`~IRightBodyTouch` do the same thing, +except these widgets will also receive touch events that occur within their +surfaces. + +`KivyMD` provides base classes such as :class:`~ImageLeftWidget`, +:class:`~ImageRightWidget`, :class:`~IconRightWidget`, :class:`~IconLeftWidget`, +based on the above classes. + +.. rubric:: Allows the use of items with custom widgets on the left. + +- OneLineAvatarListItem_ +- TwoLineAvatarListItem_ +- ThreeLineAvatarListItem_ +- OneLineIconListItem_ +- TwoLineIconListItem_ +- ThreeLineIconListItem_ + +.. rubric:: It allows the use of elements with custom widgets on the left + and the right. + +- OneLineAvatarIconListItem_ +- TwoLineAvatarIconListItem_ +- ThreeLineAvatarIconListItem_ + +Usage +----- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.list import OneLineListItem + + KV = ''' + ScrollView: + + MDList: + id: container + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for i in range(20): + self.root.ids.container.add_widget( + OneLineListItem(text=f"Single-line item {i}") + ) + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/lists.gif + :align: center + +Events of List +-------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + ScrollView: + + MDList: + + OneLineAvatarIconListItem: + on_release: print("Click!") + + IconLeftWidget: + icon: "github" + + OneLineAvatarIconListItem: + on_release: print("Click 2!") + + IconLeftWidget: + icon: "gitlab" + ''' + + + class MainApp(MDApp): + def build(self): + return Builder.load_string(KV) + + + MainApp().run() + +.. OneLineListItem: +OneLineListItem +--------------- + +.. code-block:: kv + + OneLineListItem: + text: "Single-line item" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/OneLineListItem.png + :align: center + +.. TwoLineListItem: +TwoLineListItem +--------------- + +.. code-block:: kv + + TwoLineListItem: + text: "Two-line item" + secondary_text: "Secondary text here" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/TwoLineListItem.png + :align: center + +.. ThreeLineListItem: +ThreeLineListItem +----------------- + +.. code-block:: kv + + ThreeLineListItem: + text: "Three-line item" + secondary_text: "This is a multi-line label where you can" + tertiary_text: "fit more text than usual" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ThreeLineListItem.png + :align: center + +.. OneLineAvatarListItem: +OneLineAvatarListItem +--------------------- + +.. code-block:: kv + + OneLineAvatarListItem: + text: "Single-line item with avatar" + + ImageLeftWidget: + source: "data/logo/kivy-icon-256.png" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/lists-map.png + :align: center + +.. TwoLineAvatarListItem: +TwoLineAvatarListItem +--------------------- + +.. code-block:: kv + + TwoLineAvatarListItem: + text: "Two-line item with avatar" + secondary_text: "Secondary text here" + + ImageLeftWidget: + source: "data/logo/kivy-icon-256.png" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/TwoLineAvatarListItem.png + :align: center + + +.. ThreeLineAvatarListItem: +ThreeLineAvatarListItem +----------------------- + +.. code-block:: kv + + ThreeLineAvatarListItem: + text: "Three-line item with avatar" + secondary_text: "Secondary text here" + tertiary_text: "fit more text than usual" + + ImageLeftWidget: + source: "data/logo/kivy-icon-256.png" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ThreeLineAvatarListItem.png + :align: center + +.. OneLineIconListItem: +OneLineIconListItem +------------------- + +.. code-block:: kv + + OneLineAvatarListItem: + text: "Single-line item with avatar" + + IconLeftWidget: + icon: "language-python" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/OneLineIconListItem.png + :align: center + +.. TwoLineIconListItem: +TwoLineIconListItem +------------------- + +.. code-block:: kv + + TwoLineIconListItem: + text: "Two-line item with avatar" + secondary_text: "Secondary text here" + + IconLeftWidget: + icon: "language-python" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/TwoLineIconListItem.png + :align: center + +.. ThreeLineIconListItem: +ThreeLineIconListItem +--------------------- + +.. code-block:: kv + + ThreeLineIconListItem: + text: "Three-line item with avatar" + secondary_text: "Secondary text here" + tertiary_text: "fit more text than usual" + + IconLeftWidget: + icon: "language-python" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ThreeLineIconListItem.png + :align: center + +.. OneLineAvatarIconListItem: +OneLineAvatarIconListItem +------------------------- + +.. code-block:: kv + + OneLineAvatarIconListItem: + text: "One-line item with avatar" + + IconLeftWidget: + icon: "plus" + + IconRightWidget: + icon: "minus" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/OneLineAvatarIconListItem.png + :align: center + +.. TwoLineAvatarIconListItem: +TwoLineAvatarIconListItem +------------------------- + +.. code-block:: kv + + TwoLineAvatarIconListItem: + text: "Two-line item with avatar" + secondary_text: "Secondary text here" + + IconLeftWidget: + icon: "plus" + + IconRightWidget: + icon: "minus" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/TwoLineAvatarIconListItem.png + :align: center + +.. ThreeLineAvatarIconListItem: +ThreeLineAvatarIconListItem +--------------------------- + +.. code-block:: kv + + ThreeLineAvatarIconListItem: + text: "Three-line item with avatar" + secondary_text: "Secondary text here" + tertiary_text: "fit more text than usual" + + IconLeftWidget: + icon: "plus" + + IconRightWidget: + icon: "minus" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ThreeLineAvatarIconListItem.png + :align: center + +Custom list item +---------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import StringProperty + + from kivymd.app import MDApp + from kivymd.uix.list import IRightBodyTouch, OneLineAvatarIconListItem + from kivymd.uix.selectioncontrol import MDCheckbox + from kivymd.icon_definitions import md_icons + + + KV = ''' + : + + IconLeftWidget: + icon: root.icon + + RightCheckbox: + + + BoxLayout: + + ScrollView: + + MDList: + id: scroll + ''' + + + class ListItemWithCheckbox(OneLineAvatarIconListItem): + '''Custom list item.''' + + icon = StringProperty("android") + + + class RightCheckbox(IRightBodyTouch, MDCheckbox): + '''Custom right container.''' + + + class MainApp(MDApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self): + icons = list(md_icons.keys()) + for i in range(30): + self.root.ids.scroll.add_widget( + ListItemWithCheckbox(text=f"Item {i}", icon=icons[i]) + ) + + + MainApp().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-list-item.png + :align: center + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.list import IRightBodyTouch + + KV = ''' + OneLineAvatarIconListItem: + text: "One-line item with avatar" + on_size: + self.ids._right_container.width = container.width + self.ids._right_container.x = container.width + + IconLeftWidget: + icon: "cog" + + Container: + id: container + + MDIconButton: + icon: "minus" + + MDIconButton: + icon: "plus" + ''' + + + class Container(IRightBodyTouch, MDBoxLayout): + adaptive_width = True + + + class MainApp(MDApp): + def build(self): + return Builder.load_string(KV) + + + MainApp().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-list-right-container.png + :align: center +""" + +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ListProperty, + NumericProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.image import Image + +import kivymd.material_resources as m_res +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import RectangularRippleBehavior +from kivymd.uix.button import MDIconButton +from kivymd.uix.gridlayout import MDGridLayout +from kivymd.uix.selectioncontrol import MDCheckbox + +Builder.load_string( + """ +#:import m_res kivymd.material_resources + + + + cols: 1 + adaptive_height: True + padding: 0, self._list_vertical_padding + + + + size_hint_y: None + canvas: + Color: + rgba: + self.theme_cls.divider_color if root.divider is not None\ + else (0, 0, 0, 0) + Line: + points: (root.x ,root.y, root.x+self.width, root.y)\ + if root.divider == 'Full' else\ + (root.x+root._txt_left_pad, root.y,\ + root.x+self.width-root._txt_left_pad-root._txt_right_pad,\ + root.y) + Color: + rgba: root.bg_color if root.bg_color else (0, 0, 0, 0) + Rectangle: + pos: self.pos + size: self.size + + BoxLayout: + id: _text_container + orientation: 'vertical' + pos: root.pos + padding: + root._txt_left_pad, root._txt_top_pad,\ + root._txt_right_pad, root._txt_bot_pad + + MDLabel: + id: _lbl_primary + text: root.text + font_style: root.font_style + theme_text_color: root.theme_text_color + text_color: root.text_color + size_hint_y: None + height: self.texture_size[1] + markup: True + shorten_from: 'right' + shorten: True + + MDLabel: + id: _lbl_secondary + text: '' if root._num_lines == 1 else root.secondary_text + font_style: root.secondary_font_style + theme_text_color: root.secondary_theme_text_color + text_color: root.secondary_text_color + size_hint_y: None + height: 0 if root._num_lines == 1 else self.texture_size[1] + shorten: True + shorten_from: 'right' + markup: True + + MDLabel: + id: _lbl_tertiary + text: '' if root._num_lines == 1 else root.tertiary_text + font_style: root.tertiary_font_style + theme_text_color: root.tertiary_theme_text_color + text_color: root.tertiary_text_color + size_hint_y: None + height: 0 if root._num_lines == 1 else self.texture_size[1] + shorten: True + shorten_from: 'right' + markup: True + + + + + BoxLayout: + id: _left_container + size_hint: None, None + x: root.x + dp(16) + y: root.y + root.height/2 - self.height/2 + size: dp(40), dp(40) + + + + + BoxLayout: + id: _left_container + size_hint: None, None + x: root.x + dp(16) + y: root.y + root.height - root._txt_top_pad - self.height - dp(5) + size: dp(40), dp(40) + + + + + 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) + + + + BoxLayout: + id: _left_container + size_hint: None, None + x: root.x + dp(16) + y: root.y + root.height - root._txt_top_pad - self.height - dp(5) + size: dp(48), dp(48) + + + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height/2 - self.height/2 + size: dp(48), dp(48) + + + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height/2 - self.height/2 + size: dp(48), dp(48) + + + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height/2 - self.height/2 + size: dp(48), dp(48) + + + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height/2 - self.height/2 + size: dp(48), dp(48) + + + + + BoxLayout: + id: _right_container + size_hint: None, None + x: root.x + root.width - m_res.HORIZ_MARGINS - self.width + y: root.y + root.height - root._txt_top_pad - self.height - dp(5) + size: dp(48), dp(48) +""" +) + + +class MDList(MDGridLayout): + """ListItem container. Best used in conjunction with a + :class:`kivy.uix.ScrollView`. + + When adding (or removing) a widget, it will resize itself to fit its + children, plus top and bottom paddings as described by the `MD` spec. + """ + + _list_vertical_padding = NumericProperty("8dp") + + def add_widget(self, widget, index=0, canvas=None): + super().add_widget(widget, index, canvas) + self.height += widget.height + + def remove_widget(self, widget): + super().remove_widget(widget) + self.height -= widget.height + + +class BaseListItem( + ThemableBehavior, RectangularRippleBehavior, ButtonBehavior, FloatLayout +): + """ + Base class to all ListItems. Not supposed to be instantiated on its own. + """ + + text = StringProperty() + """ + Text shown in the first line. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + text_color = ListProperty(None) + """ + Text color in ``rgba`` format used if :attr:`~theme_text_color` is set + to `'Custom'`. + + :attr:`text_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `None`. + """ + + font_style = StringProperty("Subtitle1") + """ + Text font style. See ``kivymd.font_definitions.py``. + + :attr:`font_style` is a :class:`~kivy.properties.StringProperty` + and defaults to `'Subtitle1'`. + """ + + theme_text_color = StringProperty("Primary", allownone=True) + """ + Theme text color in ``rgba`` format for primary text. + + :attr:`theme_text_color` is a :class:`~kivy.properties.StringProperty` + and defaults to `'Primary'`. + """ + + secondary_text = StringProperty() + """ + Text shown in the second line. + + :attr:`secondary_text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + tertiary_text = StringProperty() + """ + The text is displayed on the third line. + + :attr:`tertiary_text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + secondary_text_color = ListProperty(None) + """ + Text color in ``rgba`` format used for secondary text + if :attr:`~secondary_theme_text_color` is set to `'Custom'`. + + :attr:`secondary_text_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `None`. + """ + + tertiary_text_color = ListProperty(None) + """ + Text color in ``rgba`` format used for tertiary text + if :attr:`~tertiary_theme_text_color` is set to 'Custom'. + + :attr:`tertiary_text_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `None`. + """ + + secondary_theme_text_color = StringProperty("Secondary", allownone=True) + """ + Theme text color for secondary text. + + :attr:`secondary_theme_text_color` is a :class:`~kivy.properties.StringProperty` + and defaults to `'Secondary'`. + """ + + tertiary_theme_text_color = StringProperty("Secondary", allownone=True) + """ + Theme text color for tertiary text. + + :attr:`tertiary_theme_text_color` is a :class:`~kivy.properties.StringProperty` + and defaults to `'Secondary'`. + """ + + secondary_font_style = StringProperty("Body1") + """ + Font style for secondary line. See ``kivymd.font_definitions.py``. + + :attr:`secondary_font_style` is a :class:`~kivy.properties.StringProperty` + and defaults to `'Body1'`. + """ + + tertiary_font_style = StringProperty("Body1") + """ + Font style for tertiary line. See ``kivymd.font_definitions.py``. + + :attr:`tertiary_font_style` is a :class:`~kivy.properties.StringProperty` + and defaults to `'Body1'`. + """ + + divider = OptionProperty( + "Full", options=["Full", "Inset", None], allownone=True + ) + """ + Divider mode. Available options are: `'Full'`, `'Inset'` + and default to `'Full'`. + + :attr:`divider` is a :class:`~kivy.properties.OptionProperty` + and defaults to `'Full'`. + """ + + bg_color = ListProperty() + """ + Background color for menu item. + + :attr:`bg_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + _txt_left_pad = NumericProperty("16dp") + _txt_top_pad = NumericProperty() + _txt_bot_pad = NumericProperty() + _txt_right_pad = NumericProperty(m_res.HORIZ_MARGINS) + _num_lines = 3 + _no_ripple_effect = BooleanProperty(False) + + +class ILeftBody: + """ + Pseudo-interface for widgets that go in the left container for + ListItems that support it. + + Implements nothing and requires no implementation, for annotation only. + """ + + pass + + +class ILeftBodyTouch: + """ + Same as :class:`~ILeftBody`, but allows the widget to receive touch + events instead of triggering the ListItem's ripple effect. + """ + + pass + + +class IRightBody: + """ + Pseudo-interface for widgets that go in the right container for + ListItems that support it. + + Implements nothing and requires no implementation, for annotation only. + """ + + pass + + +class IRightBodyTouch: + """ + Same as :class:`~IRightBody`, but allows the widget to receive touch + events instead of triggering the ``ListItem``'s ripple effect + """ + + pass + + +class ContainerSupport: + """ + Overrides ``add_widget`` in a ``ListItem`` to include support + for ``I*Body`` widgets when the appropiate containers are present. + """ + + _touchable_widgets = ListProperty() + + def add_widget(self, widget, index=0): + if issubclass(widget.__class__, ILeftBody): + self.ids._left_container.add_widget(widget) + elif issubclass(widget.__class__, ILeftBodyTouch): + self.ids._left_container.add_widget(widget) + self._touchable_widgets.append(widget) + elif issubclass(widget.__class__, IRightBody): + self.ids._right_container.add_widget(widget) + elif issubclass(widget.__class__, IRightBodyTouch): + self.ids._right_container.add_widget(widget) + self._touchable_widgets.append(widget) + else: + return super().add_widget(widget) + + def remove_widget(self, widget): + super().remove_widget(widget) + if widget in self._touchable_widgets: + self._touchable_widgets.remove(widget) + + def on_touch_down(self, touch): + if self.propagate_touch_to_touchable_widgets(touch, "down"): + return + super().on_touch_down(touch) + + def on_touch_move(self, touch, *args): + if self.propagate_touch_to_touchable_widgets(touch, "move", *args): + return + super().on_touch_move(touch, *args) + + def on_touch_up(self, touch): + if self.propagate_touch_to_touchable_widgets(touch, "up"): + return + super().on_touch_up(touch) + + def propagate_touch_to_touchable_widgets(self, touch, touch_event, *args): + triggered = False + for i in self._touchable_widgets: + if i.collide_point(touch.x, touch.y): + triggered = True + if touch_event == "down": + i.on_touch_down(touch) + elif touch_event == "move": + i.on_touch_move(touch, *args) + elif touch_event == "up": + i.on_touch_up(touch) + return triggered + + +class OneLineListItem(BaseListItem): + """A one line list item.""" + + _txt_top_pad = NumericProperty("16dp") + _txt_bot_pad = NumericProperty("15dp") # dp(20) - dp(5) + _height = NumericProperty() + _num_lines = 1 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.height = dp(48) if not self._height else self._height + + +class TwoLineListItem(BaseListItem): + """A two line list item.""" + + _txt_top_pad = NumericProperty("20dp") + _txt_bot_pad = NumericProperty("15dp") # dp(20) - dp(5) + _height = NumericProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.height = dp(72) if not self._height else self._height + + +class ThreeLineListItem(BaseListItem): + """A three line list item.""" + + _txt_top_pad = NumericProperty("16dp") + _txt_bot_pad = NumericProperty("15dp") # dp(20) - dp(5) + _height = NumericProperty() + _num_lines = 3 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.height = dp(88) if not self._height else self._height + + +class OneLineAvatarListItem(ContainerSupport, BaseListItem): + _txt_left_pad = NumericProperty("72dp") + _txt_top_pad = NumericProperty("20dp") + _txt_bot_pad = NumericProperty("19dp") # dp(24) - dp(5) + _height = NumericProperty() + _num_lines = 1 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.height = dp(56) if not self._height else self._height + + +class TwoLineAvatarListItem(OneLineAvatarListItem): + _txt_top_pad = NumericProperty("20dp") + _txt_bot_pad = NumericProperty("15dp") # dp(20) - dp(5) + _height = NumericProperty() + _num_lines = 2 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.height = dp(72) if not self._height else self._height + + +class ThreeLineAvatarListItem(ContainerSupport, ThreeLineListItem): + _txt_left_pad = NumericProperty("72dp") + + +class OneLineIconListItem(ContainerSupport, OneLineListItem): + _txt_left_pad = NumericProperty("72dp") + + +class TwoLineIconListItem(OneLineIconListItem): + _txt_top_pad = NumericProperty("20dp") + _txt_bot_pad = NumericProperty("15dp") # dp(20) - dp(5) + _height = NumericProperty() + _num_lines = 2 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.height = dp(72) if not self._height else self._height + + +class ThreeLineIconListItem(ContainerSupport, ThreeLineListItem): + _txt_left_pad = NumericProperty("72dp") + + +class OneLineRightIconListItem(ContainerSupport, OneLineListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty("40dp") + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS + + +class TwoLineRightIconListItem(OneLineRightIconListItem): + _txt_top_pad = NumericProperty("20dp") + _txt_bot_pad = NumericProperty("15dp") # dp(20) - dp(5) + _height = NumericProperty() + _num_lines = 2 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.height = dp(72) if not self._height else self._height + + +class ThreeLineRightIconListItem(ContainerSupport, ThreeLineListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty("40dp") + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS + + +class OneLineAvatarIconListItem(OneLineAvatarListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty("40dp") + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS + + +class TwoLineAvatarIconListItem(TwoLineAvatarListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty("40dp") + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS + + +class ThreeLineAvatarIconListItem(ThreeLineAvatarListItem): + # dp(40) = dp(16) + dp(24): + _txt_right_pad = NumericProperty("40dp") + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._txt_right_pad = dp(40) + m_res.HORIZ_MARGINS + + +class ImageLeftWidget(ILeftBody, Image): + pass + + +class ImageRightWidget(IRightBodyTouch, Image): + pass + + +class IconRightWidget(IRightBodyTouch, MDIconButton): + pass + + +class IconLeftWidget(ILeftBodyTouch, MDIconButton): + pass + + +class CheckboxLeftWidget(ILeftBodyTouch, MDCheckbox): + pass diff --git a/kivymd/uix/menu.py b/kivymd/uix/menu.py new file mode 100755 index 0000000..6774cf9 --- /dev/null +++ b/kivymd/uix/menu.py @@ -0,0 +1,1198 @@ +""" +Components/Menu +=============== + +.. seealso:: + + `Material Design spec, Menus `_ + +.. rubric:: Menus display a list of choices on temporary surfaces. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-previous.png + :align: center + +Usage +----- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + Screen: + + MDRaisedButton: + id: button + text: "PRESS ME" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.menu.open() + ''' + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [{"text": f"Item {i}"} for i in range(5)] + self.menu = MDDropdownMenu( + caller=self.screen.ids.button, + items=menu_items, + width_mult=4, + ) + self.menu.bind(on_release=self.menu_callback) + + def menu_callback(self, instance_menu, instance_menu_item): + print(instance_menu, instance_menu_item) + + def build(self): + return self.screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-usage.gif + :align: center + +.. Warning:: Do not create the :class:`~MDDropdownMenu` object when you open + the menu window. Because on a mobile device this one will be very slow! + +Wrong +----- + +.. code-block:: python + + menu = MDDropdownMenu(caller=self.screen.ids.button, items=menu_items) + menu.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-wrong.gif + :align: center + +Customization of menu item +-------------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-right.gif + :align: center + +You must create a new class that inherits from the :class:`~RightContent` class: + +.. code-block:: python + + class RightContentCls(RightContent): + pass + +Now in the KV rule you can create your own elements that will be displayed in +the menu item on the right: + +.. code-block:: kv + + + disabled: True + + MDIconButton: + icon: root.icon + user_font_size: "16sp" + pos_hint: {"center_y": .5} + + MDLabel: + text: root.text + font_style: "Caption" + size_hint_x: None + width: self.texture_size[0] + text_size: None, None + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-right-detail.png + :align: center + +Now create menu items as usual, but add the key ``right_content_cls`` whose +value is the class ``RightContentCls`` that you created: + +.. code-block:: python + + menu_items = [ + { + "right_content_cls": RightContentCls( + text=f"R+{i}", icon="apple-keyboard-command", + ), + "icon": "git", + "text": f"Item {i}", + } + for i in range(5) + ] + self.menu = MDDropdownMenu( + caller=self.screen.ids.button, items=menu_items, width_mult=4 + ) + +Full example +------------ + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu, RightContent + + KV = ''' + + disabled: True + + MDIconButton: + icon: root.icon + user_font_size: "16sp" + pos_hint: {"center_y": .5} + + MDLabel: + text: root.text + font_style: "Caption" + size_hint_x: None + width: self.texture_size[0] + text_size: None, None + + + Screen: + + MDRaisedButton: + id: button + text: "PRESS ME" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.menu.open() + ''' + + + class RightContentCls(RightContent): + pass + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [ + { + "right_content_cls": RightContentCls( + text=f"R+{i}", icon="apple-keyboard-command", + ), + "icon": "git", + "text": f"Item {i}", + } + for i in range(5) + ] + self.menu = MDDropdownMenu( + caller=self.screen.ids.button, items=menu_items, width_mult=4 + ) + self.menu.bind(on_release=self.menu_callback) + + def menu_callback(self, instance_menu, instance_menu_item): + instance_menu.dismiss() + + def build(self): + return self.screen + + + Test().run() + +Menu without icons on the left +------------------------------ + +If you do not want to use the icons in the menu items on the left, +then do not use the "icon" key when creating menu items: + +.. code-block:: python + + menu_items = [ + { + "right_content_cls": RightContentCls( + text=f"R+{i}", icon="apple-keyboard-command", + ), + "text": f"Item {i}", + } + for i in range(5) + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-without-icon.png + :align: center + +Item height adjustment +---------------------- + +.. code-block:: python + + menu_items = [ + { + "right_content_cls": RightContentCls( + text=f"R+{i}", icon="apple-keyboard-command", + ), + "text": f"Item {i}", + "height": "36dp", + "top_pad": "10dp", + "bot_pad": "10dp", + } + for i in range(5) + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-pad.png + :align: center + +Mixin items +----------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu, RightContent + + KV = ''' + + disabled: True + + MDIconButton: + icon: root.icon + user_font_size: "16sp" + pos_hint: {"center_y": .5} + + MDLabel: + text: root.text + font_style: "Caption" + size_hint_x: None + width: self.texture_size[0] + text_size: None, None + + + Screen: + + MDRaisedButton: + id: button + text: "PRESS ME" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.menu.open() + ''' + + + class RightContentCls(RightContent): + pass + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + + menu_items = [] + data = [ + {"": "Open"}, + {}, + {"open-in-app": "Open in app >"}, + {"trash-can-outline": "Move to Trash"}, + {"rename-box": "Rename"}, + {"zip-box-outline": "Create zip"}, + {}, + {"": "Properties"}, + ] + + for data_item in data: + if data_item: + if list(data_item.items())[0][1].endswith(">"): + menu_items.append( + { + "right_content_cls": RightContentCls( + icon="menu-right-outline", + ), + "icon": list(data_item.items())[0][0], + "text": list(data_item.items())[0][1][:-2], + "height": "36dp", + "top_pad": "10dp", + "bot_pad": "10dp", + "divider": None, + } + ) + else: + menu_items.append( + { + "text": list(data_item.items())[0][1], + "icon": list(data_item.items())[0][0], + "font_style": "Caption", + "height": "36dp", + "top_pad": "10dp", + "bot_pad": "10dp", + "divider": None, + } + ) + else: + menu_items.append( + {"viewclass": "MDSeparator", "height": 1} + ) + self.menu = MDDropdownMenu( + caller=self.screen.ids.button, + items=menu_items, + width_mult=4, + ) + self.menu.bind(on_release=self.menu_callback) + + def menu_callback(self, instance_menu, instance_menu_item): + print(instance_menu, instance_menu_item + + + def build(self): + return self.screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-mixin.png + :align: center + +Hover Behavior +-------------- + +.. code-block:: python + + self.menu = MDDropdownMenu( + ..., + ..., + selected_color=self.theme_cls.primary_dark_hue, + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-with-hover.gif + :align: center + +Create submenu +-------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + Screen: + + MDRaisedButton: + id: button + text: "PRESS ME" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.menu.open() + ''' + + + class CustomDrop(MDDropdownMenu): + def set_bg_color_items(self, instance_selected_item): + if self.selected_color and not MDApp.get_running_app().submenu: + for item in self.menu.ids.box.children: + if item is not instance_selected_item: + item.bg_color = (0, 0, 0, 0) + else: + instance_selected_item.bg_color = self.selected_color + + + class Test(MDApp): + submenu = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [ + { + "icon": "git", + "text": f"Item {i}" if i != 3 else "Open submenu", + } + for i in range(5) + ] + self.menu = CustomDrop( + caller=self.screen.ids.button, + items=menu_items, + width_mult=4, + selected_color=self.theme_cls.bg_darkest + ) + self.menu.bind(on_enter=self.check_item) + + def check_item(self, menu, item): + if item.text == "Open submenu" and not self.submenu: + menu_items = [{"text": f"Item {i}"} for i in range(5)] + self.submenu = MDDropdownMenu( + caller=item, + items=menu_items, + width_mult=4, + selected_color=self.theme_cls.bg_darkest, + ) + self.submenu.bind(on_dismiss=self.set_state_submenu) + self.submenu.open() + + def set_state_submenu(self, *args): + self.submenu = None + + def build(self): + return self.screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-submenu.gif + :align: center + +Menu with MDToolbar +------------------- + +.. Warning:: The :class:`~MDDropdownMenu` does not work with the standard + :class:`~kivymd.uix.toolbar.MDToolbar`. You can use your own + ``CustomToolbar`` and bind the menu window output to its elements. + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + from kivymd.theming import ThemableBehavior + from kivymd.uix.behaviors import RectangularElevationBehavior + from kivymd.uix.boxlayout import MDBoxLayout + + KV = ''' + : + size_hint_y: None + height: self.theme_cls.standard_increment + padding: "5dp" + spacing: "12dp" + + MDIconButton: + id: button_1 + icon: "menu" + pos_hint: {"center_y": .5} + on_release: app.menu_1.open() + + MDLabel: + text: "MDDropdownMenu" + pos_hint: {"center_y": .5} + size_hint_x: None + width: self.texture_size[0] + text_size: None, None + font_style: 'H6' + + Widget: + + MDIconButton: + id: button_2 + icon: "dots-vertical" + pos_hint: {"center_y": .5} + on_release: app.menu_2.open() + + + Screen: + + CustomToolbar: + id: toolbar + elevation: 10 + pos_hint: {"top": 1} + ''' + + + class CustomToolbar( + ThemableBehavior, RectangularElevationBehavior, MDBoxLayout, + ): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.md_bg_color = self.theme_cls.primary_color + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + self.menu_1 = self.create_menu( + "Button menu", self.screen.ids.toolbar.ids.button_1, + ) + self.menu_2 = self.create_menu( + "Button dots", self.screen.ids.toolbar.ids.button_2, + ) + + def create_menu(self, text, instance): + menu_items = [{"icon": "git", "text": text} for i in range(5)] + menu = MDDropdownMenu(caller=instance, items=menu_items, width_mult=5) + menu.bind(on_release=self.menu_callback) + return menu + + def menu_callback(self, instance_menu, instance_menu_item): + instance_menu.dismiss() + + def build(self): + return self.screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-with-toolbar.gif + :align: center + +Position menu +============= + +Bottom position +--------------- + +.. seealso:: + + :attr:`~MDDropdownMenu.position` + +.. code-block:: python + + from kivy.clock import Clock + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + Screen + + MDTextField: + id: field + pos_hint: {'center_x': .5, 'center_y': .5} + size_hint_x: None + width: "200dp" + hint_text: "Password" + on_focus: if self.focus: app.menu.open() + ''' + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [{"icon": "git", "text": f"Item {i}"} for i in range(5)] + self.menu = MDDropdownMenu( + caller=self.screen.ids.field, + items=menu_items, + position="bottom", + width_mult=4, + ) + self.menu.bind(on_release=self.set_item) + + def set_item(self, instance_menu, instance_menu_item): + def set_item(interval): + self.screen.ids.field.text = instance_menu_item.text + instance_menu.dismiss() + Clock.schedule_once(set_item, 0.5) + + def build(self): + return self.screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.gif + :align: center + +Center position +--------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + Screen + + MDDropDownItem: + id: drop_item + pos_hint: {'center_x': .5, 'center_y': .5} + text: 'Item 0' + on_release: app.menu.open() + ''' + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [{"icon": "git", "text": f"Item {i}"} for i in range(5)] + self.menu = MDDropdownMenu( + caller=self.screen.ids.drop_item, + items=menu_items, + position="center", + width_mult=4, + ) + self.menu.bind(on_release=self.set_item) + + def set_item(self, instance_menu, instance_menu_item): + self.screen.ids.drop_item.set_item(instance_menu_item.text) + self.menu.dismiss() + + def build(self): + return self.screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position-center.gif + :align: center +""" + +__all__ = ("MDDropdownMenu", "RightContent") + +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 ( + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.scrollview import ScrollView + +import kivymd.material_resources as m_res +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import HoverBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.list import ( + IRightBodyTouch, + OneLineAvatarIconListItem, + OneLineListItem, + OneLineRightIconListItem, +) + +Builder.load_string( + """ +#:import STD_INC kivymd.material_resources.STANDARD_INCREMENT + + + + adaptive_width: True + + + + + IconLeftWidget: + id: icon_widget + icon: root.icon + + + + size_hint: None, None + width: root.width_mult * STD_INC + bar_width: 0 + + MDGridLayout: + id: box + cols: 1 + adaptive_height: True + + + + + MDCard: + id: card + elevation: 10 + size_hint: None, None + size: md_menu.size + pos: md_menu.pos + md_bg_color: 0, 0, 0, 0 + opacity: md_menu.opacity + + canvas: + Clear + Color: + rgba: root.background_color if root.background_color else root.theme_cls.bg_dark + RoundedRectangle: + size: self.size + pos: self.pos + radius: [root.radius,] + + MDMenu: + id: md_menu + drop_cls: root + width_mult: root.width_mult + size_hint: None, None + size: 0, 0 + opacity: 0 +""" +) + + +class RightContent(IRightBodyTouch, MDBoxLayout): + text = StringProperty() + """ + Text item. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon = StringProperty() + """ + Icon item. + + :attr:`icon` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + +class MDMenuItemBase(HoverBehavior): + """ + Base class for MenuItem + """ + + def on_enter(self): + self.parent.parent.drop_cls.set_bg_color_items(self) + self.parent.parent.drop_cls.dispatch("on_enter", self) + + def on_leave(self): + self.parent.parent.drop_cls.dispatch("on_leave", self) + + +class MDMenuItemIcon(MDMenuItemBase, OneLineAvatarIconListItem): + icon = StringProperty() + """ + Icon item. + + :attr:`icon` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + +class MDMenuItem(MDMenuItemBase, OneLineListItem): + + pass + + +class MDMenuItemRight(MDMenuItemBase, OneLineRightIconListItem): + + pass + + +class MDMenu(ScrollView): + width_mult = NumericProperty(1) + """ + See :attr:`~MDDropdownMenu.width_mult`. + """ + + drop_cls = ObjectProperty() + """ + See :class:`~MDDropdownMenu` class. + """ + + +class MDDropdownMenu(ThemableBehavior, FloatLayout): + """ + :Events: + :attr:`on_enter` + Call when mouse enter the bbox of item menu. + :attr:`on_leave` + Call when the mouse exit the item menu. + :attr:`on_dismiss` + Call when closes menu. + :attr:`on_release` + The method that will be called when you click menu items. + """ + + selected_color = ListProperty() + """Custom color (``rgba`` format) for list item when hover behavior occurs. + + :attr:`selected_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + items = ListProperty() + """ + See :attr:`~kivy.uix.recycleview.RecycleView.data`. + + :attr:`items` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + width_mult = NumericProperty(1) + """ + This number multiplied by the standard increment (56dp on mobile, + 64dp on desktop, determines the width of the menu items. + + If the resulting number were to be too big for the application Window, + the multiplier will be adjusted for the biggest possible one. + + :attr:`width_mult` is a :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + max_height = NumericProperty() + """ + The menu will grow no bigger than this number. Set to 0 for no limit. + + :attr:`max_height` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + border_margin = NumericProperty("4dp") + """ + Margin between Window border and menu. + + :attr:`border_margin` is a :class:`~kivy.properties.NumericProperty` + and defaults to `4dp`. + """ + + ver_growth = OptionProperty(None, allownone=True, options=["up", "down"]) + """ + Where the menu will grow vertically to when opening. Set to None to let + the widget pick for you. Available options are: `'up'`, `'down'`. + + :attr:`ver_growth` is a :class:`~kivy.properties.OptionProperty` + and defaults to `None`. + """ + + hor_growth = OptionProperty(None, allownone=True, options=["left", "right"]) + """ + Where the menu will grow horizontally to when opening. Set to None to let + the widget pick for you. Available options are: `'left'`, `'right'`. + + :attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty` + and defaults to `None`. + """ + + background_color = ListProperty() + """ + Color of the background of the menu. + + :attr:`background_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + opening_transition = StringProperty("out_cubic") + """ + Type of animation for opening a menu window. + + :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_cubic'`. + """ + + opening_time = NumericProperty(0.2) + """ + Menu window opening animation time. + + :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + caller = ObjectProperty() + """ + The widget object that caller the menu window. + + :attr:`caller` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + position = OptionProperty("auto", options=["auto", "center", "bottom"]) + """ + Menu window position relative to parent element. + Available options are: `'auto'`, `'center'`, `'bottom'`. + + :attr:`position` is a :class:`~kivy.properties.OptionProperty` + and defaults to `'auto'`. + """ + + radius = NumericProperty(7) + """ + Menu radius. + + :attr:`radius` is a :class:`~kivy.properties.NumericProperty` + and defaults to `'7'`. + """ + + _start_coords = [] + _calculate_complete = False + _calculate_process = False + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Window.bind(on_resize=self.check_position_caller) + Window.bind(on_maximize=self.set_menu_properties) + Window.bind(on_restore=self.set_menu_properties) + self.register_event_type("on_dismiss") + self.register_event_type("on_enter") + self.register_event_type("on_leave") + self.register_event_type("on_release") + self.menu = self.ids.md_menu + self.target_height = 0 + + def check_position_caller(self, instance, width, height): + self.set_menu_properties(0) + + def set_bg_color_items(self, instance_selected_item): + """Called when a Hover Behavior event occurs for a list item. + + :type instance_selected_item: + """ + + if self.selected_color: + for item in self.menu.ids.box.children: + if item is not instance_selected_item: + item.bg_color = (0, 0, 0, 0) + else: + instance_selected_item.bg_color = self.selected_color + + def create_menu_items(self): + """Creates menu items.""" + + for data in self.items: + if data.get("icon") and data.get("right_content_cls", None): + + item = MDMenuItemIcon( + text=data.get("text", ""), + divider=data.get("divider", "Full"), + _txt_top_pad=data.get("top_pad", "20dp"), + _txt_bot_pad=data.get("bot_pad", "20dp"), + ) + + elif data.get("icon"): + item = MDMenuItemIcon( + text=data.get("text", ""), + divider=data.get("divider", "Full"), + _txt_top_pad=data.get("top_pad", "20dp"), + _txt_bot_pad=data.get("bot_pad", "20dp"), + ) + + elif data.get("right_content_cls", None): + item = MDMenuItemRight( + text=data.get("text", ""), + divider=data.get("divider", "Full"), + _txt_top_pad=data.get("top_pad", "20dp"), + _txt_bot_pad=data.get("bot_pad", "20dp"), + ) + + else: + + item = MDMenuItem( + text=data.get("text", ""), + divider=data.get("divider", "Full"), + _txt_top_pad=data.get("top_pad", "20dp"), + _txt_bot_pad=data.get("bot_pad", "20dp"), + ) + + # Set height item. + if data.get("height", ""): + item.height = data.get("height") + # Compensate icon area by some left padding. + if not data.get("icon"): + item._txt_left_pad = data.get("left_pad", "32dp") + # Set left icon. + else: + item.icon = data.get("icon", "") + item.bind(on_release=lambda x=item: self.dispatch("on_release", x)) + right_content_cls = data.get("right_content_cls", None) + # Set right content. + if isinstance(right_content_cls, RightContent): + item.ids._right_container.width = right_content_cls.width + dp( + 20 + ) + item.ids._right_container.padding = ("10dp", 0, 0, 0) + item.add_widget(right_content_cls) + else: + if "_right_container" in item.ids: + item.ids._right_container.width = 0 + self.menu.ids.box.add_widget(item) + + def set_menu_properties(self, interval=0): + """Sets the size and position for the menu window.""" + + if self.caller: + if not self.menu.ids.box.children: + self.create_menu_items() + # We need to pick a starting point, see how big we need to be, + # and where to grow to. + self._start_coords = self.caller.to_window( + self.caller.center_x, self.caller.center_y + ) + self.target_width = self.width_mult * m_res.STANDARD_INCREMENT + + # If we're wider than the Window... + if self.target_width > Window.width: + # ...reduce our multiplier to max allowed. + self.target_width = ( + int(Window.width / m_res.STANDARD_INCREMENT) + * m_res.STANDARD_INCREMENT + ) + + # Set the target_height of the menu depending on the size of + # each MDMenuItem or MDMenuItemIcon + self.target_height = 0 + for item in self.menu.ids.box.children: + self.target_height += item.height + + # If we're over max_height... + if 0 < self.max_height < self.target_height: + self.target_height = self.max_height + + # Establish vertical growth direction. + if self.ver_growth is not None: + ver_growth = self.ver_growth + else: + # If there's enough space below us: + if ( + self.target_height + <= self._start_coords[1] - self.border_margin + ): + ver_growth = "down" + # if there's enough space above us: + elif ( + self.target_height + < Window.height - self._start_coords[1] - self.border_margin + ): + ver_growth = "up" + # Otherwise, let's pick the one with more space and adjust ourselves. + else: + # If there"s more space below us: + if ( + self._start_coords[1] + >= Window.height - self._start_coords[1] + ): + ver_growth = "down" + self.target_height = ( + self._start_coords[1] - self.border_margin + ) + # If there's more space above us: + else: + ver_growth = "up" + self.target_height = ( + Window.height + - self._start_coords[1] + - self.border_margin + ) + + if self.hor_growth is not None: + hor_growth = self.hor_growth + else: + # If there's enough space to the right: + if ( + self.target_width + <= Window.width - self._start_coords[0] - self.border_margin + ): + hor_growth = "right" + # if there's enough space to the left: + elif ( + self.target_width + < self._start_coords[0] - self.border_margin + ): + hor_growth = "left" + # Otherwise, let's pick the one with more space and adjust ourselves. + else: + # if there"s more space to the right: + if ( + Window.width - self._start_coords[0] + >= self._start_coords[0] + ): + hor_growth = "right" + self.target_width = ( + Window.width + - self._start_coords[0] + - self.border_margin + ) + # if there"s more space to the left: + else: + hor_growth = "left" + self.target_width = ( + self._start_coords[0] - self.border_margin + ) + + if ver_growth == "down": + self.tar_y = self._start_coords[1] - self.target_height + else: # should always be "up" + self.tar_y = self._start_coords[1] + + if hor_growth == "right": + self.tar_x = self._start_coords[0] + else: # should always be "left" + self.tar_x = self._start_coords[0] - self.target_width + self._calculate_complete = True + + def open(self): + """Animate the opening of a menu window.""" + + def open(interval): + if not self._calculate_complete: + return + if self.position == "auto": + self.menu.pos = self._start_coords + anim = Animation( + x=self.tar_x, + y=self.tar_y, + width=self.target_width, + height=self.target_height, + duration=self.opening_time, + opacity=1, + transition=self.opening_transition, + ) + anim.start(self.menu) + else: + if self.position == "center": + self.menu.pos = ( + self._start_coords[0] - self.target_width / 2, + self._start_coords[1] - self.target_height / 2, + ) + elif self.position == "bottom": + self.menu.pos = ( + self._start_coords[0] - self.target_width / 2, + self.caller.pos[1] - self.target_height, + ) + anim = Animation( + width=self.target_width, + height=self.target_height, + duration=self.opening_time, + opacity=1, + transition=self.opening_transition, + ) + anim.start(self.menu) + Window.add_widget(self) + Clock.unschedule(open) + self._calculate_process = False + + self.set_menu_properties() + if not self._calculate_process: + self._calculate_process = True + Clock.schedule_interval(open, 0) + + def on_touch_down(self, touch): + if not self.menu.collide_point(*touch.pos): + self.dispatch("on_dismiss") + return True + super().on_touch_down(touch) + return True + + def on_touch_move(self, touch): + super().on_touch_move(touch) + return True + + def on_touch_up(self, touch): + super().on_touch_up(touch) + return True + + def on_enter(self, instance): + """Call when mouse enter the bbox of the item of menu.""" + + def on_leave(self, instance): + """Call when the mouse exit the item of menu.""" + + def on_release(self, *args): + """The method that will be called when you click menu items.""" + + def on_dismiss(self): + """Called when the menu is closed.""" + + Window.remove_widget(self) + self.menu.width = 0 + self.menu.height = 0 + self.menu.opacity = 0 + + def dismiss(self): + """Closes the menu.""" + + self.on_dismiss() diff --git a/kivymd/uix/navigationdrawer.py b/kivymd/uix/navigationdrawer.py new file mode 100755 index 0000000..8546404 --- /dev/null +++ b/kivymd/uix/navigationdrawer.py @@ -0,0 +1,754 @@ +""" +Components/Navigation Drawer +============================ + +.. seealso:: + + `Material Design spec, 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. + : + 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 + + : + 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 = ''' + : + + 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 `_ +""" + +__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 + + +: + 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 diff --git a/kivymd/uix/picker.py b/kivymd/uix/picker.py new file mode 100755 index 0000000..24c28ff --- /dev/null +++ b/kivymd/uix/picker.py @@ -0,0 +1,1114 @@ +""" +Components/Pickers +================== + +Includes date, time and color picker + +`KivyMD` provides the following classes for use: + +- MDTimePicker_ +- MDDatePicker_ +- MDThemePicker_ + +.. MDTimePicker: +MDTimePicker +------------ + +.. rubric:: Usage + +.. code-block:: + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.picker import MDTimePicker + + KV = ''' + FloatLayout: + + MDRaisedButton: + text: "Open time picker" + pos_hint: {'center_x': .5, 'center_y': .5} + on_release: app.show_time_picker() + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def show_time_picker(self): + '''Open time picker dialog.''' + + time_dialog = MDTimePicker() + time_dialog.open() + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDTimePicker.gif + :align: center + +Binding method returning set time +--------------------------------- + +.. code-block:: python + + def show_time_picker(self): + time_dialog = MDTimePicker() + time_dialog.bind(time=self.get_time) + time_dialog.open() + + def get_time(self, instance, time): + ''' + The method returns the set time. + + :type instance: + :type time: + ''' + + return time + +Open time dialog with the specified time +---------------------------------------- + +Use the :attr:`~MDTimePicker.set_time` method of the +:class:`~MDTimePicker.` class. + +.. code-block:: python + + def show_time_picker(self): + from datetime import datetime + + # Must be a datetime object + previous_time = datetime.strptime("03:20:00", '%H:%M:%S').time() + time_dialog = MDTimePicker() + time_dialog.set_time(previous_time) + time_dialog.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/previous-time.png + :align: center + +.. MDDatePicker: +MDDatePicker +------------ + +When creating an instance of the :class:`~MDDatePicker` class, you must pass +as a parameter a method that will take one argument - a ``datetime`` object. + +.. code-block:: python + + def get_date(self, date): + ''' + :type date: + ''' + + def show_date_picker(self): + date_dialog = MDDatePicker(callback=self.get_date) + date_dialog.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDDatePicker.gif + :align: center + +Open date dialog with the specified date +---------------------------------------- + +.. code-block:: python + + def show_date_picker(self): + date_dialog = MDDatePicker( + callback=self.get_date, + year=2010, + month=2, + day=12, + ) + date_dialog.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/previous-date.png + :align: center + +You can set the time interval from and to the set date. All days of the week +that are not included in this range will have the status `disabled`. + +.. code-block:: python + + def show_date_picker(self): + min_date = datetime.strptime("2020:02:15", '%Y:%m:%d').date() + max_date = datetime.strptime("2020:02:20", '%Y:%m:%d').date() + date_dialog = MDDatePicker( + callback=self.get_date, + min_date=min_date, + max_date=max_date, + ) + date_dialog.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/range-date.png + :align: center + +.. MDThemePicker: +MDThemePicker +------------- + +.. code-block:: python + + def show_theme_picker(self): + theme_dialog = MDThemePicker() + theme_dialog.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/MDThemePicker.gif + :align: center +""" + +__all__ = ("MDTimePicker", "MDDatePicker", "MDThemePicker") + +import calendar +import datetime +from datetime import date + +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.lang import Builder +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.floatlayout import FloatLayout +from kivy.uix.modalview import ModalView +from kivy.utils import get_color_from_hex + +from kivymd.color_definitions import colors, palette +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + CircularRippleBehavior, + RectangularElevationBehavior, + SpecificBackgroundColorBehavior, +) +from kivymd.uix.button import MDIconButton +from kivymd.uix.label import MDLabel + +Builder.load_string( + """ +#:import calendar calendar +#:import platform platform + + + + cal_layout: cal_layout + size_hint: (None, None) + size: + (dp(328), dp(484)) if self.theme_cls.device_orientation == 'portrait'\ + else (dp(512), dp(304)) + pos_hint: {'center_x': .5, 'center_y': .5} + + canvas: + Color: + rgb: app.theme_cls.primary_color + Rectangle: + size: + (dp(328), dp(96))\ + if self.theme_cls.device_orientation == 'portrait'\ + else (dp(168), dp(304)) + pos: + (root.pos[0], root.pos[1] + root.height - dp(96))\ + if self.theme_cls.device_orientation == 'portrait'\ + else (root.pos[0], root.pos[1] + root.height - dp(304)) + Color: + rgb: app.theme_cls.bg_normal + Rectangle: + size: + (dp(328), dp(484) - dp(96))\ + if self.theme_cls.device_orientation == 'portrait'\ + else [dp(344), dp(304)] + pos: + (root.pos[0], root.pos[1] + root.height - dp(96) - (dp(484) - dp(96)))\ + if self.theme_cls.device_orientation == 'portrait'\ + else (root.pos[0] + dp(168), root.pos[1]) + + MDLabel: + id: label_full_date + font_style: 'H4' + text_color: root.specific_text_color + theme_text_color: 'Custom' + size_hint: (None, None) + size: + (root.width, dp(30))\ + if root.theme_cls.device_orientation == 'portrait'\ + else (dp(168), dp(30)) + pos: + (root.pos[0] + dp(23), root.pos[1] + root.height - dp(74))\ + if root.theme_cls.device_orientation == 'portrait'\ + else (root.pos[0] + dp(3), root.pos[1] + dp(214)) + line_height: .84 + valign: 'middle' + text_size: + (root.width, None)\ + if root.theme_cls.device_orientation == 'portrait'\ + else (dp(149), None) + bold: True + text: + root.fmt_lbl_date(root.sel_year, root.sel_month, root.sel_day,\ + root.theme_cls.device_orientation) + + MDLabel: + id: label_year + font_style: 'Subtitle1' + text_color: root.specific_text_color + theme_text_color: 'Custom' + size_hint: (None, None) + size: root.width, dp(30) + pos: + (root.pos[0] + dp(23), root.pos[1] + root.height - dp(40))\ + if root.theme_cls.device_orientation == 'portrait'\ + else (root.pos[0] + dp(16), root.pos[1] + root.height - dp(41)) + valign: 'middle' + text: str(root.sel_year) + + GridLayout: + id: cal_layout + cols: 7 + size: + (dp(44 * 7), dp(40 * 7))\ + if root.theme_cls.device_orientation == 'portrait'\ + else (dp(46 * 7), dp(32 * 7)) + col_default_width: + dp(42) if root.theme_cls.device_orientation == 'portrait'\ + else dp(39) + size_hint: (None, None) + padding: + (dp(2), 0) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(7), 0) + spacing: + (dp(2), 0) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(7), 0) + pos: + (root.pos[0] + dp(10), root.pos[1] + dp(60))\ + if root.theme_cls.device_orientation == 'portrait'\ + else (root.pos[0] + dp(168) + dp(8), root.pos[1] + dp(48)) + + MDLabel: + id: label_month_selector + font_style: 'Body2' + text: calendar.month_name[root.month].capitalize() + ' ' + str(root.year) + size_hint: (None, None) + size: root.width, dp(30) + pos: root.pos + pos_hint: + {'center_x': .5, 'center_y': .75}\ + if self.theme_cls.device_orientation == 'portrait'\ + else {'center_x': .67, 'center_y': .915} + valign: "middle" + halign: "center" + + MDIconButton: + icon: 'chevron-left' + theme_text_color: 'Secondary' + pos_hint: + {'center_x': .08, 'center_y': .745}\ + if root.theme_cls.device_orientation == 'portrait'\ + else {'center_x': .39, 'center_y': .925} + on_release: root.change_month('prev') + + MDIconButton: + icon: 'chevron-right' + theme_text_color: 'Secondary' + pos_hint: + {'center_x': .92, 'center_y': .745}\ + if root.theme_cls.device_orientation == 'portrait'\ + else {'center_x': .94, 'center_y': .925} + on_release: root.change_month('next') + + MDFlatButton: + width: dp(32) + id: ok_button + pos: root.pos[0] + root.size[0] - self.width - dp(10), root.pos[1] + dp(10) + text: "OK" + on_release: root.ok_click() + + MDFlatButton: + id: cancel_button + pos: root.pos[0] + root.size[0] - self.width - ok_button.width - dp(10), root.pos[1] + dp(10) + text: "Cancel" + on_release: root.dismiss() + + + + size_hint: None, None + size: + (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + + MDLabel: + font_style: 'Caption' + theme_text_color: 'Custom' if root.is_today and not root.is_selected else 'Primary' + text_color: root.theme_cls.primary_color + opposite_colors: + root.is_selected if root.owner.sel_month == root.owner.month\ + and root.owner.sel_year == root.owner.year\ + and str(self.text) == str(root.owner.sel_day) else False + size_hint_x: None + valign: 'middle' + halign: 'center' + text: root.text + + + + font_style: 'Caption' + theme_text_color: 'Secondary' + size: (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + size_hint: None, None + text_size: self.size + valign: + 'middle' if root.theme_cls.device_orientation == 'portrait'\ + else 'bottom' + halign: 'center' + + + + size: + (dp(40), dp(40)) if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + size_hint: (None, None) + + canvas: + Color: + rgba: self.theme_cls.primary_color if self.shown else [0, 0, 0, 0] + Ellipse: + size: + (dp(40), dp(40))\ + if root.theme_cls.device_orientation == 'portrait'\ + else (dp(32), dp(32)) + pos: + self.pos if root.theme_cls.device_orientation == 'portrait'\ + else (self.pos[0], self.pos[1]) +""" +) + + +class DaySelector(ThemableBehavior, AnchorLayout): + shown = BooleanProperty(False) + + def __init__(self, parent): + super().__init__() + self.parent_class = parent + self.parent_class.add_widget(self, index=7) + self.selected_widget = None + Window.bind(on_resize=self.move_resize) + + def update(self): + parent = self.parent_class + if parent.sel_month == parent.month and parent.sel_year == parent.year: + self.shown = True + else: + self.shown = False + + def set_widget(self, widget): + self.selected_widget = widget + self.pos = widget.pos + self.move_resize(do_again=True) + self.update() + + def move_resize(self, window=None, width=None, height=None, do_again=True): + self.pos = self.selected_widget.pos + if do_again: + Clock.schedule_once( + lambda x: self.move_resize(do_again=False), 0.01 + ) + + +class DayButton( + ThemableBehavior, CircularRippleBehavior, ButtonBehavior, AnchorLayout +): + text = StringProperty() + owner = ObjectProperty() + is_today = BooleanProperty(False) + is_selected = BooleanProperty(False) + + def on_release(self): + self.owner.set_selected_widget(self) + + +class WeekdayLabel(MDLabel): + pass + + +class MDDatePicker( + FloatLayout, + ThemableBehavior, + RectangularElevationBehavior, + SpecificBackgroundColorBehavior, + ModalView, +): + _sel_day_widget = ObjectProperty() + cal_list = None + cal_layout = ObjectProperty() + sel_year = NumericProperty() + sel_month = NumericProperty() + sel_day = NumericProperty() + day = NumericProperty() + month = NumericProperty() + year = NumericProperty() + today = date.today() + callback = ObjectProperty() + background_color = ListProperty([0, 0, 0, 0.7]) + + class SetDateError(Exception): + pass + + def __init__( + self, + callback, + year=None, + month=None, + day=None, + firstweekday=0, + min_date=None, + max_date=None, + **kwargs, + ): + self.callback = callback + self.cal = calendar.Calendar(firstweekday) + self.sel_year = year if year else self.today.year + self.sel_month = month if month else self.today.month + self.sel_day = day if day else self.today.day + self.month = self.sel_month + self.year = self.sel_year + self.day = self.sel_day + self.min_date = min_date + self.max_date = max_date + super().__init__(**kwargs) + self.selector = DaySelector(parent=self) + self.generate_cal_widgets() + self.update_cal_matrix(self.sel_year, self.sel_month) + self.set_month_day(self.sel_day) + self.selector.update() + + def ok_click(self): + self.callback(date(self.sel_year, self.sel_month, self.sel_day)) + self.dismiss() + + def fmt_lbl_date(self, year, month, day, orientation): + d = datetime.date(int(year), int(month), int(day)) + separator = "\n" if orientation == "landscape" else " " + return ( + d.strftime("%a,").capitalize() + + separator + + d.strftime("%b").capitalize() + + " " + + str(day).lstrip("0") + ) + + def set_date(self, year, month, day): + try: + date(year, month, day) + except Exception as e: + if str(e) == "day is out of range for month": + raise self.SetDateError( + " Day %s day is out of range for month %s" % (day, month) + ) + elif str(e) == "month must be in 1..12": + raise self.SetDateError( + "Month must be between 1 and 12, got %s" % month + ) + elif str(e) == "year is out of range": + raise self.SetDateError( + "Year must be between %s and %s, got %s" + % (datetime.MINYEAR, datetime.MAXYEAR, year) + ) + else: + self.sel_year = year + self.sel_month = month + self.sel_day = day + self.month = self.sel_month + self.year = self.sel_year + self.day = self.sel_day + self.update_cal_matrix(self.sel_year, self.sel_month) + self.set_month_day(self.sel_day) + self.selector.update() + + def set_selected_widget(self, widget): + if self._sel_day_widget: + self._sel_day_widget.is_selected = False + widget.is_selected = True + self.sel_month = int(self.month) + self.sel_year = int(self.year) + self.sel_day = int(widget.text) + self._sel_day_widget = widget + self.selector.set_widget(widget) + + def set_month_day(self, day): + for idx in range(len(self.cal_list)): + if str(day) == str(self.cal_list[idx].text): + self._sel_day_widget = self.cal_list[idx] + self.sel_day = int(self.cal_list[idx].text) + if self._sel_day_widget: + self._sel_day_widget.is_selected = False + self._sel_day_widget = self.cal_list[idx] + self.cal_list[idx].is_selected = True + self.selector.set_widget(self.cal_list[idx]) + + def update_cal_matrix(self, year, month): + try: + dates = [x for x in self.cal.itermonthdates(year, month)] + except ValueError as e: + if str(e) == "year is out of range": + pass + else: + self.year = year + self.month = month + for idx in range(len(self.cal_list)): + if idx >= len(dates) or dates[idx].month != month: + self.cal_list[idx].disabled = True + self.cal_list[idx].text = "" + else: + if self.min_date and self.max_date: + self.cal_list[idx].disabled = ( + True + if ( + dates[idx] < self.min_date + or dates[idx] > self.max_date + ) + else False + ) + elif self.min_date: + if isinstance(self.min_date, date): + self.cal_list[idx].disabled = ( + True if dates[idx] < self.min_date else False + ) + else: + raise ValueError( + "min_date must be of type {} or None, got {}".format( + date, type(self.min_date) + ) + ) + elif self.max_date: + if isinstance(self.max_date, date): + self.cal_list[idx].disabled = ( + True if dates[idx] > self.max_date else False + ) + else: + raise ValueError( + "max_date must be of type {} or None, got {}".format( + date, type(self.min_date) + ) + ) + else: + self.cal_list[idx].disabled = False + self.cal_list[idx].text = str(dates[idx].day) + self.cal_list[idx].is_today = dates[idx] == self.today + self.selector.update() + + def generate_cal_widgets(self): + cal_list = [] + for day in self.cal.iterweekdays(): + self.cal_layout.add_widget( + WeekdayLabel(text=calendar.day_abbr[day][0].upper()) + ) + for i in range(6 * 7): # 6 weeks, 7 days a week + db = DayButton(owner=self) + cal_list.append(db) + self.cal_layout.add_widget(db) + self.cal_list = cal_list + + def change_month(self, operation): + op = 1 if operation == "next" else -1 + sl, sy = self.month, self.year + m = 12 if sl + op == 0 else 1 if sl + op == 13 else sl + op + y = sy - 1 if sl + op == 0 else sy + 1 if sl + op == 13 else sy + self.update_cal_matrix(y, m) + + +Builder.load_string( + """ +#:import CircularTimePicker kivymd.vendor.circularTimePicker.CircularTimePicker +#:import dp kivy.metrics.dp + + + + size_hint: (None, None) + size: (dp(270), dp(335) + dp(95)) + pos_hint: {'center_x': .5, 'center_y': .5} + + canvas: + Color: + rgba: self.theme_cls.bg_light + Rectangle: + size: (dp(270), dp(335)) + pos: (root.pos[0], root.pos[1] + root.height - dp(335) - dp(95)) + Color: + rgba: self.theme_cls.primary_color + Rectangle: + size: (dp(270), dp(95)) + pos: (root.pos[0], root.pos[1] + root.height - dp(95)) + Color: + rgba: self.theme_cls.bg_dark + Ellipse: + size: (dp(220), dp(220)) + pos: + root.pos[0] + dp(270) / 2 - dp(220) / 2, root.pos[1]\ + + root.height - (dp(335) / 2 + dp(95)) - dp(220) / 2 + dp(35) + + CircularTimePicker: + id: time_picker + pos: (dp(270) / 2) - (self.width / 2), root.height - self.height + size_hint: (.8, .8) + pos_hint: {'center_x': .5, 'center_y': .585} + + MDFlatButton: + width: dp(32) + id: ok_button + pos: + root.pos[0] + root.size[0] - self.width - dp(10),\ + root.pos[1] + dp(10) + text: "OK" + on_release: root.close_ok() + + MDFlatButton: + id: cancel_button + pos: + root.pos[0] + root.size[0] - self.width - ok_button.width\ + - dp(10), root.pos[1] + dp(10) + text: "Cancel" + on_release: root.close_cancel() +""" +) + + +class MDTimePicker( + ThemableBehavior, FloatLayout, ModalView, RectangularElevationBehavior +): + time = ObjectProperty() + """ + Users method. Must take two parameters: + + .. code-block:: python + + def get_time(self, instance, time): + ''' + The method returns the set time. + + :type instance: + :type time: + ''' + + return time + + :attr:`time` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.current_time = self.ids.time_picker.time + + def set_time(self, time): + """ + Sets user time. + + :type time: + """ + + try: + self.ids.time_picker.set_time(time) + except AttributeError: + raise TypeError( + "MDTimePicker._set_time must receive a datetime object, " + 'not a "' + type(time).__name__ + '"' + ) + + def close_cancel(self): + self.dismiss() + + def close_ok(self): + self.current_time = self.ids.time_picker.time + self.time = self.current_time + self.dismiss() + + +Builder.load_string( + """ + + + + + size: dp(40), dp(40) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: root.rgb_hex(root.color_name) + Ellipse: + size: self.size + pos: self.pos + + + + on_release: app.theme_cls.accent_palette = root.color_name + + + + on_release: app.theme_cls.primary_palette = root.color_name + + + + size_hint: (None, None) + size: dp(284), dp(120) + dp(290) + pos_hint: {'center_x': .5, 'center_y': .5} + + canvas: + Color: + rgb: app.theme_cls.primary_color + Rectangle: + size: self.width, dp(120) + pos: root.pos[0], root.pos[1] + root.height - dp(120) + Color: + rgb: app.theme_cls.bg_normal + Rectangle: + size: self.width, dp(290) + pos: root.pos[0], root.pos[1] + root.height - (dp(120) + dp(290)) + + + MDFlatButton: + id: close_button + pos: root.pos[0] + root.size[0] - self.width - dp(10), root.pos[1] + dp(10) + text: "Close" + on_release: root.dismiss() + + MDLabel: + id: title + font_style: "H5" + text: "Change theme" + size_hint: (None, None) + size: dp(160), dp(50) + pos_hint: {'center_x': .5, 'center_y': .9} + theme_text_color: 'Custom' + text_color: root.specific_text_color + + MDTabs: + size_hint: (None, None) + size: root.width, root.height - dp(135) + pos_hint: {'center_x': .5, 'center_y': .475} + id: tab_panel + + Tab: + id: theme_tab + text: "Theme" + + BoxLayout: + spacing: dp(4) + padding: dp(4) + size_hint: (None, None) + size: dp(270), root.height # -dp(120) + pos_hint: {'center_x': .532, 'center_y': .89} + orientation: 'vertical' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + + BoxLayout: + PrimaryColorSelector: + color_name: 'Red' + BoxLayout: + PrimaryColorSelector: + color_name: 'Pink' + BoxLayout: + PrimaryColorSelector: + color_name: 'Purple' + BoxLayout: + PrimaryColorSelector: + color_name: 'DeepPurple' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + + BoxLayout: + PrimaryColorSelector: + color_name: 'Indigo' + BoxLayout: + PrimaryColorSelector: + color_name: 'Blue' + BoxLayout: + PrimaryColorSelector: + color_name: 'LightBlue' + BoxLayout: + PrimaryColorSelector: + color_name: 'Cyan' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + + BoxLayout: + PrimaryColorSelector: + color_name: 'Teal' + BoxLayout: + PrimaryColorSelector: + color_name: 'Green' + BoxLayout: + PrimaryColorSelector: + color_name: 'LightGreen' + BoxLayout: + PrimaryColorSelector: + color_name: 'Lime' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + pos: self.pos + orientation: 'horizontal' + halign: 'center' + padding: 0, 0, 0, dp(1) + + BoxLayout: + PrimaryColorSelector: + color_name: 'Yellow' + BoxLayout: + PrimaryColorSelector: + color_name: 'Amber' + BoxLayout: + PrimaryColorSelector: + color_name: 'Orange' + BoxLayout: + PrimaryColorSelector: + color_name: 'DeepOrange' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + #pos: self.pos + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + + BoxLayout: + PrimaryColorSelector: + color_name: 'Brown' + BoxLayout: + PrimaryColorSelector: + color_name: 'Gray' + BoxLayout: + PrimaryColorSelector: + color_name: 'BlueGray' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + size_hint: (None, None) + canvas: + Color: + rgba: app.theme_cls.bg_normal + Ellipse: + size: self.size + pos: self.pos + disabled: True + + Tab: + id: accent_tab + text: "Accent" + + BoxLayout: + spacing: dp(4) + padding: dp(4) + size_hint: (None, None) + size: dp(270), root.height # -dp(120) + pos_hint: {'center_x': .532, 'center_y': .89} + orientation: 'vertical' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + + BoxLayout: + AccentColorSelector: + color_name: 'Red' + BoxLayout: + AccentColorSelector: + color_name: 'Pink' + BoxLayout: + AccentColorSelector: + color_name: 'Purple' + BoxLayout: + AccentColorSelector: + color_name: 'DeepPurple' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + + BoxLayout: + AccentColorSelector: + color_name: 'Indigo' + BoxLayout: + AccentColorSelector: + color_name: 'Blue' + BoxLayout: + AccentColorSelector: + color_name: 'LightBlue' + BoxLayout: + AccentColorSelector: + color_name: 'Cyan' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + pos: self.pos + halign: 'center' + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + + BoxLayout: + AccentColorSelector: + color_name: 'Teal' + BoxLayout: + AccentColorSelector: + color_name: 'Green' + BoxLayout: + AccentColorSelector: + color_name: 'LightGreen' + BoxLayout: + AccentColorSelector: + color_name: 'Lime' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + pos: self.pos + orientation: 'horizontal' + halign: 'center' + padding: 0, 0, 0, dp(1) + + BoxLayout: + AccentColorSelector: + color_name: 'Yellow' + BoxLayout: + AccentColorSelector: + color_name: 'Amber' + BoxLayout: + AccentColorSelector: + color_name: 'Orange' + BoxLayout: + AccentColorSelector: + color_name: 'DeepOrange' + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .5} + size: dp(230), dp(40) + #pos: self.pos + orientation: 'horizontal' + padding: 0, 0, 0, dp(1) + + BoxLayout: + AccentColorSelector: + color_name: 'Brown' + BoxLayout: + AccentColorSelector: + color_name: 'Gray' + BoxLayout: + AccentColorSelector: + color_name: 'BlueGray' + BoxLayout: + MDIconButton: + size: dp(40), dp(40) + size_hint: (None, None) + canvas: + Color: + rgba: app.theme_cls.bg_normal + Ellipse: + size: self.size + pos: self.pos + disabled: True + + Tab: + id: style_tab + text: "Style" + + FloatLayout: + size: self.size + pos: self.pos + + BoxLayout: + size_hint: (None, None) + pos_hint: {'center_x': .5, 'center_y': .6} + halign: 'center' + valign: 'center' + spacing: dp(10) + width: dp(210) + height: dp(100) + + MDIconButton: + size: dp(100), dp(100) + size_hint: (None, None) + canvas: + Color: + rgba: 1, 1, 1, 1 + Ellipse: + size: self.size + pos: self.pos + Color: + rgba: 0, 0, 0, 1 + Line: + width: 1. + circle: (self.center_x, self.center_y, dp(50)) + on_release: app.theme_cls.theme_style = 'Light' + MDIconButton: + size: dp(100), dp(100) + pos: self.pos + size_hint: (None, None) + canvas: + Color: + rgba: 0, 0, 0, 1 + Ellipse: + size: self.size + pos: self.pos + on_release: app.theme_cls.theme_style = 'Dark' +""" +) + + +class ColorSelector(MDIconButton): + color_name = OptionProperty("Indigo", options=palette) + + def rgb_hex(self, col): + return get_color_from_hex(colors[col][self.theme_cls.accent_hue]) + + +class MDThemePicker( + ThemableBehavior, + FloatLayout, + ModalView, + SpecificBackgroundColorBehavior, + RectangularElevationBehavior, +): + pass diff --git a/kivymd/uix/progressbar.py b/kivymd/uix/progressbar.py new file mode 100755 index 0000000..fe5f2e6 --- /dev/null +++ b/kivymd/uix/progressbar.py @@ -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( + """ + + 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 diff --git a/kivymd/uix/refreshlayout.py b/kivymd/uix/refreshlayout.py new file mode 100755 index 0000000..437f01d --- /dev/null +++ b/kivymd/uix/refreshlayout.py @@ -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(''' + + text: root.text + + IconLeftSampleWidget: + icon: root.icon + + + + + 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 + + + + + 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 diff --git a/kivymd/uix/relativelayout.py b/kivymd/uix/relativelayout.py new file mode 100644 index 0000000..96338a7 --- /dev/null +++ b/kivymd/uix/relativelayout.py @@ -0,0 +1,38 @@ +""" +Components/RelativeLayout +========================= + +:class:`~kivy.uix.relativelayout.RelativeLayout` class equivalent. Simplifies working +with some widget properties. For example: + +RelativeLayout +-------------- + +.. code-block:: + + RelativeLayout: + canvas: + Color: + rgba: app.theme_cls.primary_color + RoundedRectangle: + pos: (0, 0) + size: self.size + radius: [25, ] + +MDRelativeLayout +---------------- + +.. code-block:: + + MDRelativeLayout: + radius: [25, ] + md_bg_color: app.theme_cls.primary_color +""" + +from kivy.uix.relativelayout import RelativeLayout + +from kivymd.uix import MDAdaptiveWidget + + +class MDRelativeLayout(RelativeLayout, MDAdaptiveWidget): + pass diff --git a/kivymd/uix/screen.py b/kivymd/uix/screen.py new file mode 100644 index 0000000..2add537 --- /dev/null +++ b/kivymd/uix/screen.py @@ -0,0 +1,38 @@ +""" +Components/Screen +================= + +:class:`~kivy.uix.screenmanager.Screen` class equivalent. Simplifies working +with some widget properties. For example: + +Screen +------ + +.. code-block:: + + Screen: + canvas: + Color: + rgba: app.theme_cls.primary_color + RoundedRectangle: + pos: self.pos + size: self.size + radius: [25, 0, 0, 0] + +MDScreen +-------- + +.. code-block:: + + MDScreen: + radius: [25, 0, 0, 0] + md_bg_color: app.theme_cls.primary_color +""" + +from kivy.uix.screenmanager import Screen + +from kivymd.uix import MDAdaptiveWidget + + +class MDScreen(Screen, MDAdaptiveWidget): + pass diff --git a/kivymd/uix/selectioncontrol.py b/kivymd/uix/selectioncontrol.py new file mode 100755 index 0000000..74e63cd --- /dev/null +++ b/kivymd/uix/selectioncontrol.py @@ -0,0 +1,537 @@ +""" +Components/Selection Controls +============================= + +.. seealso:: + + `Material Design spec, Selection controls `_ + +.. rubric:: Selection controls allow the user to select options. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-controll.png + :align: center + +`KivyMD` provides the following selection controls classes for use: + +- MDCheckbox_ +- MDSwitch_ + +.. MDCheckbox: +MDCheckbox +---------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + + KV = ''' + FloatLayout: + + MDCheckbox: + size_hint: None, None + size: "48dp", "48dp" + pos_hint: {'center_x': .5, 'center_y': .5} + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox.gif + :align: center + +.. Note:: Be sure to specify the size of the checkbox. By default, it is + ``(dp(48), dp(48))``, but the ripple effect takes up all the available + space. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox-no-size.gif + :align: center + +Control state +------------- + +.. code-block:: kv + + MDCheckbox: + on_active: app.on_checkbox_active(*args) + +.. code-block:: python + + def on_checkbox_active(self, checkbox, value): + if value: + print('The checkbox', checkbox, 'is active', 'and', checkbox.state, 'state') + else: + print('The checkbox', checkbox, 'is inactive', 'and', checkbox.state, 'state') + +MDCheckbox with group +--------------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + : + group: 'group' + size_hint: None, None + size: dp(48), dp(48) + + + FloatLayout: + + Check: + active: True + pos_hint: {'center_x': .4, 'center_y': .5} + + Check: + pos_hint: {'center_x': .6, 'center_y': .5} + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox-group.gif + :align: center + +.. MDSwitch: +MDSwitch +-------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + FloatLayout: + + MDSwitch: + pos_hint: {'center_x': .5, 'center_y': .5} + ''' + + + 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-switch.gif + :align: center + +.. Note:: For :class:`~MDCheckbox` size is not required. By default it is + ``(dp(36), dp(48))``, but you can increase the width if you want. + +.. code-block:: kv + + MDSwitch: + width: dp(64) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-switch_width.png + :align: center + +.. Note:: Control state of :class:`~MDSwitch` same way as in + :class:`~MDCheckbox`. +""" + +__all__ = ("MDCheckbox", "MDSwitch") + +from kivy.animation import Animation +from kivy.lang import Builder +from kivy.metrics import dp, sp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + ListProperty, + NumericProperty, + StringProperty, +) +from kivy.uix.behaviors import ButtonBehavior, ToggleButtonBehavior +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.widget import Widget +from kivy.utils import get_color_from_hex + +from kivymd.color_definitions import colors +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + CircularElevationBehavior, + CircularRippleBehavior, +) +from kivymd.uix.label import MDIcon + +Builder.load_string( + """ + + canvas: + Clear + Color: + rgba: self.color + Rectangle: + texture: self.texture + size: self.texture_size + pos: + int(self.center_x - self.texture_size[0] / 2.),\ + int(self.center_y - self.texture_size[1] / 2.) + + color: self._current_color + halign: 'center' + valign: 'middle' + + + + color: 1, 1, 1, 1 + canvas: + Color: + rgba: self.color + Ellipse: + size: self.size + pos: self.pos + + + + canvas.before: + Color: + rgba: + self._track_color_disabled if self.disabled else\ + (self._track_color_active if self.active\ + else self._track_color_normal) + RoundedRectangle: + size: self.width - dp(8), dp(16) + pos: self.x + dp(8), self.center_y - dp(8) + radius: [dp(7)] + + on_release: thumb.trigger_action() + + Thumb: + id: thumb + size_hint: None, None + size: dp(24), dp(24) + pos: root.pos[0] + root._thumb_pos[0], root.pos[1] + root._thumb_pos[1] + color: + root.thumb_color_disabled if root.disabled else\ + (root.thumb_color_down if root.active else root.thumb_color) + elevation: 4 if root.active else 2 + on_release: setattr(root, 'active', not root.active) +""" +) + + +class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): + active = BooleanProperty(False) + """ + Indicates if the checkbox is active or inactive. + + :attr:`active` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + checkbox_icon_normal = StringProperty("checkbox-blank-outline") + """ + Background icon of the checkbox used for the default graphical + representation when the checkbox is not pressed. + + :attr:`checkbox_icon_normal` is a :class:`~kivy.properties.StringProperty` + and defaults to `'checkbox-blank-outline'`. + """ + + checkbox_icon_down = StringProperty("checkbox-marked-outline") + """ + Background icon of the checkbox used for the default graphical + representation when the checkbox is pressed. + + :attr:`checkbox_icon_down` is a :class:`~kivy.properties.StringProperty` + and defaults to `'checkbox-marked-outline'`. + """ + + radio_icon_normal = StringProperty("checkbox-blank-circle-outline") + """ + Background icon (when using the ``group`` option) of the checkbox used for + the default graphical representation when the checkbox is not pressed. + + :attr:`radio_icon_normal` is a :class:`~kivy.properties.StringProperty` + and defaults to `'checkbox-blank-circle-outline'`. + """ + + radio_icon_down = StringProperty("checkbox-marked-circle-outline") + """ + Background icon (when using the ``group`` option) of the checkbox used for + the default graphical representation when the checkbox is pressed. + + :attr:`radio_icon_down` is a :class:`~kivy.properties.StringProperty` + and defaults to `'checkbox-marked-circle-outline'`. + """ + + selected_color = ListProperty() + """ + Selected color in ``rgba`` format. + + :attr:`selected_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + unselected_color = ListProperty() + """ + Unelected color in ``rgba`` format. + + :attr:`unselected_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + disabled_color = ListProperty() + """ + Disabled color in ``rgba`` format. + + :attr:`disabled_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + _current_color = ListProperty([0.0, 0.0, 0.0, 0.0]) + + def __init__(self, **kwargs): + self.check_anim_out = Animation(font_size=0, duration=0.1, t="out_quad") + self.check_anim_in = Animation( + font_size=sp(24), duration=0.1, t="out_quad" + ) + super().__init__(**kwargs) + self.selected_color = self.theme_cls.primary_color + self.unselected_color = self.theme_cls.secondary_text_color + self.disabled_color = self.theme_cls.divider_color + self._current_color = self.unselected_color + self.check_anim_out.bind( + on_complete=lambda *x: self.check_anim_in.start(self) + ) + self.bind( + checkbox_icon_normal=self.update_icon, + checkbox_icon_down=self.update_icon, + radio_icon_normal=self.update_icon, + radio_icon_down=self.update_icon, + group=self.update_icon, + selected_color=self.update_color, + unselected_color=self.update_color, + disabled_color=self.update_color, + disabled=self.update_color, + state=self.update_color, + ) + self.theme_cls.bind(primary_color=self.update_primary_color) + self.update_icon() + self.update_color() + + def update_primary_color(self, instance, value): + self.selected_color = value + + def update_icon(self, *args): + if self.state == "down": + self.icon = ( + self.radio_icon_down if self.group else self.checkbox_icon_down + ) + else: + self.icon = ( + self.radio_icon_normal + if self.group + else self.checkbox_icon_normal + ) + + def update_color(self, *args): + if self.disabled: + self._current_color = self.disabled_color + elif self.state == "down": + self._current_color = self.selected_color + else: + self._current_color = self.unselected_color + + def on_state(self, *args): + if self.state == "down": + self.check_anim_in.cancel(self) + self.check_anim_out.start(self) + self.update_icon() + if self.group: + self._release_group(self) + self.active = True + else: + self.check_anim_in.cancel(self) + self.check_anim_out.start(self) + self.update_icon() + self.active = False + + def on_active(self, *args): + self.state = "down" if self.active else "normal" + + +class Thumb( + CircularElevationBehavior, CircularRippleBehavior, ButtonBehavior, Widget +): + ripple_scale = NumericProperty(2) + """ + See :attr:`~kivymd.uix.behaviors.ripplebehavior.CommonRipple.ripple_scale`. + + :attr:`ripple_scale` is a :class:`~kivy.properties.NumericProperty` + and defaults to `2`. + """ + + def _set_ellipse(self, instance, value): + self.ellipse.size = (self._ripple_rad, self._ripple_rad) + if self.ellipse.size[0] > self.width * 1.5 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, + ) + self.stencil.pos = ( + self.center_x - (self.width * self.ripple_scale) / 2, + self.center_y - (self.height * self.ripple_scale) / 2, + ) + + +class MDSwitch(ThemableBehavior, ButtonBehavior, FloatLayout): + active = BooleanProperty(False) + """ + Indicates if the switch is active or inactive. + + :attr:`active` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + _thumb_color = ListProperty(get_color_from_hex(colors["Gray"]["50"])) + + def _get_thumb_color(self): + return self._thumb_color + + def _set_thumb_color(self, color, alpha=None): + if len(color) == 2: + self._thumb_color = get_color_from_hex(colors[color[0]][color[1]]) + if alpha: + self._thumb_color[3] = alpha + elif len(color) == 4: + self._thumb_color = color + + thumb_color = AliasProperty( + _get_thumb_color, _set_thumb_color, bind=["_thumb_color"] + ) + """ + Get thumb color ``rgba`` format. + + :attr:`thumb_color` is an :class:`~kivy.properties.AliasProperty` + and property is readonly. + """ + + _thumb_color_down = ListProperty([1, 1, 1, 1]) + + def _get_thumb_color_down(self): + return self._thumb_color_down + + def _set_thumb_color_down(self, color, alpha=None): + if len(color) == 2: + self._thumb_color_down = get_color_from_hex( + colors[color[0]][color[1]] + ) + if alpha: + self._thumb_color_down[3] = alpha + else: + self._thumb_color_down[3] = 1 + elif len(color) == 4: + self._thumb_color_down = color + + _thumb_color_disabled = ListProperty( + get_color_from_hex(colors["Gray"]["400"]) + ) + + thumb_color_disabled = get_color_from_hex(colors["Gray"]["800"]) + """ + Get thumb color disabled ``rgba`` format. + + :attr:`thumb_color_disabled` is an :class:`~kivy.properties.AliasProperty` + and property is readonly. + """ + + def _get_thumb_color_disabled(self): + return self._thumb_color_disabled + + def _set_thumb_color_disabled(self, color, alpha=None): + if len(color) == 2: + self._thumb_color_disabled = get_color_from_hex( + colors[color[0]][color[1]] + ) + if alpha: + self._thumb_color_disabled[3] = alpha + elif len(color) == 4: + self._thumb_color_disabled = color + + thumb_color_down = AliasProperty( + _get_thumb_color_disabled, + _set_thumb_color_disabled, + bind=["_thumb_color_disabled"], + ) + """ + Get thumb color down ``rgba`` format. + + :attr:`thumb_color_down` is an :class:`~kivy.properties.AliasProperty` + and property is readonly. + """ + + _track_color_active = ListProperty() + _track_color_normal = ListProperty() + _track_color_disabled = ListProperty() + _thumb_pos = ListProperty([0, 0]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.theme_cls.bind( + theme_style=self._set_colors, + primary_color=self._set_colors, + primary_palette=self._set_colors, + ) + self.bind(active=self._update_thumb_pos) + self._set_colors() + self.size_hint = (None, None) + self.size = (dp(36), dp(48)) + + def _set_colors(self, *args): + self._track_color_normal = self.theme_cls.disabled_hint_text_color + if self.theme_cls.theme_style == "Dark": + self._track_color_active = self.theme_cls.primary_color + self._track_color_active[3] = 0.5 + self._track_color_disabled = get_color_from_hex("FFFFFF") + self._track_color_disabled[3] = 0.1 + self.thumb_color = get_color_from_hex(colors["Gray"]["400"]) + self.thumb_color_down = get_color_from_hex( + colors[self.theme_cls.primary_palette]["200"] + ) + else: + self._track_color_active = get_color_from_hex( + colors[self.theme_cls.primary_palette]["200"] + ) + self._track_color_active[3] = 0.5 + self._track_color_disabled = self.theme_cls.disabled_hint_text_color + self.thumb_color_down = self.theme_cls.primary_color + + def _update_thumb_pos(self, *args, animation=True): + if self.active: + _thumb_pos = (self.width - dp(12), self.height / 2 - dp(12)) + else: + _thumb_pos = (0, self.height / 2 - dp(12)) + Animation.cancel_all(self, "_thumb_pos") + if animation: + Animation(_thumb_pos=_thumb_pos, duration=0.2, t="out_quad").start( + self + ) + else: + self._thumb_pos = _thumb_pos + + def on_size(self, *args): + self._update_thumb_pos(animation=False) diff --git a/kivymd/uix/slider.py b/kivymd/uix/slider.py new file mode 100755 index 0000000..f0c4109 --- /dev/null +++ b/kivymd/uix/slider.py @@ -0,0 +1,386 @@ +""" +Components/Slider +================= + +.. seealso:: + + `Material Design spec, Sliders `_ + +.. rubric:: Sliders allow users to make selections from a range of values. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slider.png + :align: center + +With value hint +--------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + Screen + + MDSlider: + min: 0 + max: 100 + value: 40 + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slider-1.gif + :align: center + +Without value hint +------------------ + +.. code-block:: kv + + MDSlider: + min: 0 + max: 100 + value: 40 + hint: False + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slider-2.gif + :align: center + +Without custom color +-------------------- + +.. code-block:: kv + + MDSlider: + min: 0 + max: 100 + value: 40 + hint: False + humb_color_down: app.theme_cls.accent_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slider-3.png + :align: center +""" + +__all__ = ("MDSlider",) + +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + ListProperty, + NumericProperty, +) +from kivy.uix.slider import Slider +from kivy.utils import get_color_from_hex + +from kivymd.color_definitions import colors +from kivymd.theming import ThemableBehavior + +Builder.load_string( + """ +#:import images_path kivymd.images_path + + + + id: slider + canvas: + Clear + Color: + rgba: + self._track_color_disabled if self.disabled \ + else (self._track_color_active if self.active \ + else self._track_color_normal) + Rectangle: + size: + (self.width - self.padding * 2 - self._offset[0], dp(4)) if \ + self.orientation == "horizontal" \ + else (dp(4),self.height - self.padding*2 - self._offset[1]) + pos: + (self.x + self.padding + self._offset[0], self.center_y - dp(4)) \ + if self.orientation == "horizontal" else \ + (self.center_x - dp(4), self.y + self.padding + self._offset[1]) + + # If 0 draw circle + Color: + rgba: + (0, 0, 0, 0) if not self._is_off \ + else (self._track_color_disabled if self.disabled \ + else (self._track_color_active \ + if self.active else self._track_color_normal)) + Line: + width: 2 + circle: + (self.x + self.padding + dp(3), self.center_y - dp(2), 8 \ + if self.active else 6 ) if self.orientation == "horizontal" \ + else (self.center_x - dp(2), self.y + self.padding + dp(3), 8 \ + if self.active else 6) + + Color: + rgba: + (0, 0, 0, 0) if self._is_off \ + else (self.thumb_color_down if not self.disabled \ + else self._track_color_disabled) + Rectangle: + size: + ((self.width - self.padding * 2) * self.value_normalized, sp(4)) \ + if slider.orientation == "horizontal" else (sp(4), \ + (self.height - self.padding * 2) * self.value_normalized) + pos: + (self.x + self.padding, self.center_y - dp(4)) \ + if self.orientation == "horizontal" \ + else (self.center_x - dp(4), self.y + self.padding) + + Thumb: + id: thumb + size_hint: None, None + size: + (dp(12), dp(12)) if root.disabled else ((dp(24), dp(24)) \ + if root.active else (dp(16), dp(16))) + pos: + (slider.value_pos[0] - dp(8), slider.center_y - thumb.height / 2 - dp(2)) \ + if slider.orientation == "horizontal" \ + else (slider.center_x - thumb.width / 2 - dp(2), \ + slider.value_pos[1] - dp(8)) + color: + (0, 0, 0, 0) if slider._is_off else (root._track_color_disabled \ + if root.disabled else root.thumb_color_down) + elevation: + 0 if slider._is_off else (4 if root.active else 2) + + MDCard: + id: hint_box + size_hint: None, None + md_bg_color: (1, 1, 1, 1) if not root.hint_bg_color else slider.hint_bg_color + elevation: 0 + opacity: 1 if slider.active else 0 + background: f"{images_path}transparent.png" + radius: [slider.hint_radius,] + size: + (dp(12), dp(12)) if root.disabled else ((dp(28), dp(28)) \ + if root.active else (dp(20), dp(20))) + pos: + (slider.value_pos[0] - dp(9), slider.center_y - hint_box.height / 2 + dp(30)) \ + if slider.orientation == "horizontal" \ + else (slider.center_x - hint_box.width / 2 + dp(30), \ + slider.value_pos[1] - dp(8)) + + MDLabel: + text: str(int(slider.value)) + font_style: "Caption" + halign: "center" + theme_text_color: "Custom" + text_color: + (root.thumb_color_down if root.active else (0, 0, 0, 0)) \ + if not slider.hint_text_color else slider.hint_text_color +""" +) + + +class MDSlider(ThemableBehavior, Slider): + active = BooleanProperty(False) + """ + If the slider is clicked. + + :attr:`active` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + hint = BooleanProperty(True) + """ + If True, then the current value is displayed above the slider. + + :attr:`hint` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + hint_bg_color = ListProperty() + """ + Hint rectangle color in ``rgba`` format. + + :attr:`hint_bg_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + hint_text_color = ListProperty() + """ + Hint text color in ``rgba`` format. + + :attr:`hint_text_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + hint_radius = NumericProperty(4) + """ + Hint radius. + + :attr:`hint_radius` is an :class:`~kivy.properties.NumericProperty` + and defaults to `4`. + """ + + show_off = BooleanProperty(True) + """ + Show the `'off'` ring when set to minimum value. + + :attr:`show_off` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + # Internal state of ring + _is_off = BooleanProperty(False) + + # Internal adjustment to reposition sliders for ring + _offset = ListProperty((0, 0)) + + _thumb_color = ListProperty(get_color_from_hex(colors["Gray"]["50"])) + + def _get_thumb_color(self): + return self._thumb_color + + def _set_thumb_color(self, color, alpha=None): + if len(color) == 2: + self._thumb_color = get_color_from_hex(colors[color[0]][color[1]]) + if alpha: + self._thumb_color[3] = alpha + elif len(color) == 4: + self._thumb_color = color + + thumb_color = AliasProperty( + _get_thumb_color, _set_thumb_color, bind=["_thumb_color"] + ) + """ + Current color slider in ``rgba`` format. + + :attr:`thumb_color` is an :class:`~kivy.properties.AliasProperty` that + returns the value of the current color slider, property is readonly. + """ + + _thumb_color_down = ListProperty([1, 1, 1, 1]) + + def _get_thumb_color_down(self): + return self._thumb_color_down + + def _set_thumb_color_down(self, color, alpha=None): + if len(color) == 2: + self._thumb_color_down = get_color_from_hex( + colors[color[0]][color[1]] + ) + if alpha: + self._thumb_color_down[3] = alpha + else: + self._thumb_color_down[3] = 1 + elif len(color) == 4: + self._thumb_color_down = color + + _thumb_color_disabled = ListProperty( + get_color_from_hex(colors["Gray"]["400"]) + ) + + def _get_thumb_color_disabled(self): + return self._thumb_color_disabled + + def _set_thumb_color_disabled(self, color, alpha=None): + if len(color) == 2: + self._thumb_color_disabled = get_color_from_hex( + colors[color[0]][color[1]] + ) + if alpha: + self._thumb_color_disabled[3] = alpha + elif len(color) == 4: + self._thumb_color_disabled = color + + thumb_color_down = AliasProperty( + _get_thumb_color_disabled, + _set_thumb_color_disabled, + bind=["_thumb_color_disabled"], + ) + """ + Color slider in ``rgba`` format. + + :attr:`thumb_color_down` is an :class:`~kivy.properties.AliasProperty` + that returns and set the value of color slider. + """ + + _track_color_active = ListProperty() + _track_color_normal = ListProperty() + _track_color_disabled = ListProperty() + _thumb_pos = ListProperty([0, 0]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.theme_cls.bind( + theme_style=self._set_colors, + primary_color=self._set_colors, + primary_palette=self._set_colors, + ) + self._set_colors() + + def on_hint(self, instance, value): + if not value: + self.remove_widget(self.ids.hint_box) + + def _set_colors(self, *args): + if self.theme_cls.theme_style == "Dark": + self._track_color_normal = get_color_from_hex("FFFFFF") + self._track_color_normal[3] = 0.3 + self._track_color_active = self._track_color_normal + self._track_color_disabled = self._track_color_normal + self.thumb_color = get_color_from_hex(colors["Gray"]["400"]) + self.thumb_color_down = get_color_from_hex( + colors[self.theme_cls.primary_palette]["200"] + ) + self.thumb_color_disabled = get_color_from_hex( + colors["Gray"]["800"] + ) + else: + self._track_color_normal = get_color_from_hex("000000") + self._track_color_normal[3] = 0.26 + self._track_color_active = get_color_from_hex("000000") + self._track_color_active[3] = 0.38 + self._track_color_disabled = get_color_from_hex("000000") + self._track_color_disabled[3] = 0.26 + self.thumb_color_down = self.theme_cls.primary_color + + def on_value_normalized(self, *args): + """When the ``value == min`` set it to `'off'` state and make slider + a ring. + """ + + self._update_is_off() + + def on_show_off(self, *args): + self._update_is_off() + + def _update_is_off(self): + self._is_off = self.show_off and (self.value_normalized == 0) + + def on__is_off(self, *args): + self._update_offset() + + def on_active(self, *args): + self._update_offset() + + def _update_offset(self): + """Offset is used to shift the sliders so the background color + shows through the off circle. + """ + + d = 2 if self.active else 0 + self._offset = (dp(11 + d), dp(11 + d)) if self._is_off else (0, 0) + + def on_touch_down(self, touch): + if super().on_touch_down(touch): + self.active = True + + def on_touch_up(self, touch): + if super().on_touch_up(touch): + self.active = False diff --git a/kivymd/uix/snackbar.py b/kivymd/uix/snackbar.py new file mode 100755 index 0000000..9033ac0 --- /dev/null +++ b/kivymd/uix/snackbar.py @@ -0,0 +1,647 @@ +""" +Components/Snackbar +=================== + +.. seealso:: + + `Material Design spec, Snackbars `_ + +.. rubric:: Snackbars provide brief messages about app processes at the bottom + of the screen. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar.png + :align: center + +Usage +----- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + #:import Snackbar kivymd.uix.snackbar.Snackbar + + + Screen: + + MDRaisedButton: + text: "Create simple snackbar" + on_release: Snackbar(text="This is a snackbar!").open() + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-simple.gif + :align: center + +Usage with snackbar_x, snackbar_y +------------------ + +.. code-block:: python + + Snackbar( + text="This is a snackbar!", + snackbar_x="10dp", + snackbar_y="10dp", + size_hint_x=( + Window.width - (dp(10) * 2) + ) / Window.width + ).open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-padding.gif + :align: center + +Control width +------------- + +.. code-block:: python + + Snackbar( + text="This is a snackbar!", + snackbar_x="10dp", + snackbar_y="10dp", + size_hint_x=.5 + ).open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-percent-width.png + :align: center + +Custom text color +----------------- + +.. code-block:: python + + Snackbar( + text="[color=#ddbb34]This is a snackbar![/color]", + snackbar_y="10dp", + snackbar_y="10dp", + size_hint_x=.7 + ).open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-custom-color.png + :align: center + +Usage with button +----------------- + +.. code-block:: python + + snackbar = Snackbar( + text="This is a snackbar!", + snackbar_x="10dp", + snackbar_y="10dp", + ) + snackbar.size_hint_x = ( + Window.width - (snackbar.snackbar_x * 2) + ) / Window.width + snackbar.buttons = [ + MDFlatButton( + text="UPDATE", + text_color=(1, 1, 1, 1), + on_release=snackbar.dismiss, + ), + MDFlatButton( + text="CANCEL", + text_color=(1, 1, 1, 1), + on_release=snackbar.dismiss, + ), + ] + snackbar.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-button.png + :align: center + +Using a button with custom color +------------------------------- + +.. code-block:: python + + Snackbar( + ... + bg_color=(0, 0, 1, 1), + ).open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-button-custom-color.png + :align: center + +Custom usage +------------ + +.. code-block:: python + + from kivy.lang import Builder + from kivy.animation import Animation + from kivy.clock import Clock + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.snackbar import Snackbar + + + KV = ''' + Screen: + + MDFloatingActionButton: + id: button + x: root.width - self.width - dp(10) + y: dp(10) + on_release: app.snackbar_show() + ''' + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + self.snackbar = None + self._interval = 0 + + def build(self): + return self.screen + + def wait_interval(self, interval): + self._interval += interval + if self._interval > self.snackbar.duration + 0.5: + anim = Animation(y=dp(10), d=.2) + anim.start(self.screen.ids.button) + Clock.unschedule(self.wait_interval) + self._interval = 0 + self.snackbar = None + + def snackbar_show(self): + if not self.snackbar: + self.snackbar = Snackbar(text="This is a snackbar!") + self.snackbar.open() + anim = Animation(y=dp(72), d=.2) + anim.bind(on_complete=lambda *args: Clock.schedule_interval( + self.wait_interval, 0)) + anim.start(self.screen.ids.button) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-custom-usage.gif + :align: center + +Custom Snackbar +--------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.core.window import Window + from kivy.properties import StringProperty, NumericProperty + + from kivymd.app import MDApp + from kivymd.uix.button import MDFlatButton + from kivymd.uix.snackbar import BaseSnackbar + + KV = ''' + + + MDIconButton: + pos_hint: {'center_y': .5} + icon: root.icon + opposite_colors: True + + MDLabel: + id: text_bar + size_hint_y: None + height: self.texture_size[1] + text: root.text + font_size: root.font_size + theme_text_color: 'Custom' + text_color: get_color_from_hex('ffffff') + shorten: True + shorten_from: 'right' + pos_hint: {'center_y': .5} + + + Screen: + + MDRaisedButton: + text: "SHOW" + pos_hint: {"center_x": .5, "center_y": .45} + on_press: app.show() + ''' + + + class CustomSnackbar(BaseSnackbar): + text = StringProperty(None) + icon = StringProperty(None) + font_size = NumericProperty("15sp") + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def show(self): + snackbar = CustomSnackbar( + text="This is a snackbar!", + icon="information", + snackbar_x="10dp", + snackbar_y="10dp", + buttons=[MDFlatButton(text="ACTION", text_color=(1, 1, 1, 1))] + ) + snackbar.size_hint_x = ( + Window.width - (snackbar.snackbar_x * 2) + ) / Window.width + snackbar.open() + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-custom.png + :align: center +""" + +__all__ = ("Snackbar",) + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.lang import Builder +from kivy.properties import ( + BooleanProperty, + ListProperty, + NumericProperty, + OptionProperty, + StringProperty, +) + +from kivymd.uix.button import BaseButton +from kivymd.uix.card import MDCard + +Builder.load_string( + """ +#:import get_color_from_hex kivy.utils.get_color_from_hex +#:import window kivy.core.window + + + size_hint_y: None + height: "58dp" + spacing: "10dp" + padding: "10dp", "10dp", "10dp", "10dp" + md_bg_color: get_color_from_hex("323232") if not root.bg_color else root.bg_color + radius: root.radius + elevation: 11 if root.padding else 0 + + canvas: + Color: + rgba: self.md_bg_color + RoundedRectangle: + size: self.size + pos: self.pos + radius: self.radius + + + + MDLabel: + id: text_bar + size_hint_y: None + height: self.texture_size[1] + text: root.text + font_size: root.font_size + theme_text_color: "Custom" + text_color: get_color_from_hex("ffffff") + shorten: True + shorten_from: "right" + markup: True + pos_hint: {"center_y": .5} +""" +) + + +class BaseSnackbar(MDCard): + """ + :Events: + :attr:`on_open` + Called when a dialog is opened. + :attr:`on_dismiss` + When the front layer rises. + + Abstract base class for all Snackbars. + This class handles sizing, positioning, shape and events for Snackbars + + All Snackbars will be made off of this `BaseSnackbar`. + + `BaseSnackbar` will always try to fill the remainder of the screen with + your Snackbar. + + To make your Snackbar dynamic and symetric with snackbar_x. + + Set size_hint_x like below: + + .. code-block:: python + + size_hint_z = ( + Window.width - (snackbar_x * 2) + ) / Window.width + """ + + duration = NumericProperty(3) + """ + The amount of time that the snackbar will stay on screen for. + + :attr:`duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `3`. + """ + + auto_dismiss = BooleanProperty(True) + """ + Whether to use automatic closing of the snackbar or not. + + :attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `'True'`. + """ + + bg_color = ListProperty() + """ + Snackbar background. + + :attr:`bg_color` is a :class:`~kivy.properties.ListProperty` + and defaults to `'[]'`. + """ + + buttons = ListProperty() + """ + Snackbar buttons. + + :attr:`buttons` is a :class:`~kivy.properties.ListProperty` + and defaults to `'[]'` + """ + + radius = ListProperty([5, 5, 5, 5]) + """ + Snackbar radius. + + :attr:`radius` is a :class:`~kivy.properties.ListProperty` + and defaults to `'[5, 5, 5, 5]'` + """ + + snackbar_animation_dir = OptionProperty( + "Bottom", + options=[ + "Top", + "Bottom", + "Left", + "Right", + ], + ) + """ + Snackbar animation direction. + + Available options are: `"Top"`, `"Bottom"`, `"Left"`, `"Right"` + + :attr:`snackbar_animation_dir` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Bottom'`. + """ + + snackbar_x = NumericProperty("0dp") + """ + The snackbar x position in the screen + + :attr:`snackbar_x` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0dp`. + """ + + snackbar_y = NumericProperty("0dp") + """ + The snackbar x position in the screen + + :attr:`snackbar_y` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0dp`. + """ + + _interval = 0 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_open") + self.register_event_type("on_dismiss") + + def dismiss(self, *args): + """Dismiss the snackbar.""" + + def dismiss(interval): + if self.snackbar_animation_dir == "Top": + anim = Animation(y=(Window.height + self.height), d=0.2) + elif self.snackbar_animation_dir == "Left": + anim = Animation(x=-self.width, d=0.2) + elif self.snackbar_animation_dir == "Right": + anim = Animation(x=Window.width, d=0.2) + else: + anim = Animation(y=-self.height, d=0.2) + + anim.bind( + on_complete=lambda *args: Window.parent.remove_widget(self) + ) + anim.start(self) + + Clock.schedule_once(dismiss, 0.5) + self.dispatch("on_dismiss") + + def open(self): + """Show the snackbar.""" + + def wait_interval(interval): + self._interval += interval + if self._interval > self.duration: + self.dismiss() + Clock.unschedule(wait_interval) + self._interval = 0 + + for c in Window.parent.children: + if isinstance(c, BaseSnackbar): + return + + if self.snackbar_y > (Window.height - self.height): + self.snackbar_y = Window.height - self.height + + self._calc_radius() + + if self.size_hint_x == 1: + self.size_hint_x = (Window.width - self.snackbar_x) / Window.width + + if ( + self.snackbar_animation_dir == "Top" + or self.snackbar_animation_dir == "Bottom" + ): + self.x = self.snackbar_x + + if self.snackbar_animation_dir == "Top": + self.y = Window.height + self.height + else: + self.y = -self.height + + Window.parent.add_widget(self) + + if self.snackbar_animation_dir == "Top": + anim = Animation( + y=self.snackbar_y + if self.snackbar_y != 0 + else Window.height - self.height, + d=0.2, + ) + else: + anim = Animation( + y=self.snackbar_y if self.snackbar_y != 0 else 0, d=0.2 + ) + + elif ( + self.snackbar_animation_dir == "Left" + or self.snackbar_animation_dir == "Right" + ): + self.y = self.snackbar_y + + if self.snackbar_animation_dir == "Left": + self.x = -Window.width + else: + self.x = Window.width + + Window.parent.add_widget(self) + anim = Animation( + x=self.snackbar_x if self.snackbar_x != 0 else 0, d=0.2 + ) + + if self.auto_dismiss: + anim.bind( + on_complete=lambda *args: Clock.schedule_interval( + wait_interval, 0 + ) + ) + anim.start(self) + self.dispatch("on_open") + + def on_open(self, *args): + """Called when a dialog is opened.""" + + def on_dismiss(self, *args): + """Called when the dialog is closed.""" + + def on_buttons(self, instance, value): + def on_buttons(interval): + for button in value: + if issubclass(button.__class__, (BaseButton,)): + self.add_widget(button) + else: + raise ValueError( + f"The {button} object must be inherited from the base class " + ) + + Clock.schedule_once(on_buttons) + + def _calc_radius(self): + if ( + self.snackbar_animation_dir == "Top" + or self.snackbar_animation_dir == "Bottom" + ): + + if self.snackbar_y == 0 and self.snackbar_x == 0: + + if self.size_hint_x == 1: + self.radius = [0, 0, 0, 0] + else: + if self.snackbar_animation_dir == "Top": + self.radius = [0, 0, self.radius[2], 0] + else: + self.radius = [0, self.radius[1], 0, 0] + + elif self.snackbar_y != 0 and self.snackbar_x == 0: + + if self.size_hint_x == 1: + self.radius = [0, 0, 0, 0] + else: + if self.snackbar_y >= Window.height - self.height: + self.radius = [0, 0, self.radius[2], 0] + else: + self.radius = [0, self.radius[1], self.radius[2], 0] + + elif self.snackbar_y == 0 and self.snackbar_x != 0: + + if self.size_hint_x == 1: + if self.snackbar_animation_dir == "Top": + self.radius = [0, 0, 0, self.radius[3]] + else: + self.radius = [self.radius[0], 0, 0, 0] + else: + if self.snackbar_animation_dir == "Top": + self.radius = [0, 0, self.radius[2], self.radius[3]] + else: + self.radius = [self.radius[0], self.radius[1], 0, 0] + + else: # self.snackbar_y != 0 and self.snackbar_x != 0 + + if self.size_hint_x == 1: + self.radius = [self.radius[0], 0, 0, self.radius[3]] + elif self.snackbar_y >= Window.height - self.height: + self.radius = [0, 0, self.radius[2], self.radius[3]] + + elif ( + self.snackbar_animation_dir == "Left" + or self.snackbar_animation_dir == "Right" + ): + + if self.snackbar_y == 0 and self.snackbar_x == 0: + + if self.size_hint_x == 1: + self.radius = [0, 0, 0, 0] + else: + self.radius = [0, self.radius[1], 0, 0] + + elif self.snackbar_y != 0 and self.snackbar_x == 0: + + if self.size_hint_x == 1: + self.radius = [0, 0, 0, 0] + else: + self.radius = [0, self.radius[1], self.radius[2], 0] + + elif self.snackbar_y == 0 and self.snackbar_x != 0: + + if self.size_hint_x == 1: + self.radius = [self.radius[0], 0, 0, 0] + else: + self.radius = [self.radius[0], self.radius[1], 0, 0] + + else: # self.snackbar_y != 0 and self.snackbar_x != 0 + + if self.size_hint_x == 1: + if self.snackbar_y >= Window.height - self.height: + self.radius = [0, 0, 0, self.radius[3]] + else: + self.radius = [self.radius[0], 0, 0, self.radius[3]] + elif self.snackbar_y >= Window.height - self.height: + self.radius = [0, 0, self.radius[2], self.radius[3]] + + +class Snackbar(BaseSnackbar): + """ + Snackbar inherits all its functionality from `BaseSnackbar` + """ + + text = StringProperty() + """ + The text that will appear in the snackbar. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + font_size = NumericProperty("15sp") + """ + The font size of the text that will appear in the snackbar. + + :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and + defaults to `'15sp'`. + """ diff --git a/kivymd/uix/spinner.py b/kivymd/uix/spinner.py new file mode 100755 index 0000000..748979a --- /dev/null +++ b/kivymd/uix/spinner.py @@ -0,0 +1,260 @@ +""" +Components/Spinner +================== + +.. rubric:: Circular progress indicator in Google's Material Design. + +Usage +----- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + Screen: + + MDSpinner: + size_hint: None, None + size: dp(46), dp(46) + pos_hint: {'center_x': .5, 'center_y': .5} + active: True if check.active else False + + MDCheckbox: + id: check + size_hint: None, None + size: dp(48), dp(48) + pos_hint: {'center_x': .5, 'center_y': .4} + active: True + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/spinner.gif + :align: center + +Spinner palette +--------------- + +.. code-block:: kv + + MDSpinner: + # The number of color values ​​can be any. + palette: + [0.28627450980392155, 0.8431372549019608, 0.596078431372549, 1], \ + [0.3568627450980392, 0.3215686274509804, 0.8666666666666667, 1], \ + [0.8862745098039215, 0.36470588235294116, 0.592156862745098, 1], \ + [0.8784313725490196, 0.9058823529411765, 0.40784313725490196, 1], + +.. code-block:: python + + MDSpinner( + size_hint=(None, None), + size=(dp(46), dp(46)), + pos_hint={'center_x': .5, 'center_y': .5}, + active=True, + palette=[ + [0.28627450980392155, 0.8431372549019608, 0.596078431372549, 1], + [0.3568627450980392, 0.3215686274509804, 0.8666666666666667, 1], + [0.8862745098039215, 0.36470588235294116, 0.592156862745098, 1], + [0.8784313725490196, 0.9058823529411765, 0.40784313725490196, 1], + ] + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/spinner-palette.gif + :align: center +""" + +__all__ = ("MDSpinner",) + +from kivy.animation import Animation +from kivy.lang import Builder +from kivy.properties import BooleanProperty, ListProperty, NumericProperty +from kivy.uix.widget import Widget + +from kivymd.theming import ThemableBehavior + +Builder.load_string( + """ + + canvas.before: + PushMatrix + Rotate: + angle: self._rotation_angle + origin: self.center + canvas: + Color: + rgba: self.color + a: self._alpha + SmoothLine: + circle: self.center_x, self.center_y, self.width / 2,\ + self._angle_start, self._angle_end + cap: 'square' + width: dp(2.25) + canvas.after: + PopMatrix + +""" +) + + +class MDSpinner(ThemableBehavior, Widget): + """:class:`MDSpinner` is an implementation of the circular progress + indicator in `Google's Material Design`. + + It can be used either as an indeterminate indicator that loops while + the user waits for something to happen, or as a determinate indicator. + + Set :attr:`determinate` to **True** to activate determinate mode, and + :attr:`determinate_time` to set the duration of the animation. + """ + + determinate = BooleanProperty(False) + """ + Determinate value. + + :attr:`determinate` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + determinate_time = NumericProperty(2) + """ + Determinate time value. + + :attr:`determinate_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to `2`. + """ + + active = BooleanProperty(True) + """Use :attr:`active` to start or stop the spinner. + + :attr:`active` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + color = ListProperty([0, 0, 0, 0]) + """ + Spinner color. + + :attr:`color` is a :class:`~kivy.properties.ListProperty` + and defaults to ``self.theme_cls.primary_color``. + """ + + palette = ListProperty() + """ + A set of colors. Changes with each completed spinner cycle. + + :attr:`palette` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + _alpha = NumericProperty(0) + _rotation_angle = NumericProperty(360) + _angle_start = NumericProperty(0) + _angle_end = NumericProperty(0) + _palette = [] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.color = self.theme_cls.primary_color + self._alpha_anim_in = Animation(_alpha=1, duration=0.8, t="out_quad") + self._alpha_anim_out = Animation(_alpha=0, duration=0.3, t="out_quad") + self._alpha_anim_out.bind(on_complete=self._reset) + self.theme_cls.bind(primary_color=self._update_color) + + if self.determinate: + self._start_determinate() + else: + self._start_loop() + + def _update_color(self, *args): + self.color = self.theme_cls.primary_color + + def _start_determinate(self, *args): + self._alpha_anim_in.start(self) + + _rot_anim = Animation( + _rotation_angle=0, + duration=self.determinate_time * 0.7, + t="out_quad", + ) + _rot_anim.start(self) + + _angle_start_anim = Animation( + _angle_end=360, duration=self.determinate_time, t="in_out_quad" + ) + _angle_start_anim.bind( + on_complete=lambda *x: self._alpha_anim_out.start(self) + ) + + _angle_start_anim.start(self) + + def _start_loop(self, *args): + if self._alpha == 0: + _rot_anim = Animation(_rotation_angle=0, duration=2, t="linear") + _rot_anim.start(self) + + self._alpha = 1 + self._alpha_anim_in.start(self) + _angle_start_anim = Animation( + _angle_end=self._angle_end + 270, duration=0.6, t="in_out_cubic" + ) + _angle_start_anim.bind(on_complete=self._anim_back) + _angle_start_anim.start(self) + + def _anim_back(self, *args): + _angle_back_anim = Animation( + _angle_start=self._angle_end - 8, duration=0.6, t="in_out_cubic" + ) + _angle_back_anim.bind(on_complete=self._start_loop) + + _angle_back_anim.start(self) + + def on__rotation_angle(self, *args): + if self._rotation_angle == 0: + self._rotation_angle = 360 + if not self.determinate: + _rot_anim = Animation(_rotation_angle=0, duration=2) + _rot_anim.start(self) + elif self._rotation_angle == 360: + if self._palette: + try: + Animation(color=next(self._palette), duration=2).start(self) + except StopIteration: + self._palette = iter(self.palette) + Animation(color=next(self._palette), duration=2).start(self) + + def _reset(self, *args): + Animation.cancel_all( + self, + "_angle_start", + "_rotation_angle", + "_angle_end", + "_alpha", + "color", + ) + self._angle_start = 0 + self._angle_end = 0 + self._rotation_angle = 360 + self._alpha = 0 + self.active = False + + def on_palette(self, instance, value): + self._palette = iter(value) + + def on_active(self, *args): + if not self.active: + self._reset() + else: + if self.determinate: + self._start_determinate() + else: + self._start_loop() diff --git a/kivymd/uix/stacklayout.py b/kivymd/uix/stacklayout.py new file mode 100644 index 0000000..09f7005 --- /dev/null +++ b/kivymd/uix/stacklayout.py @@ -0,0 +1,92 @@ +""" +Components/StackLayout +====================== + +:class:`~kivy.uix.stacklayout.StackLayout` class equivalent. Simplifies working +with some widget properties. For example: + +StackLayout +----------- + +.. code-block:: + + StackLayout: + size_hint_y: None + height: self.minimum_height + + canvas: + Color: + rgba: app.theme_cls.primary_color + Rectangle: + pos: self.pos + size: self.size + +MDStackLayout +------------- + +.. code-block:: + + MDStackLayout: + 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.stacklayout import StackLayout + +from kivymd.uix import MDAdaptiveWidget + + +class MDStackLayout(StackLayout, MDAdaptiveWidget): + pass diff --git a/kivymd/uix/tab.py b/kivymd/uix/tab.py new file mode 100755 index 0000000..2bd5a04 --- /dev/null +++ b/kivymd/uix/tab.py @@ -0,0 +1,1148 @@ +""" +Components/Tabs +=============== + +.. seealso:: + + `Material Design spec, Tabs `_ + +.. rubric:: Tabs organize content across different screens, data sets, + and other interactions. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs.png + :align: center + +.. Note:: Module provides tabs in the form of icons or text. + +Usage +----- + +To create a tab, you must create a new class that inherits from the +:class:`~MDTabsBase` class and the `Kivy` container, in which you will create +content for the tab. + +.. code-block:: python + + class Tab(FloatLayout, MDTabsBase): + '''Class implementing content for a tab.''' + +.. code-block:: kv + + : + + MDLabel: + text: "Content" + pos_hint: {"center_x": .5, "center_y": .5} + +Tabs must be placed in the :class:`~MDTabs` container: + +.. code-block:: kv + + Root: + + MDTabs: + + Tab: + text: "Tab 1" + + Tab: + text: "Tab 1" + + ... + +Example with tab icon +--------------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.uix.floatlayout import FloatLayout + + from kivymd.app import MDApp + from kivymd.uix.tab import MDTabsBase + from kivymd.icon_definitions import md_icons + + KV = ''' + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Example Tabs" + + MDTabs: + id: tabs + on_tab_switch: app.on_tab_switch(*args) + + + : + + MDIconButton: + id: icon + icon: app.icons[0] + user_font_size: "48sp" + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class Tab(FloatLayout, MDTabsBase): + '''Class implementing content for a tab.''' + + + class Example(MDApp): + icons = list(md_icons.keys())[15:30] + + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for name_tab in self.icons: + self.root.ids.tabs.add_widget(Tab(text=name_tab)) + + def on_tab_switch( + self, instance_tabs, instance_tab, instance_tab_label, tab_text + ): + '''Called when switching tabs. + + :type instance_tabs: ; + :param instance_tab: <__main__.Tab object>; + :param instance_tab_label: ; + :param tab_text: text or name icon of tab; + ''' + + count_icon = [k for k, v in md_icons.items() if v == tab_text] + instance_tab.ids.icon.icon = count_icon[0] + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example.gif + :align: center + +Example with tab text +--------------------- + +.. Note:: The :class:`~MDTabsBase` class has an icon parameter and, by default, + tries to find the name of the icon in the file + ``kivymd/icon_definitions.py``. If the name of the icon is not found, + then the name of the tab will be plain text, if found, the tab will look + like the corresponding icon. + +.. code-block:: python + + from kivy.lang import Builder + from kivy.uix.floatlayout import FloatLayout + + from kivymd.app import MDApp + from kivymd.uix.tab import MDTabsBase + + KV = ''' + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Example Tabs" + + MDTabs: + id: tabs + on_tab_switch: app.on_tab_switch(*args) + + + : + + MDLabel: + id: label + text: "Tab 0" + halign: "center" + ''' + + + class Tab(FloatLayout, MDTabsBase): + '''Class implementing content for a tab.''' + + + class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for i in range(20): + self.root.ids.tabs.add_widget(Tab(text=f"Tab {i}")) + + def on_tab_switch( + self, instance_tabs, instance_tab, instance_tab_label, tab_text + ): + '''Called when switching tabs. + + :type instance_tabs: ; + :param instance_tab: <__main__.Tab object>; + :param instance_tab_label: ; + :param tab_text: text or name icon of tab; + ''' + + instance_tab.ids.label.text = tab_text + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example-text.gif + :align: center + +Example with tab icon and text +------------------------------ + +.. code-block:: python + + from kivy.lang import Builder + from kivy.uix.floatlayout import FloatLayout + + from kivymd.app import MDApp + from kivymd.uix.tab import MDTabsBase + from kivymd.font_definitions import fonts + from kivymd.icon_definitions import md_icons + + KV = ''' + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Example Tabs" + + MDTabs: + id: tabs + ''' + + + class Tab(FloatLayout, MDTabsBase): + pass + + + class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for name_tab in list(md_icons.keys())[15:30]: + self.root.ids.tabs.add_widget( + Tab( + text=f"[size=20][font={fonts[-1]['fn_regular']}]{md_icons[name_tab]}[/size][/font] {name_tab}" + ) + ) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example-icon-text.png + :align: center + +Dynamic tab management +---------------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.uix.scrollview import ScrollView + + from kivymd.app import MDApp + from kivymd.uix.tab import MDTabsBase + + KV = ''' + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Example Tabs" + + MDTabs: + id: tabs + + + : + + MDList: + + MDBoxLayout: + adaptive_height: True + + MDFlatButton: + text: "ADD TAB" + on_release: app.add_tab() + + MDFlatButton: + text: "REMOVE LAST TAB" + on_release: app.remove_tab() + + MDFlatButton: + text: "GET TAB LIST" + on_release: app.get_tab_list() + ''' + + + class Tab(ScrollView, MDTabsBase): + '''Class implementing content for a tab.''' + + + class Example(MDApp): + index = 0 + + def build(self): + return Builder.load_string(KV) + + def on_start(self): + self.add_tab() + + def get_tab_list(self): + '''Prints a list of tab objects.''' + + print(self.root.ids.tabs.get_tab_list()) + + def add_tab(self): + self.index += 1 + self.root.ids.tabs.add_widget(Tab(text=f"{self.index} tab")) + + def remove_tab(self): + if self.index > 1: + self.index -= 1 + self.root.ids.tabs.remove_widget( + self.root.ids.tabs.get_tab_list()[0] + ) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-dynamic-managmant.gif + :align: center + +Use on_ref_press method +----------------------- + +You can use markup for the text of the tabs and use the ``on_ref_press`` +method accordingly: + +.. code-block:: python + + from kivy.lang import Builder + from kivy.uix.floatlayout import FloatLayout + + from kivymd.app import MDApp + from kivymd.font_definitions import fonts + from kivymd.uix.tab import MDTabsBase + from kivymd.icon_definitions import md_icons + + KV = ''' + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Example Tabs" + + MDTabs: + id: tabs + on_ref_press: app.on_ref_press(*args) + + + : + + MDIconButton: + id: icon + icon: app.icons[0] + user_font_size: "48sp" + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class Tab(FloatLayout, MDTabsBase): + '''Class implementing content for a tab.''' + + + class Example(MDApp): + icons = list(md_icons.keys())[15:30] + + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for name_tab in self.icons: + self.root.ids.tabs.add_widget( + Tab( + text=f"[ref={name_tab}][font={fonts[-1]['fn_regular']}]{md_icons['close']}[/font][/ref] {name_tab}" + ) + ) + + def on_ref_press( + self, + instance_tabs, + instance_tab_label, + instance_tab, + instance_tab_bar, + instance_carousel, + ): + ''' + The method will be called when the ``on_ref_press`` event + occurs when you, for example, use markup text for tabs. + + :param instance_tabs: + :param instance_tab_label: + :param instance_tab: <__main__.Tab object> + :param instance_tab_bar: + :param instance_carousel: + ''' + + # Removes a tab by clicking on the close icon on the left. + for instance_tab in instance_carousel.slides: + if instance_tab.text == instance_tab_label.text: + instance_tabs.remove_widget(instance_tab_label) + break + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-on-ref-press.gif + :align: center + +Switching the tab by name +------------------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.uix.floatlayout import FloatLayout + + from kivymd.app import MDApp + from kivymd.uix.tab import MDTabsBase + from kivymd.icon_definitions import md_icons + + KV = ''' + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "Example Tabs" + + MDTabs: + id: tabs + + + : + + MDIconButton: + id: icon + icon: "arrow-right" + user_font_size: "48sp" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.switch_tab() + ''' + + + class Tab(FloatLayout, MDTabsBase): + '''Class implementing content for a tab.''' + + + class Example(MDApp): + icons = list(md_icons.keys())[15:30] + + def build(self): + self.iter_list = iter(list(self.icons)) + return Builder.load_string(KV) + + def on_start(self): + for name_tab in list(self.icons): + self.root.ids.tabs.add_widget(Tab(text=name_tab)) + + def switch_tab(self): + '''Switching the tab by name.''' + + try: + self.root.ids.tabs.switch_tab(next(self.iter_list)) + except StopIteration: + pass + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/switching-tab-by-name.gif + :align: center +""" + +__all__ = ("MDTabs", "MDTabsBase") + +from kivy.clock import Clock +from kivy.graphics import Rectangle +from kivy.lang import Builder +from kivy.properties import ( + AliasProperty, + BooleanProperty, + BoundedNumericProperty, + ListProperty, + NumericProperty, + ObjectProperty, + StringProperty, +) +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.behaviors import ToggleButtonBehavior +from kivy.uix.label import Label +from kivy.uix.scrollview import ScrollView +from kivy.uix.widget import Widget +from kivy.utils import boundary + +from kivymd import fonts_path +from kivymd.icon_definitions import md_icons +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + RectangularElevationBehavior, + SpecificBackgroundColorBehavior, +) +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.carousel import MDCarousel + +Builder.load_string( + """ +#:import DampedScrollEffect kivy.effects.dampedscroll.DampedScrollEffect + + + + size_hint: None, 1 + halign: 'center' + padding: '12dp', 0 + group: 'tabs' + font: root.font_name + allow_no_selection: False + markup: True + on_ref_press: + self.tab_bar.parent.dispatch(\ + "on_ref_press", + self, \ + self.tab, \ + self.tab_bar, \ + self.tab_bar.parent.carousel) + text_color_normal: + (\ + (0, 0, 0, .5) \ + if app.theme_cls.theme_style == 'Dark' and not self.text_color_normal \ + else (1, 1, 1, .6) \ + if app.theme_cls.theme_style == 'Light' and not self.text_color_normal \ + else self.text_color_normal \ + ) + text_color_active: + (\ + (0, 0, 0, .75) \ + if app.theme_cls.theme_style == 'Dark' and not self.text_color_active \ + else (1, 1, 1, 1) \ + if app.theme_cls.theme_style == 'Light' and not self.text_color_normal \ + else self.text_color_active + ) + color: + self.text_color_active if self.state == 'down' \ + else self.text_color_normal + on_x: self._trigger_update_tab_indicator() + on_width: self._trigger_update_tab_indicator() + + + + size_hint: 1, 1 + do_scroll_y: False + bar_color: 0, 0, 0, 0 + bar_inactive_color: 0, 0, 0, 0 + bar_width: 0 + effect_cls: DampedScrollEffect + + + + carousel: carousel + tab_bar: tab_bar + anchor_y: 'top' + background_palette: "Primary" + text_color_normal: self.specific_secondary_text_color + text_color_active: self.specific_text_color + + MDTabsMain: + padding: 0, tab_bar.height, 0, 0 + + MDTabsCarousel: + id: carousel + lock_swiping: root.lock_swiping + ignore_perpendicular_swipes: True + anim_move_duration: root.anim_duration + on_index: root.on_carousel_index(*args) + on__offset: tab_bar.android_animation(*args) + on_slides: self.index = root.default_tab + on_slides: root.on_carousel_index(self, 0) + + MDTabsBar: + id: tab_bar + carousel: carousel + scrollview: scrollview + layout: layout + size_hint: 1, None + elevation: root.elevation + height: root.tab_bar_height + md_bg_color: self.theme_cls.primary_color if not root.background_color else root.background_color + + MDTabsScrollView: + id: scrollview + on_width: tab_bar._trigger_update_tab_bar() + + MDGridLayout: + id: layout + rows: 1 + size_hint_y: 1 + adaptive_width: True + on_width: tab_bar._trigger_update_tab_bar() + + canvas.after: + Color: + rgba: root.theme_cls.accent_color if not root.color_indicator else root.color_indicator + Rectangle: + pos: self.pos + size: 0, root.tab_indicator_height +""" +) + + +class MDTabsException(Exception): + pass + + +class MDTabsLabel(ToggleButtonBehavior, Label): + """This class it represent the label of each tab.""" + + text_color_normal = ListProperty((1, 1, 1, 1)) + text_color_active = ListProperty((1, 1, 1, 1)) + tab = ObjectProperty() + tab_bar = ObjectProperty() + font_name = StringProperty("Roboto") + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.min_space = 0 + + def on_release(self): + self.tab_bar.parent.dispatch("on_tab_switch", self.tab, self, self.text) + # if the label is selected load the relative tab from carousel + if self.state == "down": + self.tab_bar.parent.carousel.load_slide(self.tab) + + def on_texture(self, widget, texture): + # just save the minimum width of the label based of the content + if texture: + self.width = texture.width + self.min_space = self.width + + def _trigger_update_tab_indicator(self): + # update the position and size of the indicator + # when the label changes size or position + if self.state == "down": + self.tab_bar.update_indicator(self.x, self.width) + + +class MDTabsBase(Widget): + """ + This class allow you to create a tab. + You must create a new class that inherits from MDTabsBase. + In this way you have total control over the views of your tabbed panel. + """ + + text = StringProperty() + """ + It will be the label text of the tab. + + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + tab_label = ObjectProperty() + """ + It is the label object reference of the tab. + + :attr:`tab_label` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + def __init__(self, **kwargs): + self.tab_label = MDTabsLabel(tab=self) + super().__init__(**kwargs) + + def on_text(self, widget, text): + # Set the icon + if text in md_icons: + self.tab_label.font_name = ( + fonts_path + "materialdesignicons-webfont.ttf" + ) + self.tab_label.text = md_icons[self.text] + self.tab_label.font_size = "24sp" + # Set the label text + else: + self.tab_label.text = self.text + + +class MDTabsMain(MDBoxLayout): + """ + This class is just a boxlayout that contain the carousel. + It allows you to have control over the carousel. + """ + + +class MDTabsCarousel(MDCarousel): + lock_swiping = BooleanProperty(False) + """ + If True - disable switching tabs by swipe. + + :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + def on_touch_move(self, touch): + # lock a swiping + if self.lock_swiping: + return + if not self.touch_mode_change: + if self.ignore_perpendicular_swipes and self.direction in ( + "top", + "bottom", + ): + if abs(touch.oy - touch.y) < self.scroll_distance: + if abs(touch.ox - touch.x) > self.scroll_distance: + self._change_touch_mode() + self.touch_mode_change = True + elif self.ignore_perpendicular_swipes and self.direction in ( + "right", + "left", + ): + if abs(touch.ox - touch.x) < self.scroll_distance: + if abs(touch.oy - touch.y) > self.scroll_distance: + self._change_touch_mode() + self.touch_mode_change = True + + if self._get_uid("cavoid") in touch.ud: + return + if self._touch is not touch: + super().on_touch_move(touch) + return self._get_uid() in touch.ud + if touch.grab_current is not self: + return True + ud = touch.ud[self._get_uid()] + direction = self.direction[0] + if ud["mode"] == "unknown": + if direction in "rl": + distance = abs(touch.ox - touch.x) + else: + distance = abs(touch.oy - touch.y) + if distance > self.scroll_distance: + ev = self._change_touch_mode_ev + if ev is not None: + ev.cancel() + ud["mode"] = "scroll" + else: + if direction in "rl": + self._offset += touch.dx + if direction in "tb": + self._offset += touch.dy + return True + + +class MDTabsScrollView(ScrollView): + """This class hacked version to fix scroll_x manual setting.""" + + def goto(self, scroll_x, scroll_y): + """Update event value along with scroll_*.""" + + def _update(e, x): + if e: + e.value = (e.max + e.min) * x + + if not (scroll_x is None): + self.scroll_x = scroll_x + _update(self.effect_x, scroll_x) + + if not (scroll_y is None): + self.scroll_y = scroll_y + _update(self.effect_y, scroll_y) + + +class MDTabsBar(ThemableBehavior, RectangularElevationBehavior, MDBoxLayout): + """ + This class is just a boxlayout that contains the scroll view for tabs. + He is also responsible for resizing the tab shortcut when necessary. + """ + + target = ObjectProperty(None, allownone=True) + """ + Is the carousel reference of the next tab / slide. + When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the + target tab / slide of the carousel. + + :attr:`target` is an :class:`~kivy.properties.ObjectProperty` + and default to `None`. + """ + + def get_rect_instruction(self): + for i in self.layout.canvas.after.children: + if isinstance(i, Rectangle): + return i + + indicator = AliasProperty(get_rect_instruction, cache=True) + """ + Is the Rectangle instruction reference of the tab indicator. + + :attr:`indicator` is an :class:`~kivy.properties.AliasProperty`. + """ + + def get_last_scroll_x(self): + return self.scrollview.scroll_x + + last_scroll_x = AliasProperty( + get_last_scroll_x, bind=("target",), cache=True + ) + """ + Is the carousel reference of the next tab/slide. + When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the + target tab/slide of the carousel. + + :attr:`last_scroll_x` is an :class:`~kivy.properties.AliasProperty`. + """ + + def __init__(self, **kwargs): + self._trigger_update_tab_bar = Clock.schedule_once( + self._update_tab_bar, 0 + ) + super().__init__(**kwargs) + + def _update_tab_bar(self, *args): + if self.parent.allow_stretch: + # update width of the labels when it is needed + width, tabs = self.scrollview.width, self.layout.children + tabs_widths = [t.min_space for t in tabs if t.min_space] + tabs_space = float(sum(tabs_widths)) + + if not tabs_space: + return + + ratio = width / tabs_space + use_ratio = True in (width / len(tabs) < w for w in tabs_widths) + + for t in tabs: + t.width = ( + t.min_space + if tabs_space > width + else t.min_space * ratio + if use_ratio is True + else width / len(tabs) + ) + + def update_indicator(self, x, w): + # update position and size of the indicator + self.indicator.pos = (x, 0) + self.indicator.size = (w, self.indicator.size[1]) + + def tab_bar_autoscroll(self, target, step): + # automatic scroll animation of the tab bar. + bound_left = self.center_x + bound_right = self.layout.width - bound_left + dt = target.center_x - bound_left + sx, sy = self.scrollview.convert_distance_to_scroll(dt, 0) + + # last scroll x of the tab bar + lsx = self.last_scroll_x + # determine scroll direction + scroll_is_late = lsx < sx + # distance to run + dst = abs(lsx - sx) * step + + if not dst: + return + + if scroll_is_late and target.center_x > bound_left: + x = lsx + dst + + elif not scroll_is_late and target.center_x < bound_right: + x = lsx - dst + + x = boundary(x, 0.0, 1.0) + self.scrollview.goto(x, None) + + def android_animation(self, carousel, offset): + # try to reproduce the android animation effect. + if offset != 0 and abs(offset) < carousel.width: + forward = offset < 0 + offset = abs(offset) + step = offset / float(carousel.width) + distance = abs(offset - carousel.width) + threshold = self.parent.anim_threshold + breakpoint = carousel.width - (carousel.width * threshold) + traveled = distance / breakpoint if breakpoint else 0 + break_step = 1.0 - traveled + indicator_animation = self.parent.tab_indicator_anim + + skip_slide = ( + carousel.slides[carousel._skip_slide] + if carousel._skip_slide is not None + else None + ) + next_slide = ( + carousel.next_slide if forward else carousel.previous_slide + ) + self.target = skip_slide if skip_slide else next_slide + + if not self.target: + return + + a = carousel.current_slide.tab_label + b = self.target.tab_label + self.tab_bar_autoscroll(b, step) + + if not indicator_animation: + return + + if step <= threshold: + if forward: + gap_w = abs((a.x + a.width) - (b.x + b.width)) + w_step = a.width + (gap_w * step) + x_step = a.x + else: + gap = abs((a.x - b.x)) + x_step = a.x - gap * step + w_step = a.width + gap * step + else: + if forward: + x_step = a.x + abs((a.x - b.x)) * break_step + gap_w = abs((a.x + a.width) - (b.x + b.width)) + ind_width = a.width + gap_w * threshold + gap_w = ind_width - b.width + w_step = ind_width - (gap_w * break_step) + else: + x_step = a.x - abs((a.x - b.x)) * threshold + x_step = x_step - abs(x_step - b.x) * break_step + ind_width = ( + (a.x + a.width) - x_step if threshold else a.width + ) + gap_w = ind_width - b.width + w_step = ind_width - (gap_w * break_step) + w_step = ( + w_step + if w_step + x_step <= a.x + a.width + else ind_width + ) + self.update_indicator(x_step, w_step) + + +class MDTabs(ThemableBehavior, SpecificBackgroundColorBehavior, AnchorLayout): + """ + You can use this class to create your own tabbed panel.. + + :Events: + `on_tab_switch` + Called when switching tabs. + `on_slide_progress` + Called while the slide is scrolling. + `on_ref_press` + The method will be called when the ``on_ref_press`` event + occurs when you, for example, use markup text for tabs. + """ + + default_tab = NumericProperty(0) + """ + Index of the default tab. + + :attr:`default_tab` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + tab_bar_height = NumericProperty("48dp") + """ + Height of the tab bar. + + :attr:`tab_bar_height` is an :class:`~kivy.properties.NumericProperty` + and defaults to `'48dp'`. + """ + + tab_indicator_anim = BooleanProperty(False) + """ + Tab indicator animation. If you want use animation set it to ``True``. + + :attr:`tab_indicator_anim` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + tab_indicator_height = NumericProperty("2dp") + """ + Height of the tab indicator. + + :attr:`tab_indicator_height` is an :class:`~kivy.properties.NumericProperty` + and defaults to `'2dp'`. + """ + + anim_duration = NumericProperty(0.2) + """ + Duration of the slide animation. + + :attr:`anim_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + anim_threshold = BoundedNumericProperty( + 0.8, min=0.0, max=1.0, errorhandler=lambda x: 0.0 if x < 0.0 else 1.0 + ) + """ + Animation threshold allow you to change the tab indicator animation effect. + + :attr:`anim_threshold` is an :class:`~kivy.properties.BoundedNumericProperty` + and defaults to `0.8`. + """ + + allow_stretch = BooleanProperty(True) + """ + If False - tabs will not stretch to full screen. + + :attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + background_color = ListProperty() + """ + Background color of tabs in ``rgba`` format. + + :attr:`background_color` is an :class:`~kivy.properties.ListProperty` + 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)`. + """ + + elevation = NumericProperty(0) + """ + Tab value elevation. + + .. seealso:: + + `Behaviors/Elevation `_ + + :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + color_indicator = ListProperty() + """ + Color indicator in ``rgba`` format. + + :attr:`color_indicator` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + lock_swiping = BooleanProperty(False) + """ + If True - disable switching tabs by swipe. + + :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + font_name = StringProperty("Roboto") + """ + Font name for tab text. + + :attr:`font_name` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Roboto'`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_tab_switch") + self.register_event_type("on_ref_press") + self.register_event_type("on_slide_progress") + Clock.schedule_once(self._carousel_bind, 1) + + def switch_tab(self, name_tab): + """Switching the tab by name.""" + + for instance_tab in self.tab_bar.parent.carousel.slides: + if instance_tab.text == name_tab: + self.tab_bar.parent.carousel.load_slide(instance_tab) + break + + def get_tab_list(self): + """Returns a list of tab objects.""" + + return self.tab_bar.layout.children + + def add_widget(self, widget, index=0, canvas=None): + # You can add only subclass of MDTabsBase. + if len(self.children) >= 2: + try: + widget.tab_label.group = str(self) + widget.tab_label.tab_bar = self.tab_bar + widget.tab_label.text_color_normal = self.text_color_normal + widget.tab_label.text_color_active = self.text_color_active + self.bind( + text_color_normal=widget.tab_label.setter( + "text_color_normal" + ) + ) + self.bind( + text_color_active=widget.tab_label.setter( + "text_color_active" + ) + ) + self.bind(font_name=widget.tab_label.setter("font_name")) + self.tab_bar.layout.add_widget(widget.tab_label) + self.carousel.add_widget(widget) + return + except AttributeError: + pass + return super().add_widget(widget) + + def remove_widget(self, widget): + # You can remove only subclass of MDTabsLabel. + if not issubclass(widget.__class__, MDTabsLabel): + raise MDTabsException( + "MDTabs can remove only subclass of MDTabsLabel" + ) + # The last tab is not deleted. + if len(self.tab_bar.layout.children) == 1: + return + self.tab_bar.layout.remove_widget(widget) + for tab in self.carousel.slides: + if tab.text == widget.text: + self.carousel.remove_widget(tab) + break + + def on_slide_progress(self, *args): + """Called while the slide is scrolling.""" + + def on_carousel_index(self, carousel, index): + """Called when the carousel index changes.""" + + # when the index of the carousel change, update + # tab indicator, select the current tab and reset threshold data. + if carousel.current_slide: + current_tab_label = carousel.current_slide.tab_label + if current_tab_label.state == "normal": + current_tab_label._do_press() + current_tab_label.dispatch("on_release") + self.tab_bar.update_indicator( + current_tab_label.x, current_tab_label.width + ) + + def on_ref_press(self, *args): + """The method will be called when the ``on_ref_press`` event + occurs when you, for example, use markup text for tabs.""" + + def on_tab_switch(self, *args): + """Called when switching tabs.""" + + def _carousel_bind(self, i): + self.carousel.bind(on_slide_progress=self._on_slide_progress) + + def _on_slide_progress(self, *args): + self.dispatch("on_slide_progress", args) diff --git a/kivymd/uix/taptargetview.py b/kivymd/uix/taptargetview.py new file mode 100644 index 0000000..18bfefa --- /dev/null +++ b/kivymd/uix/taptargetview.py @@ -0,0 +1,847 @@ +""" +Components/TapTargetView +======================== + +.. seealso:: + + `TapTargetView, GitHub `_ + + `TapTargetView, Material archive `_ + +.. rubric:: Provide value and improve engagement by introducing users to new + features and functionality at relevant moments. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-previous.gif + :align: center + +Usage +----- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.taptargetview import MDTapTargetView + + KV = ''' + Screen: + + MDFloatingActionButton: + id: button + icon: "plus" + pos: 10, 10 + on_release: app.tap_target_start() + ''' + + + class TapTargetViewDemo(MDApp): + def build(self): + screen = Builder.load_string(KV) + self.tap_target_view = MDTapTargetView( + widget=screen.ids.button, + title_text="This is an add button", + description_text="This is a description of the button", + widget_position="left_bottom", + ) + + return screen + + def tap_target_start(self): + if self.tap_target_view.state == "close": + self.tap_target_view.start() + else: + self.tap_target_view.stop() + + + TapTargetViewDemo().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-usage.gif + :align: center + +Widget position +--------------- + +Sets the position of the widget relative to the floating circle. + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + widget_position="right", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-right.png + :align: center + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + widget_position="left", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-left.png + :align: center + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + widget_position="top", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-top.png + :align: center + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + widget_position="bottom", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-bottom.png + :align: center + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + widget_position="left_top", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-left_top.png + :align: center + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + widget_position="right_top", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-right_top.png + :align: center + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + widget_position="left_bottom", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-left_bottom.png + :align: center + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + widget_position="right_bottom", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-right_bottom.png + :align: center + +If you use ``the widget_position = "center"`` parameter then you must +definitely specify the :attr:`~MDTapTargetView.title_position`. + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + widget_position="center", + title_position="left_top", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-position-center.png + :align: center + +Text options +------------ + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + title_text="Title text", + description_text="Description text", + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-text.png + :align: center + + +You can use the following options to control font size, color, and boldness: + +- :attr:`~MDTapTargetView.title_text_size` +- :attr:`~MDTapTargetView.title_text_color` +- :attr:`~MDTapTargetView.title_text_bold` +- :attr:`~MDTapTargetView.description_text_size` +- :attr:`~MDTapTargetView.description_text_color` +- :attr:`~MDTapTargetView.description_text_bold` + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + title_text="Title text", + title_text_size="36sp", + description_text="Description text", + description_text_color=[1, 0, 0, 1] + ) + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-text-option.png + :align: center + +But you can also use markup to set these values. + +.. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + title_text="[size=36]Title text[/size]", + description_text="[color=#ff0000ff]Description text[/color]", + ) + +Events control +-------------- + +.. code-block:: python + + self.tap_target_view.bind(on_open=self.on_open, on_close=self.on_close) + +.. code-block:: python + + def on_open(self, instance_tap_target_view): + '''Called at the time of the start of the widget opening animation.''' + + print("Open", instance_tap_target_view) + + def on_close(self, instance_tap_target_view): + '''Called at the time of the start of the widget closed animation.''' + + print("Close", instance_tap_target_view) + +.. Note:: See other parameters in the :class:`~MDTapTargetView` class. +""" + +from kivy.animation import Animation +from kivy.event import EventDispatcher +from kivy.graphics import Color, Ellipse, Rectangle +from kivy.logger import Logger +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.label import Label + +from kivymd.theming import ThemableBehavior + + +class MDTapTargetView(ThemableBehavior, EventDispatcher): + """Rough try to mimic the working of Android's TapTargetView. + + :Events: + :attr:`on_open` + Called at the time of the start of the widget opening animation. + :attr:`on_close` + Called at the time of the start of the widget closed animation. + """ + + widget = ObjectProperty() + """ + Widget to add ``TapTargetView`` upon. + + :attr:`widget` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + outer_radius = NumericProperty(dp(200)) + """ + Radius for outer circle. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-outer-radius.png + :align: center + + :attr:`outer_radius` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(200)`. + """ + + outer_circle_color = ListProperty() + """ + Color for the outer circle in ``rgb`` format. + + .. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + outer_circle_color=(1, 0, 0) + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-outer-circle-color.png + :align: center + + :attr:`outer_circle_color` is an :class:`~kivy.properties.ListProperty` + and defaults to ``theme_cls.primary_color``. + """ + + outer_circle_alpha = NumericProperty(0.96) + """ + Alpha value for outer circle. + + :attr:`outer_circle_alpha` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.96`. + """ + + target_radius = NumericProperty(dp(45)) + """ + Radius for target circle. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-target-radius.png + :align: center + + :attr:`target_radius` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(45)`. + """ + + target_circle_color = ListProperty([1, 1, 1]) + """ + Color for target circle in ``rgb`` format. + + .. code-block:: python + + self.tap_target_view = MDTapTargetView( + ... + target_circle_color=(1, 0, 0) + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tap-target-view-widget-target-circle-color.png + :align: center + + :attr:`target_circle_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[1, 1, 1]`. + """ + + title_text = StringProperty() + """ + Title to be shown on the view. + + :attr:`title_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + title_text_size = NumericProperty(dp(25)) + """ + Text size for title. + + :attr:`title_text_size` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(25)`. + """ + + title_text_color = ListProperty([1, 1, 1, 1]) + """ + Text color for title. + + :attr:`title_text_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[1, 1, 1, 1]`. + """ + + title_text_bold = BooleanProperty(True) + """ + Whether title should be bold. + + :attr:`title_text_bold` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + description_text = StringProperty() + """ + Description to be shown below the title (keep it short). + + :attr:`description_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + description_text_size = NumericProperty(dp(20)) + """ + Text size for description text. + + :attr:`description_text_size` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(20)`. + """ + + description_text_color = ListProperty([0.9, 0.9, 0.9, 1]) + """ + Text size for description text. + + :attr:`description_text_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0.9, 0.9, 0.9, 1]`. + """ + + description_text_bold = BooleanProperty(False) + """ + Whether description should be bold. + + :attr:`description_text_bold` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + draw_shadow = BooleanProperty(False) + """ + Whether to show shadow. + + :attr:`draw_shadow` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + cancelable = BooleanProperty(False) + """ + Whether clicking outside the outer circle dismisses the view. + + :attr:`cancelable` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + widget_position = OptionProperty( + "left", + options=[ + "left", + "right", + "top", + "bottom", + "left_top", + "right_top", + "left_bottom", + "right_bottom", + "center", + ], + ) + """ + Sets the position of the widget on the :attr:`~outer_circle`. Available options are + `'left`', `'right`', `'top`', `'bottom`', `'left_top`', `'right_top`', + `'left_bottom`', `'right_bottom`', `'center`'. + + :attr:`widget_position` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'left'`. + """ + + title_position = OptionProperty( + "auto", + options=[ + "auto", + "left", + "right", + "top", + "bottom", + "left_top", + "right_top", + "left_bottom", + "right_bottom", + ], + ) + """ + Sets the position of :attr`~title_text` on the outer circle. Only works if + :attr`~widget_position` is set to `'center'`. In all other cases, it + calculates the :attr`~title_position` itself. + Must be set to other than `'auto`' when :attr`~widget_position` is set + to `'center`'. + + Available options are `'auto'`, `'left`', `'right`', `'top`', `'bottom`', + `'left_top`', `'right_top`', `'left_bottom`', `'right_bottom`', `'center`'. + + :attr:`title_position` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'auto'`. + """ + + stop_on_outer_touch = BooleanProperty(False) + """ + Whether clicking on outer circle stops the animation. + + :attr:`stop_on_outer_touch` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + stop_on_target_touch = BooleanProperty(True) + """ + Whether clicking on target circle should stop the animation. + + :attr:`stop_on_target_touch` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + state = OptionProperty("close", options=["close", "open"]) + """ + State of :class:`~MDTapTargetView`. + + :attr:`state` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'close'`. + """ + + _outer_radius = NumericProperty(0) + _target_radius = NumericProperty(0) + + def __init__(self, **kwargs): + self.ripple_max_dist = dp(90) + self.on_outer_radius(self, self.outer_radius) + self.on_target_radius(self, self.target_radius) + + self.core_title_text = Label( + markup=True, size_hint=(None, None), bold=self.title_text_bold + ) + self.core_title_text.bind( + texture_size=self.core_title_text.setter("size") + ) + self.core_description_text = Label(markup=True, size_hint=(None, None)) + self.core_description_text.bind( + texture_size=self.core_description_text.setter("size") + ) + + super().__init__(**kwargs) + self.register_event_type("on_outer_touch") + self.register_event_type("on_target_touch") + self.register_event_type("on_outside_click") + self.register_event_type("on_open") + self.register_event_type("on_close") + + if not self.outer_circle_color: + self.outer_circle_color = self.theme_cls.primary_color[:-1] + + def _initialize(self): + setattr(self.widget, "_outer_radius", 0) + setattr(self.widget, "_target_radius", 0) + setattr(self.widget, "target_ripple_radius", 0) + setattr(self.widget, "target_ripple_alpha", 0) + + # Bind some function on widget event when this function is called + # instead of when the class itself is initialized to prevent all + # widgets of all instances to get bind at once and start messing up. + self.widget.bind(on_touch_down=self._some_func) + + def _draw_canvas(self): + _pos = self._ttv_pos() + self.widget.canvas.before.clear() + + with self.widget.canvas.before: + # Outer circle. + Color( + *self.outer_circle_color, + self.outer_circle_alpha, + group="ttv_group", + ) + _rad1 = self.widget._outer_radius + Ellipse(size=(_rad1, _rad1), pos=_pos[0], group="ttv_group") + + # Title text. + Color(*self.title_text_color, group="ttv_group") + Rectangle( + size=self.core_title_text.texture.size, + texture=self.core_title_text.texture, + pos=_pos[1], + group="ttv_group", + ) + + # Description text. + Color(*self.description_text_color, group="ttv_group") + Rectangle( + size=self.core_description_text.texture.size, + texture=self.core_description_text.texture, + pos=( + _pos[1][0], + _pos[1][1] - self.core_description_text.size[1] - 5, + ), + group="ttv_group", + ) + + # Target circle. + Color(*self.target_circle_color, group="ttv_group") + _rad2 = self.widget._target_radius + Ellipse( + size=(_rad2, _rad2), + pos=( + self.widget.x - (_rad2 / 2 - self.widget.size[0] / 2), + self.widget.y - (_rad2 / 2 - self.widget.size[0] / 2), + ), + group="ttv_group", + ) + + # Target ripple. + Color( + *self.target_circle_color, + self.widget.target_ripple_alpha, + group="ttv_group", + ) + _rad3 = self.widget.target_ripple_radius + Ellipse( + size=(_rad3, _rad3), + pos=( + self.widget.x - (_rad3 / 2 - self.widget.size[0] / 2), + self.widget.y - (_rad3 / 2 - self.widget.size[0] / 2), + ), + group="ttv_group", + ) + + def stop(self, *args): + """Starts widget close animation.""" + + # It needs a better implementation. + self.anim_ripple.unbind(on_complete=self._repeat_ripple) + self.core_title_text.opacity = 0 + self.core_description_text.opacity = 0 + anim = Animation( + d=0.15, + t="in_cubic", + **dict( + zip( + ["_outer_radius", "_target_radius", "target_ripple_radius"], + [0, 0, 0], + ) + ), + ) + anim.bind(on_complete=self._after_stop) + anim.start(self.widget) + + def _after_stop(self, *args): + self.widget.canvas.before.remove_group("ttv_group") + args[0].stop_all(self.widget) + elev = getattr(self.widget, "elevation", None) + + if elev: + self._fix_elev() + self.dispatch("on_close") + + # Don't forget to unbind the function or it'll mess + # up with other next bindings. + self.widget.unbind(on_touch_down=self._some_func) + self.state = "close" + + def _fix_elev(self): + with self.widget.canvas.before: + Color(a=self.widget._soft_shadow_a) + Rectangle( + texture=self.widget._soft_shadow_texture, + size=self.widget._soft_shadow_size, + pos=self.widget._soft_shadow_pos, + ) + Color(a=self.widget._hard_shadow_a) + Rectangle( + texture=self.widget._hard_shadow_texture, + size=self.widget._hard_shadow_size, + pos=self.widget._hard_shadow_pos, + ) + Color(a=1) + + def start(self, *args): + """Starts widget opening animation.""" + + self._initialize() + self._animate_outer() + self.state = "open" + self.core_title_text.opacity = 1 + self.core_description_text.opacity = 1 + self.dispatch("on_open") + + def _animate_outer(self): + anim = Animation( + d=0.2, + t="out_cubic", + **dict( + zip( + ["_outer_radius", "_target_radius"], + [self._outer_radius, self._target_radius], + ) + ), + ) + anim.cancel_all(self.widget) + anim.bind(on_progress=lambda x, y, z: self._draw_canvas()) + anim.bind(on_complete=self._animate_ripple) + anim.start(self.widget) + setattr(self.widget, "target_ripple_radius", self._target_radius) + setattr(self.widget, "target_ripple_alpha", 1) + + def _animate_ripple(self, *args): + self.anim_ripple = Animation( + d=1, + t="in_cubic", + target_ripple_radius=self._target_radius + self.ripple_max_dist, + target_ripple_alpha=0, + ) + self.anim_ripple.stop_all(self.widget) + self.anim_ripple.bind(on_progress=lambda x, y, z: self._draw_canvas()) + self.anim_ripple.bind(on_complete=self._repeat_ripple) + self.anim_ripple.start(self.widget) + + def _repeat_ripple(self, *args): + setattr(self.widget, "target_ripple_radius", self._target_radius) + setattr(self.widget, "target_ripple_alpha", 1) + self._animate_ripple() + + def on_open(self, *args): + """Called at the time of the start of the widget opening animation.""" + + def on_close(self, *args): + """Called at the time of the start of the widget closed animation.""" + + def on_draw_shadow(self, instance, value): + Logger.warning( + "The shadow adding method will be implemented in future versions" + ) + + def on_description_text(self, instance, value): + self.core_description_text.text = value + + def on_description_text_size(self, instance, value): + self.core_description_text.font_size = value + + def on_description_text_bold(self, instance, value): + self.core_description_text.bold = value + + def on_title_text(self, instance, value): + self.core_title_text.text = value + + def on_title_text_size(self, instance, value): + self.core_title_text.font_size = value + + def on_title_text_bold(self, instance, value): + self.core_title_text.bold = value + + def on_outer_radius(self, instance, value): + self._outer_radius = self.outer_radius * 2 + + def on_target_radius(self, instance, value): + self._target_radius = self.target_radius * 2 + + def on_target_touch(self): + if self.stop_on_target_touch: + self.stop() + + def on_outer_touch(self): + if self.stop_on_outer_touch: + self.stop() + + def on_outside_click(self): + if self.cancelable: + self.stop() + + def _some_func(self, wid, touch): + """ + This function decides which one to dispatch based on the touch + position. + """ + + if self._check_pos_target(touch.pos): + self.dispatch("on_target_touch") + elif self._check_pos_outer(touch.pos): + self.dispatch("on_outer_touch") + else: + self.dispatch("on_outside_click") + + def _check_pos_outer(self, pos): + """ + Checks if a given `pos` coordinate is within the :attr:`~outer_radius`. + """ + + cx = self.circ_pos[0] + self._outer_radius / 2 + cy = self.circ_pos[1] + self._outer_radius / 2 + r = self._outer_radius / 2 + h, k = pos + + lhs = (cx - h) ** 2 + (cy - k) ** 2 + rhs = r ** 2 + if lhs <= rhs: + return True + return False + + def _check_pos_target(self, pos): + """ + Checks if a given `pos` coordinate is within the + :attr:`~target_radius`. + """ + + cx = self.widget.pos[0] + self.widget.width / 2 + cy = self.widget.pos[1] + self.widget.height / 2 + r = self._target_radius / 2 + h, k = pos + + lhs = (cx - h) ** 2 + (cy - k) ** 2 + rhs = r ** 2 + if lhs <= rhs: + return True + return False + + def _ttv_pos(self): + """ + Calculates the `pos` value for outer circle and text + based on the position provided. + + :returns: A tuple containing pos for the circle and text. + """ + + _rad1 = self.widget._outer_radius + _center_x = self.widget.x - (_rad1 / 2 - self.widget.size[0] / 2) + _center_y = self.widget.y - (_rad1 / 2 - self.widget.size[0] / 2) + + if self.widget_position == "left": + circ_pos = (_center_x + _rad1 / 3, _center_y) + title_pos = (_center_x + _rad1 / 1.4, _center_y + _rad1 / 1.4) + elif self.widget_position == "right": + circ_pos = (_center_x - _rad1 / 3, _center_y) + title_pos = (_center_x - _rad1 / 10, _center_y + _rad1 / 1.4) + elif self.widget_position == "top": + circ_pos = (_center_x, _center_y - _rad1 / 3) + title_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 4) + elif self.widget_position == "bottom": + circ_pos = (_center_x, _center_y + _rad1 / 3) + title_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 1.2) + # Corner ones need to be at a little smaller distance + # than edge ones that's why _rad1/4. + elif self.widget_position == "left_top": + circ_pos = (_center_x + _rad1 / 4, _center_y - _rad1 / 4) + title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 4) + elif self.widget_position == "right_top": + circ_pos = (_center_x - _rad1 / 4, _center_y - _rad1 / 4) + title_pos = (_center_x - _rad1 / 10, _center_y + _rad1 / 4) + elif self.widget_position == "left_bottom": + circ_pos = (_center_x + _rad1 / 4, _center_y + _rad1 / 4) + title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 1.2) + elif self.widget_position == "right_bottom": + circ_pos = (_center_x - _rad1 / 4, _center_y + _rad1 / 4) + title_pos = (_center_x, _center_y + _rad1 / 1.2) + else: + # Center. + circ_pos = (_center_x, _center_y) + + if self.title_position == "auto": + raise ValueError( + "widget_position='center' requires title_position to be set." + ) + elif self.title_position == "left": + title_pos = (_center_x + _rad1 / 10, _center_y + _rad1 / 2) + elif self.title_position == "right": + title_pos = (_center_x + _rad1 / 1.6, _center_y + _rad1 / 2) + elif self.title_position == "top": + title_pos = (_center_x + _rad1 / 2.5, _center_y + _rad1 / 1.3) + elif self.title_position == "bottom": + title_pos = (_center_x + _rad1 / 2.5, _center_y + _rad1 / 4) + elif self.title_position == "left_top": + title_pos = (_center_x + _rad1 / 8, _center_y + _rad1 / 1.4) + elif self.title_position == "right_top": + title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 1.3) + elif self.title_position == "left_bottom": + title_pos = (_center_x + _rad1 / 8, _center_y + _rad1 / 4) + elif self.title_position == "right_bottom": + title_pos = (_center_x + _rad1 / 2, _center_y + _rad1 / 3.5) + else: + raise ValueError( + f"'{self.title_position}'" + f"is not a valid value for title_position" + ) + + self.circ_pos = circ_pos + return circ_pos, title_pos diff --git a/kivymd/uix/textfield.py b/kivymd/uix/textfield.py new file mode 100755 index 0000000..b9a6397 --- /dev/null +++ b/kivymd/uix/textfield.py @@ -0,0 +1,1265 @@ +""" +Components/Text Field +===================== + +.. seealso:: + + `Material Design spec, Text fields `_ + +.. rubric:: Text fields let users enter and edit text. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields.png + :align: center + +`KivyMD` provides the following field classes for use: + +- MDTextField_ +- MDTextFieldRound_ +- MDTextFieldRect_ + +.. Note:: :class:`~MDTextField` inherited from + :class:`~kivy.uix.textinput.TextInput`. Therefore, most parameters and all + events of the :class:`~kivy.uix.textinput.TextInput` class are also + available in the :class:`~MDTextField` class. + +.. MDTextField: +MDTextField +----------- + + +:class:`~MDTextField` can be with helper text and without. + +Without helper text mode +------------------------ + +.. code-block:: kv + + MDTextField: + hint_text: "No helper text" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-no-helper-mode.gif + :align: center + +Helper text mode on ``on_focus`` event +-------------------------------------- + +.. code-block:: kv + + MDTextField: + hint_text: "Helper text on focus" + helper_text: "This will disappear when you click off" + helper_text_mode: "on_focus" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-mode-on-focus.gif + :align: center + +Persistent helper text mode +--------------------------- + +.. code-block:: kv + + MDTextField: + hint_text: "Persistent helper text" + helper_text: "Text is always here" + helper_text_mode: "persistent" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-mode-persistent.gif + :align: center + +Helper text mode `'on_error'` +---------------------------- + +To display an error in a text field when using the +``helper_text_mode: "on_error"`` parameter, set the `"error"` text field +parameter to `True`: + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + BoxLayout: + padding: "10dp" + + MDTextField: + id: text_field_error + hint_text: "Helper text on error (press 'Enter')" + helper_text: "There will always be a mistake" + helper_text_mode: "on_error" + pos_hint: {"center_y": .5} + ''' + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + + def build(self): + self.screen.ids.text_field_error.bind( + on_text_validate=self.set_error_message, + on_focus=self.set_error_message, + ) + return self.screen + + def set_error_message(self, instance_textfield): + self.screen.ids.text_field_error.error = True + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-mode-on-error.gif + :align: center + +Helper text mode `'on_error'` (with required) +-------------------------------------------- + +.. code-block:: kv + + MDTextField: + hint_text: "required = True" + required: True + helper_text_mode: "on_error" + helper_text: "Enter text" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-required.gif + :align: center + +Text length control +------------------- + +.. code-block:: kv + + MDTextField: + hint_text: "Max text length = 5" + max_text_length: 5 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-length.gif + :align: center + + +Multi line text +--------------- + +.. code-block:: kv + + MDTextField: + multiline: True + hint_text: "Multi-line text" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-multi-line.gif + :align: center + +Color mode +---------- + +.. code-block:: kv + + MDTextField: + hint_text: "color_mode = 'accent'" + color_mode: 'accent' + +Available options are `'primary'`, `'accent'` or `'custom`'. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-color-mode.gif + :align: center + +.. code-block:: kv + + MDTextField: + hint_text: "color_mode = 'custom'" + color_mode: 'custom' + helper_text_mode: "on_focus" + helper_text: "Color is defined by 'line_color_focus' property" + line_color_focus: 1, 0, 1, 1 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-color-mode-custom.gif + :align: center + +.. code-block:: kv + + MDTextField: + hint_text: "Line color normal" + line_color_normal: app.theme_cls.accent_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-line-color-normal.png + :align: center + +Rectangle mode +-------------- + +.. code-block:: kv + + MDTextField: + hint_text: "Rectangle mode" + mode: "rectangle" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-rectangle-mode.gif + :align: center + +Fill mode +--------- + +.. code-block:: kv + + MDTextField: + hint_text: "Fill mode" + mode: "fill" + fill_color: 0, 0, 0, .4 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-mode.gif + :align: center + +.. MDTextFieldRect: +MDTextFieldRect +--------------- + +.. Note:: :class:`~MDTextFieldRect` inherited from + :class:`~kivy.uix.textinput.TextInput`. You can use all parameters and + attributes of the :class:`~kivy.uix.textinput.TextInput` class in the + :class:`~MDTextFieldRect` class. + +.. code-block:: kv + + MDTextFieldRect: + size_hint: 1, None + height: "30dp" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-rect.gif + :align: center + +.. Warning:: While there is no way to change the color of the border. + +.. MDTextFieldRound: +MDTextFieldRound +---------------- + +Without icon +------------ + +.. code-block:: kv + + MDTextFieldRound: + hint_text: 'Empty field' + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round.gif + :align: center + +With left icon +-------------- + +.. Warning:: The icons in the :class:`~MDTextFieldRound` are static. You cannot + bind events to them. + +.. code-block:: kv + + MDTextFieldRound: + icon_left: "email" + hint_text: "Field with left icon" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-left-icon.png + :align: center + +With left and right icons +------------------------- + +.. code-block:: kv + + MDTextFieldRound: + icon_left: 'key-variant' + icon_right: 'eye-off' + hint_text: 'Field with left and right icons' + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-left-right-icon.png + :align: center + +Control background color +------------------------ + +.. code-block:: kv + + MDTextFieldRound: + icon_left: 'key-variant' + normal_color: app.theme_cls.accent_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-normal-color.gif + :align: center + +.. code-block:: kv + + MDTextFieldRound: + icon_left: 'key-variant' + normal_color: app.theme_cls.accent_color + color_active: 1, 0, 0, 1 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-active-color.gif + :align: center + +Clickable icon for MDTextFieldRound +----------------------------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import StringProperty + + from kivymd.app import MDApp + from kivymd.uix.relativelayout import MDRelativeLayout + + KV = ''' + : + size_hint_y: None + height: text_field.height + + MDTextFieldRound: + id: text_field + hint_text: root.hint_text + text: root.text + password: True + color_active: app.theme_cls.primary_light + icon_left: "key-variant" + padding: + self._lbl_icon_left.texture_size[1] + dp(10) if self.icon_left else dp(15), \ + (self.height / 2) - (self.line_height / 2), \ + self._lbl_icon_right.texture_size[1] + dp(20), \ + 0 + + MDIconButton: + icon: "eye-off" + ripple_scale: .5 + pos_hint: {"center_y": .5} + pos: text_field.width - self.width + dp(8), 0 + on_release: + self.icon = "eye" if self.icon == "eye-off" else "eye-off" + text_field.password = False if text_field.password is True else True + + + MDScreen: + + ClickableTextFieldRound: + size_hint_x: None + width: "300dp" + hint_text: "Password" + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class ClickableTextFieldRound(MDRelativeLayout): + text = StringProperty() + hint_text = StringProperty() + # Here specify the required parameters for MDTextFieldRound: + # [...] + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +With right icon +--------------- + +.. Note:: The icon on the right is available for use in all text fields. + +.. code-block:: kv + + MDTextField: + hint_text: "Name" + mode: "fill" + fill_color: 0, 0, 0, .4 + icon_right: "arrow-down-drop-circle-outline" + icon_right_color: app.theme_cls.primary_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-mode-icon.png + :align: center + +.. code-block:: kv + + MDTextField: + hint_text: "Name" + icon_right: "arrow-down-drop-circle-outline" + icon_right_color: app.theme_cls.primary_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-right-icon.png + :align: center + +.. code-block:: kv + + MDTextField: + hint_text: "Name" + mode: "rectangle" + icon_right: "arrow-down-drop-circle-outline" + icon_right_color: app.theme_cls.primary_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-rectangle-right-icon.png + :align: center + +.. seealso:: + + See more information in the :class:`~MDTextFieldRect` class. +""" + +__all__ = ("MDTextField", "MDTextFieldRect", "MDTextFieldRound") + +import sys + +from kivy.animation import Animation +from kivy.lang import Builder +from kivy.metrics import dp, sp +from kivy.properties import ( + BooleanProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.label import Label +from kivy.uix.textinput import TextInput + +from kivymd.font_definitions import theme_font_styles +from kivymd.material_resources import DEVICE_TYPE +from kivymd.theming import ThemableBehavior +from kivymd.uix.label import MDIcon + +Builder.load_string( + """ +#:import images_path kivymd.images_path + + + + + canvas.before: + Clear + + # Disabled line. + Color: + rgba: self.line_color_normal if root.mode == "line" else (0, 0, 0, 0) + Line: + points: self.x, self.y + dp(16), self.x + self.width, self.y + dp(16) + width: 1 + dash_length: dp(3) + dash_offset: 2 if self.disabled else 0 + + # Active line. + Color: + rgba: self._current_line_color if root.mode in ("line", "fill") and root.active_line else (0, 0, 0, 0) + Rectangle: + size: self._line_width, dp(2) + pos: self.center_x - (self._line_width / 2), self.y + (dp(16) if root.mode != "fill" else 0) + + # Helper text. + Color: + rgba: self._current_error_color + Rectangle: + texture: self._msg_lbl.texture + size: + self._msg_lbl.texture_size[0] - (dp(3) if root.mode in ("fill", "rectangle") else 0), \ + self._msg_lbl.texture_size[1] - (dp(3) if root.mode in ("fill", "rectangle") else 0) + pos: self.x + (dp(8) if root.mode == "fill" else 0), self.y + (dp(3) if root.mode in ("fill", "rectangle") else 0) + + # Texture of right Icon. + Color: + rgba: self.icon_right_color + Rectangle: + texture: self._lbl_icon_right.texture + size: self._lbl_icon_right.texture_size if self.icon_right else (0, 0) + pos: + (self.width + self.x) - (self._lbl_icon_right.texture_size[1]) - dp(8), \ + self.center[1] - self._lbl_icon_right.texture_size[1] / 2 + (dp(8) if root.mode != "fill" else 0) \ + if root.mode != "rectangle" else \ + self.center[1] - self._lbl_icon_right.texture_size[1] / 2 - dp(4) + + Color: + rgba: self._current_right_lbl_color + Rectangle: + texture: self._right_msg_lbl.texture + size: self._right_msg_lbl.texture_size + pos: self.x + self.width - self._right_msg_lbl.texture_size[0] - dp(16), self.y + + Color: + rgba: + (self._current_line_color if self.focus and not \ + self._cursor_blink else (0, 0, 0, 0)) + Rectangle: + pos: (int(x) for x in self.cursor_pos) + size: 1, -self.line_height + + # Hint text. + Color: + rgba: self._current_hint_text_color if not self.current_hint_text_color else self.current_hint_text_color + Rectangle: + texture: self._hint_lbl.texture + size: self._hint_lbl.texture_size + pos: self.x + (dp(8) if root.mode == "fill" else 0), self.y + self.height - self._hint_y + + Color: + rgba: + self.disabled_foreground_color if self.disabled else\ + (self.hint_text_color if not self.text and not\ + self.focus else self.foreground_color) + + # "rectangle" mode + Color: + rgba: self._current_line_color + Line: + width: dp(1) if root.mode == "rectangle" else dp(0.00001) + points: + ( + self.x + root._line_blank_space_right_point, self.top - self._hint_lbl.texture_size[1] // 2, + self.right + dp(12), self.top - self._hint_lbl.texture_size[1] // 2, + self.right + dp(12), self.y, + self.x - dp(12), self.y, + self.x - dp(12), self.top - self._hint_lbl.texture_size[1] // 2, + self.x + root._line_blank_space_left_point, self.top - self._hint_lbl.texture_size[1] // 2 + ) + + # "fill" mode. + canvas.after: + Color: + rgba: root.fill_color if root.mode == "fill" else (0, 0, 0, 0) + RoundedRectangle: + pos: self.x, self.y + size: self.width, self.height + dp(8) + radius: (10, 10, 0, 0, 0) + + font_name: "Roboto" if not root.font_name else root.font_name + foreground_color: app.theme_cls.text_color + font_size: "16sp" + bold: False + padding: + 0 if root.mode != "fill" else "8dp", \ + "16dp" if root.mode != "fill" else "24dp", \ + 0 if root.mode != "fill" and not root.icon_right else ("14dp" if not root.icon_right else self._lbl_icon_right.texture_size[1] + dp(20)), \ + "16dp" if root.mode == "fill" else "10dp" + multiline: False + size_hint_y: None + height: self.minimum_height + (dp(8) if root.mode != "fill" else 0) + + + + size_hint_x: None + width: self.texture_size[0] + shorten: True + shorten_from: "right" + + + + on_focus: + root.anim_rect((root.x, root.y, root.right, root.y, root.right, \ + root.top, root.x, root.top, root.x, root.y), 1) if root.focus \ + else root.anim_rect((root.x - dp(60), root.y - dp(60), \ + root.right + dp(60), root.y - dp(60), + root.right + dp(60), root.top + dp(60), \ + root.x - dp(60), root.top + dp(60), \ + root.x - dp(60), root.y - dp(60)), 0) + + canvas.after: + Color: + rgba: root._primary_color + Line: + width: dp(1.5) + points: + ( + self.x - dp(60), self.y - dp(60), + self.right + dp(60), self.y - dp(60), + self.right + dp(60), self.top + dp(60), + self.x - dp(60), self.top + dp(60), + self.x - dp(60), self.y - dp(60) + ) + + +: + multiline: False + size_hint: 1, None + height: self.line_height + dp(10) + background_active: f'{images_path}transparent.png' + background_normal: f'{images_path}transparent.png' + padding: + self._lbl_icon_left.texture_size[1] + dp(10) if self.icon_left else dp(15), \ + (self.height / 2) - (self.line_height / 2), \ + self._lbl_icon_right.texture_size[1] + dp(20) if self.icon_right else dp(15), \ + 0 + + canvas.before: + Color: + rgba: self.normal_color if not self.focus else self._color_active + Ellipse: + angle_start: 180 + angle_end: 360 + pos: self.pos[0] - self.size[1] / 2, self.pos[1] + size: self.size[1], self.size[1] + Ellipse: + angle_start: 360 + angle_end: 540 + pos: self.size[0] + self.pos[0] - self.size[1]/2.0, self.pos[1] + size: self.size[1], self.size[1] + Rectangle: + pos: self.pos + size: self.size + + Color: + rgba: self.line_color + Line: + points: self.pos[0] , self.pos[1], self.pos[0] + self.size[0], self.pos[1] + Line: + points: self.pos[0], self.pos[1] + self.size[1], self.pos[0] + self.size[0], self.pos[1] + self.size[1] + Line: + ellipse: self.pos[0] - self.size[1] / 2, self.pos[1], self.size[1], self.size[1], 180, 360 + Line: + ellipse: self.size[0] + self.pos[0] - self.size[1] / 2.0, self.pos[1], self.size[1], self.size[1], 360, 540 + + # Texture of left Icon. + Color: + rgba: self.icon_left_color + Rectangle: + texture: self._lbl_icon_left.texture + size: + self._lbl_icon_left.texture_size if self.icon_left \ + else (0, 0) + pos: + self.x, \ + self.center[1] - self._lbl_icon_right.texture_size[1] / 2 + + # Texture of right Icon. + Color: + rgba: self.icon_right_color + Rectangle: + texture: self._lbl_icon_right.texture + size: + self._lbl_icon_right.texture_size if self.icon_right \ + else (0, 0) + pos: + (self.width + self.x) - (self._lbl_icon_right.texture_size[1]), \ + self.center[1] - self._lbl_icon_right.texture_size[1] / 2 + + Color: + rgba: + root.theme_cls.disabled_hint_text_color if not self.focus \ + else root.foreground_color +""" +) + + +class MDTextFieldRect(ThemableBehavior, TextInput): + _primary_color = ListProperty((0, 0, 0, 0)) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._update_primary_color() + self.theme_cls.bind(primary_color=self._update_primary_color) + + def anim_rect(self, points, alpha): + instance_line = self.canvas.children[-1].children[-1] + instance_color = self.canvas.children[-1].children[0] + if alpha == 1: + d_line = 0.3 + d_color = 0.4 + else: + d_line = 0.05 + d_color = 0.05 + + Animation(points=points, d=d_line, t="out_cubic").start(instance_line) + Animation(a=alpha, d=d_color).start(instance_color) + + def _update_primary_color(self, *args): + self._primary_color = self.theme_cls.primary_color + self._primary_color[3] = 0 + + +class TextfieldLabel(ThemableBehavior, Label): + font_style = OptionProperty("Body1", options=theme_font_styles) + # + field = ObjectProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.font_size = sp(self.theme_cls.font_styles[self.font_style][1]) + + +class MDTextField(ThemableBehavior, TextInput): + helper_text = StringProperty("This field is required") + """ + Text for ``helper_text`` mode. + + :attr:`helper_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `'This field is required'`. + """ + + helper_text_mode = OptionProperty( + "none", options=["none", "on_error", "persistent", "on_focus"] + ) + """ + Helper text mode. Available options are: `'on_error'`, `'persistent'`, + `'on_focus'`. + + :attr:`helper_text_mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'none'`. + """ + + max_text_length = NumericProperty(None) + """ + Maximum allowed value of characters in a text field. + + :attr:`max_text_length` is an :class:`~kivy.properties.NumericProperty` + and defaults to `None`. + """ + + required = BooleanProperty(False) + """ + Required text. If True then the text field requires text. + + :attr:`required` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + color_mode = OptionProperty( + "primary", options=["primary", "accent", "custom"] + ) + """ + Color text mode. Available options are: `'primary'`, `'accent'`, + `'custom'`. + + :attr:`color_mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'primary'`. + """ + + mode = OptionProperty("line", options=["rectangle", "fill"]) + """ + Text field mode. Available options are: `'line'`, `'rectangle'`, `'fill'`. + + :attr:`mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'line'`. + """ + + line_color_normal = ListProperty() + """ + Line color normal in ``rgba`` format. + + :attr:`line_color_normal` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + line_color_focus = ListProperty() + """ + Line color focus in ``rgba`` format. + + :attr:`line_color_focus` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + error_color = ListProperty() + """ + Error color in ``rgba`` format for ``required = True``. + + :attr:`error_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + fill_color = ListProperty((0, 0, 0, 0)) + """ + The background color of the fill in rgba format when the ``mode`` parameter + is "fill". + + :attr:`fill_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `(0, 0, 0, 0)`. + """ + + active_line = BooleanProperty(True) + """ + Show active line or not. + + :attr:`active_line` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + error = BooleanProperty(False) + """ + If True, then the text field goes into ``error`` mode. + + :attr:`error` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + current_hint_text_color = ListProperty() + """ + ``hint_text`` text color. + + :attr:`current_hint_text_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + icon_right = StringProperty() + """Right icon. + + :attr:`icon_right` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon_right_color = ListProperty((0, 0, 0, 1)) + """Color of right icon in ``rgba`` format. + + :attr:`icon_right_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `(0, 0, 0, 1)`. + """ + + _text_len_error = BooleanProperty(False) + _hint_lbl_font_size = NumericProperty("16sp") + _line_blank_space_right_point = NumericProperty(0) + _line_blank_space_left_point = NumericProperty(0) + _hint_y = NumericProperty("38dp") + _line_width = NumericProperty(0) + _current_line_color = ListProperty((0, 0, 0, 0)) + _current_error_color = ListProperty((0, 0, 0, 0)) + _current_hint_text_color = ListProperty((0, 0, 0, 0)) + _current_right_lbl_color = ListProperty((0, 0, 0, 0)) + + _msg_lbl = None + _right_msg_lbl = None + _hint_lbl = None + _lbl_icon_right = None + + def __init__(self, **kwargs): + self.set_objects_labels() + super().__init__(**kwargs) + # Sets default colors. + self.line_color_normal = self.theme_cls.divider_color + self.line_color_focus = self.theme_cls.primary_color + self.error_color = self.theme_cls.error_color + self._current_hint_text_color = self.theme_cls.disabled_hint_text_color + self._current_line_color = self.theme_cls.primary_color + + self.bind( + helper_text=self._set_msg, + hint_text=self._set_hint, + _hint_lbl_font_size=self._hint_lbl.setter("font_size"), + helper_text_mode=self._set_message_mode, + max_text_length=self._set_max_text_length, + text=self.on_text, + ) + self.theme_cls.bind( + primary_color=self._update_primary_color, + theme_style=self._update_theme_style, + accent_color=self._update_accent_color, + ) + self.has_had_text = False + + def set_objects_labels(self): + """Creates labels objects for the parameters + `helper_text`,`hint_text`, etc.""" + + # Label object for `helper_text` parameter. + self._msg_lbl = TextfieldLabel( + font_style="Caption", + halign="left", + valign="middle", + text=self.helper_text, + field=self, + ) + # Label object for `max_text_length` parameter. + self._right_msg_lbl = TextfieldLabel( + font_style="Caption", + halign="right", + valign="middle", + text="", + field=self, + ) + # Label object for `hint_text` parameter. + self._hint_lbl = TextfieldLabel( + font_style="Subtitle1", halign="left", valign="middle", field=self + ) + # MDIcon object for the icon on the right. + self._lbl_icon_right = MDIcon(theme_text_color="Custom") + + def on_icon_right(self, instance, value): + self._lbl_icon_right.icon = value + + def on_icon_right_color(self, instance, value): + self._lbl_icon_right.text_color = value + + def on_width(self, instance, width): + """Called when the application window is resized.""" + + if ( + any((self.focus, self.error, self._text_len_error)) + and instance is not None + ): + # Bottom line width when active focus. + self._line_width = width + self._msg_lbl.width = self.width + self._right_msg_lbl.width = self.width + + def on_focus(self, *args): + disabled_hint_text_color = self.theme_cls.disabled_hint_text_color + Animation.cancel_all( + self, "_line_width", "_hint_y", "_hint_lbl_font_size" + ) + self._set_text_len_error() + + if self.focus: + self._line_blank_space_right_point = ( + self._get_line_blank_space_right_point() + ) + _fill_color = self.fill_color + _fill_color[3] = self.fill_color[3] - 0.1 + if not self._get_has_error(): + Animation( + _line_blank_space_right_point=self._line_blank_space_right_point, + _line_blank_space_left_point=self._hint_lbl.x - dp(5), + _current_hint_text_color=self.line_color_focus, + fill_color=_fill_color, + duration=0.2, + t="out_quad", + ).start(self) + self.has_had_text = True + Animation.cancel_all( + self, "_line_width", "_hint_y", "_hint_lbl_font_size" + ) + if not self.text: + self._anim_lbl_font_size(dp(14), sp(12)) + Animation(_line_width=self.width, duration=0.2, t="out_quad").start( + self + ) + if self._get_has_error(): + self._anim_current_error_color(self.error_color) + if self.helper_text_mode == "on_error" and ( + self.error or self._text_len_error + ): + self._anim_current_error_color(self.error_color) + elif ( + self.helper_text_mode == "on_error" + and not self.error + and not self._text_len_error + ): + self._anim_current_error_color((0, 0, 0, 0)) + elif self.helper_text_mode in ("persistent", "on_focus"): + self._anim_current_error_color(disabled_hint_text_color) + else: + self._anim_current_right_lbl_color(disabled_hint_text_color) + Animation( + duration=0.2, _current_hint_text_color=self.line_color_focus + ).start(self) + if self.helper_text_mode == "on_error": + self._anim_current_error_color((0, 0, 0, 0)) + if self.helper_text_mode in ("persistent", "on_focus"): + self._anim_current_error_color(disabled_hint_text_color) + else: + _fill_color = self.fill_color + _fill_color[3] = self.fill_color[3] + 0.1 + Animation(fill_color=_fill_color, duration=0.2, t="out_quad").start( + self + ) + if not self.text: + self._anim_lbl_font_size(dp(38), sp(16)) + Animation( + _line_blank_space_right_point=0, + _line_blank_space_left_point=0, + duration=0.2, + t="out_quad", + ).start(self) + if self._get_has_error(): + self._anim_get_has_error_color(self.error_color) + if self.helper_text_mode == "on_error" and ( + self.error or self._text_len_error + ): + self._anim_current_error_color(self.error_color) + elif ( + self.helper_text_mode == "on_error" + and not self.error + and not self._text_len_error + ): + self._anim_current_error_color((0, 0, 0, 0)) + elif self.helper_text_mode == "persistent": + self._anim_current_error_color(disabled_hint_text_color) + elif self.helper_text_mode == "on_focus": + self._anim_current_error_color((0, 0, 0, 0)) + else: + Animation(duration=0.2, color=(1, 1, 1, 1)).start( + self._hint_lbl + ) + self._anim_get_has_error_color() + if self.helper_text_mode == "on_error": + self._anim_current_error_color((0, 0, 0, 0)) + elif self.helper_text_mode == "persistent": + self._anim_current_error_color(disabled_hint_text_color) + elif self.helper_text_mode == "on_focus": + self._anim_current_error_color((0, 0, 0, 0)) + Animation(_line_width=0, duration=0.2, t="out_quad").start(self) + + def on_text(self, instance, text): + if len(text) > 0: + self.has_had_text = True + if self.max_text_length is not None: + self._right_msg_lbl.text = f"{len(text)}/{self.max_text_length}" + self._set_text_len_error() + if self.error or self._text_len_error: + if self.focus: + self._anim_current_line_color(self.error_color) + if self.helper_text_mode == "on_error" and ( + self.error or self._text_len_error + ): + self._anim_current_error_color(self.error_color) + if self._text_len_error: + self._anim_current_right_lbl_color(self.error_color) + else: + if self.focus: + self._anim_current_right_lbl_color( + self.theme_cls.disabled_hint_text_color + ) + self._anim_current_line_color(self.line_color_focus) + if self.helper_text_mode == "on_error": + self._anim_current_error_color((0, 0, 0, 0)) + if len(self.text) != 0 and not self.focus: + self._hint_y = dp(14) + self._hint_lbl_font_size = sp(12) + + def on_text_validate(self): + self.has_had_text = True + self._set_text_len_error() + + def on_color_mode(self, instance, mode): + if mode == "primary": + self._update_primary_color() + elif mode == "accent": + self._update_accent_color() + elif mode == "custom": + self._update_colors(self.line_color_focus) + + def on_line_color_focus(self, *args): + if self.color_mode == "custom": + self._update_colors(self.line_color_focus) + + def on__hint_text(self, instance, value): + pass + + def _anim_get_has_error_color(self, color=None): + # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_get_has_error.png + if not color: + line_color = self.line_color_focus + hint_text_color = self.theme_cls.disabled_hint_text_color + right_lbl_color = (0, 0, 0, 0) + else: + line_color = color + hint_text_color = color + right_lbl_color = color + Animation( + duration=0.2, + _current_line_color=line_color, + _current_hint_text_color=hint_text_color, + _current_right_lbl_color=right_lbl_color, + ).start(self) + + def _anim_current_line_color(self, color): + # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_line_color.gif + Animation( + duration=0.2, + _current_hint_text_color=color, + _current_line_color=color, + ).start(self) + + def _anim_lbl_font_size(self, hint_y, font_size): + Animation( + _hint_y=hint_y, + _hint_lbl_font_size=font_size, + duration=0.2, + t="out_quad", + ).start(self) + + def _anim_current_right_lbl_color(self, color, duration=0.2): + # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_right_lbl_color.png + Animation(duration=duration, _current_right_lbl_color=color).start(self) + + def _anim_current_error_color(self, color, duration=0.2): + # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_error_color_to_disabled_color.gif + # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_anim_current_error_color_to_fade.gif + Animation(duration=duration, _current_error_color=color).start(self) + + def _update_colors(self, color): + self.line_color_focus = color + if not self.error and not self._text_len_error: + self._current_line_color = color + if self.focus: + self._current_line_color = color + + def _update_accent_color(self, *args): + if self.color_mode == "accent": + self._update_colors(self.theme_cls.accent_color) + + def _update_primary_color(self, *args): + if self.color_mode == "primary": + self._update_colors(self.theme_cls.primary_color) + + def _update_theme_style(self, *args): + self.line_color_normal = self.theme_cls.divider_color + if not any([self.error, self._text_len_error]): + if not self.focus: + self._current_hint_text_color = ( + self.theme_cls.disabled_hint_text_color + ) + self._current_right_lbl_color = ( + self.theme_cls.disabled_hint_text_color + ) + if self.helper_text_mode == "persistent": + self._current_error_color = ( + self.theme_cls.disabled_hint_text_color + ) + + def _get_has_error(self): + if self.error or all( + [ + self.max_text_length is not None + and len(self.text) > self.max_text_length + ] + ): + has_error = True + else: + if all((self.required, len(self.text) == 0, self.has_had_text)): + has_error = True + else: + has_error = False + return has_error + + def _get_line_blank_space_right_point(self): + # https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/_line_blank_space_right_point.png + return ( + self._hint_lbl.texture_size[0] + - self._hint_lbl.texture_size[0] / 100 * dp(18) + if DEVICE_TYPE == "desktop" + else dp(10) + ) + + def _get_max_text_length(self): + """Returns the maximum number of characters that can be entered in a + text field.""" + + return ( + sys.maxsize + if self.max_text_length is None + else self.max_text_length + ) + + def _set_text_len_error(self): + if len(self.text) > self._get_max_text_length() or all( + (self.required, len(self.text) == 0, self.has_had_text) + ): + self._text_len_error = True + else: + self._text_len_error = False + + def _set_hint(self, instance, text): + self._hint_lbl.text = text + + def _set_msg(self, instance, text): + self._msg_lbl.text = text + self.helper_text = text + + def _set_message_mode(self, instance, text): + self.helper_text_mode = text + if self.helper_text_mode == "persistent": + self._anim_current_error_color( + self.theme_cls.disabled_hint_text_color, 0.1 + ) + + def _set_max_text_length(self, instance, length): + self.max_text_length = length + self._right_msg_lbl.text = f"{len(self.text)}/{length}" + + def _refresh_hint_text(self): + pass + + +class MDTextFieldRound(ThemableBehavior, TextInput): + icon_left = StringProperty() + """Left icon. + + :attr:`icon_left` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon_left_color = ListProperty((0, 0, 0, 1)) + """Color of left icon in ``rgba`` format. + + :attr:`icon_left_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `(0, 0, 0, 1)`. + """ + + icon_right = StringProperty() + """Right icon. + + :attr:`icon_right` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon_right_color = ListProperty((0, 0, 0, 1)) + """Color of right icon. + + :attr:`icon_right_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `(0, 0, 0, 1)`. + """ + + line_color = ListProperty() + """Field line color. + + :attr:`line_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + normal_color = ListProperty() + """Field color if `focus` is `False`. + + :attr:`normal_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + color_active = ListProperty() + """Field color if `focus` is `True`. + + :attr:`color_active` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + _color_active = ListProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._lbl_icon_left = MDIcon(theme_text_color="Custom") + self._lbl_icon_right = MDIcon(theme_text_color="Custom") + self.cursor_color = self.theme_cls.primary_color + + if not self.normal_color: + self.normal_color = self.theme_cls.primary_light + if not self.line_color: + self.line_color = self.theme_cls.primary_dark + if not self.color_active: + self._color_active = (0.5, 0.5, 0.5, 0.5) + + def on_focus(self, instance, value): + if value: + self.icon_left_color = self.theme_cls.primary_color + self.icon_right_color = self.theme_cls.primary_color + else: + self.icon_left_color = self.theme_cls.text_color + self.icon_right_color = self.theme_cls.text_color + + def on_icon_left(self, instance, value): + self._lbl_icon_left.icon = value + + def on_icon_left_color(self, instance, value): + self._lbl_icon_left.text_color = value + + def on_icon_right(self, instance, value): + self._lbl_icon_right.icon = value + + def on_icon_right_color(self, instance, value): + self._lbl_icon_right.text_color = value + + def on_color_active(self, instance, value): + if value != [0, 0, 0, 0.5]: + self._color_active = value + self._color_active[-1] = 0.5 + else: + self._color_active = value diff --git a/kivymd/uix/toolbar.py b/kivymd/uix/toolbar.py new file mode 100755 index 0000000..72cd321 --- /dev/null +++ b/kivymd/uix/toolbar.py @@ -0,0 +1,612 @@ +""" +Components/Toolbar +================== + +.. seealso:: + + `Material Design spec, App bars: top `_ + + `Material Design spec, App bars: bottom `_ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-top.png + :align: center + +`KivyMD` provides the following toolbar positions for use: + +- Top_ +- Bottom_ + +.. Top: +Top +--- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + BoxLayout: + orientation: "vertical" + + MDToolbar: + title: "MDToolbar" + + MDLabel: + text: "Content" + halign: "center" + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-1.png + :align: center + +Add left menu +------------- + +.. code-block:: kv + + MDToolbar: + title: "MDToolbar" + left_action_items: [["menu", lambda x: app.callback()]] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-2.png + :align: center + +Add right menu +-------------- + +.. code-block:: kv + + MDToolbar: + title: "MDToolbar" + right_action_items: [["dots-vertical", lambda x: app.callback()]] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-3.png + :align: center + +Add two item to the right menu +------------------------------ + +.. code-block:: kv + + MDToolbar: + title: "MDToolbar" + right_action_items: [["dots-vertical", lambda x: app.callback_1()], ["clock", lambda x: app.callback_2()]] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-4.png + :align: center + +Change toolbar color +-------------------- + +.. code-block:: kv + + MDToolbar: + title: "MDToolbar" + md_bg_color: app.theme_cls.accent_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-5.png + :align: center + +Change toolbar text color +------------------------- + +.. code-block:: kv + + MDToolbar: + title: "MDToolbar" + specific_text_color: app.theme_cls.accent_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-6.png + :align: center + +Shadow elevation control +------------------------ + +.. code-block:: kv + + MDToolbar: + title: "Elevation 10" + elevation: 10 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-7.png + :align: center + +.. Bottom: +Bottom +------ + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-bottom.png + :align: center + +Usage +----- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + BoxLayout: + + # Will always be at the bottom of the screen. + MDBottomAppBar: + + MDToolbar: + title: "Title" + icon: "git" + type: "bottom" + left_action_items: [["menu", lambda x: x]] + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-8.png + :align: center + +Event on floating button +------------------------ + +Event ``on_action_button``: + +.. code-block:: kv + + MDBottomAppBar: + + MDToolbar: + title: "Title" + icon: "git" + type: "bottom" + left_action_items: [["menu", lambda x: x]] + on_action_button: app.callback(self.icon) + +Floating button position +------------------------ + +Mode: + +- `'free-end'` +- `'free-center'` +- `'end'` +- `'center'` + +.. code-block:: kv + + MDBottomAppBar: + + MDToolbar: + title: "Title" + icon: "git" + type: "bottom" + left_action_items: [["menu", lambda x: x]] + mode: "end" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-9.png + :align: center + +.. code-block:: kv + + MDBottomAppBar: + + MDToolbar: + title: "Title" + icon: "git" + type: "bottom" + left_action_items: [["menu", lambda x: x]] + mode: "free-end" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-10.png + :align: center + +.. seealso:: + + `Components-Bottom-App-Bar `_ +""" + +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 ( + ListProperty, + NumericProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout + +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + RectangularElevationBehavior, + SpecificBackgroundColorBehavior, +) +from kivymd.uix.button import MDFloatingActionButton, MDIconButton + +Builder.load_string( + """ +#:import m_res kivymd.material_resources + + +: + md_bg_color: self.theme_cls.primary_color + + canvas.before: + PushMatrix + Scale: + origin: self.center + x: root._scale_x + y: root._scale_y + canvas.after: + PopMatrix + + + + size_hint_y: None + height: root.theme_cls.standard_increment + padding: [root.theme_cls.horizontal_margins - dp(12), 0] + opposite_colors: True + elevation: root.elevation + md_bg_color: self.theme_cls.primary_color if root.type != "bottom" else [0, 0, 0, 0] + + canvas: + Color: + rgba: root.theme_cls.primary_color + RoundedRectangle: + pos: + self.pos \ + if root.mode == "center" else \ + (self.width - root.action_button.width + dp(6), self.y) + size: + (((self.width - root.action_button.width) / 2 - dp(6), self.height) \ + if root.mode == "center" else \ + (root.action_button.width - dp(6), self.height)) if root.type == "bottom" else (0, 0) + radius: + (0, root.round, 0, 0) if root.mode == "center" else (root.round, 0, 0, 0) + Rectangle: + pos: + ((self.width / 2 - root.action_button.width / 2) - dp(6), self.y - root._shift) \ + if root.mode == "center" else \ + (self.width - root.action_button.width * 2 - dp(6), self.y - root._shift) + size: + (root.action_button.width + dp(6) * 2, self.height - root._shift * 2) \ + if root.type == "bottom" else (0, 0) + RoundedRectangle: + pos: + ((self.width + root.action_button.width) / 2 + dp(6), self.y) \ + if root.mode == "center" else self.pos + size: + (((self.width - root.action_button.width) / 2 + dp(6), self.height) \ + if root.mode == "center" else \ + ((self.width - root.action_button.width * 2 - dp(6)), self.height)) \ + if root.type == "bottom" else (0, 0) + radius: (root.round, 0, 0, 0) if root.mode == "center" else (0, root.round, 0, 0) + Color: + rgba: 1, 1, 1, 1 + Ellipse: + pos: + (self.center[0] - root.action_button.width / 2 - dp(6), self.center[1] - root._shift * 2) \ + if root.mode == "center" else \ + (self.width - root.action_button.width * 2 - dp(6), self.center[1] - root._shift * 2) + size: + (root.action_button.width + dp(6) * 2, root.action_button.width) \ + if root.type == "bottom" else (0, 0) + angle_start: root._angle_start + angle_end: root._angle_end + + BoxLayout: + id: left_actions + orientation: 'horizontal' + size_hint_x: None + padding: [0, (self.height - dp(48))/2] + + BoxLayout: + padding: dp(12), 0 + + MDLabel: + id: label_title + font_style: 'H6' + opposite_colors: root.opposite_colors + theme_text_color: 'Custom' + text_color: root.specific_text_color + text: root.title + shorten: True + shorten_from: 'right' + halign: root.anchor_title + + BoxLayout: + id: right_actions + orientation: 'horizontal' + size_hint_x: None + padding: [0, (self.height - dp(48)) / 2] +""" +) + + +class MDActionBottomAppBarButton(MDFloatingActionButton): + _scale_x = NumericProperty(1) + _scale_y = NumericProperty(1) + + +class MDToolbar( + ThemableBehavior, + RectangularElevationBehavior, + SpecificBackgroundColorBehavior, + BoxLayout, +): + """ + :Events: + `on_action_button` + Method for the button used for the :class:`~MDBottomAppBar` class. + """ + + elevation = NumericProperty(6) + """ + Elevation value. + + :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` + and defaults to `6`. + """ + + left_action_items = ListProperty() + """The icons on the left of the toolbar. + To add one, append a list like the following: + + .. code-block:: kv + + left_action_items: [`'icon_name'`, callback] + + where `'icon_name'` is a string that corresponds to an icon definition and + ``callback`` is the function called on a touch release event. + + :attr:`left_action_items` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + right_action_items = ListProperty() + """The icons on the left of the toolbar. + Works the same way as :attr:`left_action_items`. + + :attr:`right_action_items` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + title = StringProperty() + """Text toolbar. + + :attr:`title` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + md_bg_color = ListProperty([0, 0, 0, 0]) + """Color toolbar. + + :attr:`md_bg_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + anchor_title = OptionProperty("left", options=["left", "center", "right"]) + """Position toolbar title. + Available options are: `'left'`, `'center'`, `'right'`. + + :attr:`anchor_title` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'left'`. + """ + + mode = OptionProperty( + "center", options=["free-end", "free-center", "end", "center"] + ) + """Floating button position. Only for :class:`~MDBottomAppBar` class. + Available options are: `'free-end'`, `'free-center'`, `'end'`, `'center'`. + + :attr:`mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'center'`. + """ + + round = NumericProperty("10dp") + """ + Rounding the corners at the notch for a button. + Onle for :class:`~MDBottomAppBar` class. + + :attr:`round` is an :class:`~kivy.properties.NumericProperty` + and defaults to `'10dp'`. + """ + + icon = StringProperty("android") + """ + Floating button. Onle for :class:`~MDBottomAppBar` class. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'android'`. + """ + + icon_color = ListProperty() + """ + Color action button. Onle for :class:`~MDBottomAppBar` class. + + :attr:`icon_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + type = OptionProperty("top", options=["top", "bottom"]) + """ + When using the :class:`~MDBottomAppBar` class, the parameter ``type`` + must be set to `'bottom'`: + + .. code-block:: kv + + MDBottomAppBar: + + MDToolbar: + type: "bottom" + + Available options are: `'top'`, `'bottom'`. + + :attr:`type` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'top'`. + """ + + _shift = NumericProperty("3.5dp") + _angle_start = NumericProperty(90) + _angle_end = NumericProperty(270) + + def __init__(self, **kwargs): + self.action_button = MDActionBottomAppBarButton() + super().__init__(**kwargs) + self.register_event_type("on_action_button") + self.action_button.bind( + on_release=lambda x: self.dispatch("on_action_button") + ) + self.action_button.x = Window.width / 2 - self.action_button.width / 2 + self.action_button.y = ( + (self.center[1] - self.height / 2) + + self.theme_cls.standard_increment / 2 + + self._shift + ) + if not self.icon_color: + self.icon_color = self.theme_cls.primary_color + Window.bind(on_resize=self._on_resize) + self.bind(specific_text_color=self.update_action_bar_text_colors) + Clock.schedule_once( + lambda x: self.on_left_action_items(0, self.left_action_items) + ) + Clock.schedule_once( + lambda x: self.on_right_action_items(0, self.right_action_items) + ) + + def on_action_button(self, *args): + pass + + def on_md_bg_color(self, instance, value): + if self.type == "bottom": + self.md_bg_color = [0, 0, 0, 0] + + def on_left_action_items(self, instance, value): + self.update_action_bar(self.ids["left_actions"], value) + + def on_right_action_items(self, instance, value): + self.update_action_bar(self.ids["right_actions"], value) + + def update_action_bar(self, action_bar, action_bar_items): + action_bar.clear_widgets() + new_width = 0 + for item in action_bar_items: + new_width += dp(48) + action_bar.add_widget( + MDIconButton( + icon=item[0], + on_release=item[1], + opposite_colors=True, + text_color=self.specific_text_color, + theme_text_color="Custom", + ) + ) + action_bar.width = new_width + + def update_action_bar_text_colors(self, instance, value): + for child in self.ids["left_actions"].children: + child.text_color = self.specific_text_color + for child in self.ids["right_actions"].children: + child.text_color = self.specific_text_color + + def _on_resize(self, instance, width, height): + if self.mode == "center": + self.action_button.x = width / 2 - self.action_button.width / 2 + else: + self.action_button.x = width - self.action_button.width * 2 + + def on_icon(self, instance, value): + self.action_button.icon = value + + def on_icon_color(self, instance, value): + self.action_button.md_bg_color = value + + def on_mode(self, instance, value): + def set_button_pos(*args): + self.action_button.x = x + self.action_button.y = y + self.action_button._hard_shadow_size = (0, 0) + self.action_button._soft_shadow_size = (0, 0) + anim = Animation(_scale_x=1, _scale_y=1, d=0.05) + anim.bind(on_complete=self.set_shadow) + anim.start(self.action_button) + + if value == "center": + self.set_notch() + x = Window.width / 2 - self.action_button.width / 2 + y = ( + (self.center[1] - self.height / 2) + + self.theme_cls.standard_increment / 2 + + self._shift + ) + elif value == "end": + + self.set_notch() + x = Window.width - self.action_button.width * 2 + y = ( + (self.center[1] - self.height / 2) + + self.theme_cls.standard_increment / 2 + + self._shift + ) + self.right_action_items = [] + elif value == "free-end": + self.remove_notch() + x = Window.width - self.action_button.width - dp(10) + y = self.action_button.height + self.action_button.height / 2 + elif value == "free-center": + self.remove_notch() + x = Window.width / 2 - self.action_button.width / 2 + y = self.action_button.height + self.action_button.height / 2 + self.remove_shadow() + anim = Animation(_scale_x=0, _scale_y=0, d=0.05) + anim.bind(on_complete=set_button_pos) + anim.start(self.action_button) + + def remove_notch(self): + self._angle_start = 0 + self._angle_end = 0 + self.round = 0 + self._shift = 0 + + def set_notch(self): + self._angle_start = 90 + self._angle_end = 270 + self.round = dp(10) + self._shift = dp(3.5) + + def remove_shadow(self): + self.action_button._hard_shadow_size = (0, 0) + self.action_button._soft_shadow_size = (0, 0) + + def set_shadow(self, *args): + self.action_button._hard_shadow_size = (dp(112), dp(112)) + self.action_button._soft_shadow_size = (dp(112), dp(112)) + + +class MDBottomAppBar(FloatLayout): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.size_hint_y = None + + def add_widget(self, widget, index=0, canvas=None): + if widget.__class__ is MDToolbar: + super().add_widget(widget) + return super().add_widget(widget.action_button) diff --git a/kivymd/uix/tooltip.py b/kivymd/uix/tooltip.py new file mode 100644 index 0000000..b69538c --- /dev/null +++ b/kivymd/uix/tooltip.py @@ -0,0 +1,296 @@ +""" +Components/Tooltip +================== + +.. seealso:: + + `Material Design spec, Tooltips `_ + +.. rubric:: Tooltips display informative text when users hover over, focus on, + or tap an element. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tooltip.png + :align: center + +To use the :class:`~MDTooltip` class, you must create a new class inherited +from the :class:`~MDTooltip` class: + +In Kv-language: + +.. code-block:: kv + + + +In Python code: + +.. code-block:: python + + class TooltipMDIconButton(MDIconButton, MDTooltip): + pass + +.. Warning:: :class:`~MDTooltip` only works correctly with button and label classes. + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + + + + Screen: + + TooltipMDIconButton: + icon: "language-python" + tooltip_text: self.icon + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tooltip.gif + :align: center + +.. Note:: The behavior of tooltips on desktop and mobile devices is different. + For more detailed information, + `click here `_. +""" + +__all__ = ("MDTooltip", "MDTooltipViewClass") + +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 ( + BoundedNumericProperty, + ListProperty, + NumericProperty, + StringProperty, +) +from kivy.uix.boxlayout import BoxLayout + +from kivymd.material_resources import DEVICE_TYPE +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import HoverBehavior, TouchBehavior + +Builder.load_string( + """ +#:import DEVICE_TYPE kivymd.material_resources.DEVICE_TYPE + + + + size_hint: None, None + width: self.minimum_width + height: self.minimum_height + root.padding[1] + opacity: 0 + + canvas.before: + PushMatrix + Color: + rgba: + root.theme_cls.opposite_bg_dark if not root.tooltip_bg_color \ + else root.tooltip_bg_color + RoundedRectangle: + pos: self.pos + size: self.size + radius: [5] + Scale: + origin: self.center + x: root._scale_x + y: root._scale_y + canvas.after: + PopMatrix + + Label: + id: label_tooltip + text: root.tooltip_text + size_hint: None, None + size: self.texture_size + bold: True + color: + ([0, 0, 0, 1] if not root.tooltip_text_color else root.tooltip_text_color) \ + if root.theme_cls.theme_style == "Dark" else \ + ([1, 1, 1, 1] if not root.tooltip_text_color else root.tooltip_text_color) + pos_hint: {"center_y": .5} +""" +) + + +class MDTooltip(ThemableBehavior, HoverBehavior, TouchBehavior, BoxLayout): + tooltip_bg_color = ListProperty() + """ + Tooltip background color in ``rgba`` format. + + :attr:`tooltip_bg_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + tooltip_text_color = ListProperty() + """ + Tooltip text color in ``rgba`` format. + + :attr:`tooltip_text_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + tooltip_text = StringProperty() + """ + Tooltip text. + + :attr:`tooltip_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + tooltip_display_delay = BoundedNumericProperty(0, min=0, max=4) + """ + Tooltip dsiplay delay. + + :attr:`tooltip_display_delay` is an :class:`~kivy.properties.BoundedNumericProperty` + and defaults to `0`, min of `0` & max of `4`. This property only works on desktop. + """ + + shift_y = NumericProperty() + """ + Y-offset of tooltip text. + + :attr:`shift_y` is an :class:`~kivy.properties.StringProperty` + and defaults to `0`. + """ + + _tooltip = None + + def delete_clock(self, widget, touch, *args): + if self.collide_point(touch.x, touch.y) and touch.grab_current: + try: + Clock.unschedule(touch.ud["event"]) + except KeyError: + pass + self.on_leave() + + def adjust_tooltip_position(self, x, y): + """Returns the coordinates of the tooltip + that fit into the borders of the screen.""" + + # If the position of the tooltip is outside the right border + # of the screen. + if x + self._tooltip.width > Window.width: + x = Window.width - (self._tooltip.width + dp(10)) + else: + # If the position of the tooltip is outside the left border + # of the screen. + if x < 0: + x = "10dp" + # If the tooltip position is below bottom the screen border. + if y < 0: + y = dp(10) + # If the tooltip position is below top the screen border. + else: + if Window.height - self._tooltip.height < y: + y = Window.height - (self._tooltip.height + dp(10)) + return x, y + + def display_tooltip(self, interval): + if not self._tooltip: + return + Window.add_widget(self._tooltip) + pos = self.to_window(self.center_x, self.center_y) + x = pos[0] - self._tooltip.width / 2 + + if not self.shift_y: + y = pos[1] - self._tooltip.height / 2 - self.height / 2 - dp(20) + else: + y = pos[1] - self._tooltip.height / 2 - self.height + self.shift_y + + x, y = self.adjust_tooltip_position(x, y) + self._tooltip.pos = (x, y) + + if DEVICE_TYPE == "desktop": + Clock.schedule_once( + self.animation_tooltip_show, self.tooltip_display_delay + ) + else: + Clock.schedule_once(self.animation_tooltip_show, 0) + + def animation_tooltip_show(self, interval): + if not self._tooltip: + return + ( + Animation(_scale_x=1, _scale_y=1, d=0.1) + + Animation(opacity=1, d=0.2) + ).start(self._tooltip) + + def remove_tooltip(self, *args): + Window.remove_widget(self._tooltip) + + def on_long_touch(self, touch, *args): + if DEVICE_TYPE != "desktop": + self.on_enter(True) + + def on_enter(self, *args): + """See + :attr:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior.on_enter` + method in :class:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior` + class. + """ + + if not args and DEVICE_TYPE != "desktop": + return + else: + if not self.tooltip_text: + return + self._tooltip = MDTooltipViewClass( + tooltip_bg_color=self.tooltip_bg_color, + tooltip_text_color=self.tooltip_text_color, + tooltip_text=self.tooltip_text, + ) + Clock.schedule_once(self.display_tooltip, -1) + + def on_leave(self): + """See + :attr:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior.on_leave` + method in :class:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior` + class. + """ + + if self._tooltip: + Window.remove_widget(self._tooltip) + self._tooltip = None + + +class MDTooltipViewClass(ThemableBehavior, BoxLayout): + tooltip_bg_color = ListProperty() + """ + See :attr:`~MDTooltip.tooltip_bg_color`. + """ + + tooltip_text_color = ListProperty() + """ + See :attr:`~MDTooltip.tooltip_text_color`. + """ + + tooltip_text = StringProperty() + """ + See :attr:`~MDTooltip.tooltip_text`. + """ + + _scale_x = NumericProperty(0) + _scale_y = NumericProperty(0) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.padding = [ + dp(8) if DEVICE_TYPE == "desktop" else dp(16), + dp(4), + dp(8) if DEVICE_TYPE == "desktop" else dp(16), + dp(4), + ] diff --git a/kivymd/utils/__init__.py b/kivymd/utils/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/kivymd/utils/asynckivy.py b/kivymd/utils/asynckivy.py new file mode 100755 index 0000000..373ab0b --- /dev/null +++ b/kivymd/utils/asynckivy.py @@ -0,0 +1,67 @@ +""" +asynckivy +========= + +Copyright (c) 2019 Nattōsai Mitō + +GitHub - + https://github.com/gottadiveintopython +GitHub Gist - + https://gist.github.com/gottadiveintopython/5f4a775849f9277081c396de65dc57c1 + +""" + +__all__ = ("start", "sleep", "event") + +import types +from collections import namedtuple +from functools import partial + +from kivy.clock import Clock + +CallbackParameter = namedtuple("CallbackParameter", ("args", "kwargs")) + + +def start(coro): + def step(*args, **kwargs): + try: + coro.send(CallbackParameter(args, kwargs))(step) + except StopIteration: + pass + + try: + coro.send(None)(step) + except StopIteration: + pass + + +@types.coroutine +def sleep(duration): + # The partial() here looks meaningless. But this is needed in order + # to avoid weak reference. + param = yield lambda step_coro: Clock.schedule_once( + partial(step_coro), duration + ) + return param.args[0] + + +class event: + def __init__(self, ed, name): + self.bind_id = None + self.ed = ed + self.name = name + + def bind(self, step_coro): + self.bind_id = bind_id = self.ed.fbind(self.name, self.callback) + assert bind_id > 0 # check if binding succeeded + self.step_coro = step_coro + + def callback(self, *args, **kwargs): + self.parameter = CallbackParameter(args, kwargs) + ed = self.ed + ed.unbind_uid(self.name, self.bind_id) + self.step_coro() + + def __await__(self): + yield self.bind + return self.parameter diff --git a/kivymd/utils/cropimage.py b/kivymd/utils/cropimage.py new file mode 100755 index 0000000..647be4c --- /dev/null +++ b/kivymd/utils/cropimage.py @@ -0,0 +1,117 @@ +""" +Crop Image +========== +""" + + +def crop_image( + cutting_size, + path_to_image, + path_to_save_crop_image, + corner=0, + blur=0, + corner_mode="all", +): + """Call functions of cropping/blurring/rounding image. + + cutting_size: size to which the image will be cropped; + path_to_image: path to origin image; + path_to_save_crop_image: path to new image; + corner: value of rounding corners; + blur: blur value; + corner_mode: 'all'/'top'/'bottom' - indicates which corners to round out; + + """ + + im = _crop_image(cutting_size, path_to_image, path_to_save_crop_image) + if corner: + im = add_corners(im, corner, corner_mode) + if blur: + im = add_blur(im, blur) + try: + im.save(path_to_save_crop_image) + except IOError: + im.save(path_to_save_crop_image, "JPEG") + + +def add_blur(im, mode): + from PIL import ImageFilter + + im = im.filter(ImageFilter.GaussianBlur(mode)) + + return im + + +def _crop_image(cutting_size, path_to_image, path_to_save_crop_image): + from PIL import Image, ImageOps + + image = Image.open(path_to_image) + image = ImageOps.fit(image, cutting_size) + image.save(path_to_save_crop_image) + + return image + + +def add_corners(im, corner, corner_mode): + def add_top_corners(): + alpha.paste(circle.crop((0, 0, corner, corner)), (0, 0)) + alpha.paste( + circle.crop((corner, 0, corner * 2, corner)), (w - corner, 0) + ) + + def add_bottom_corners(): + alpha.paste( + circle.crop((0, corner, corner, corner * 2)), (0, h - corner) + ) + alpha.paste( + circle.crop((corner, corner, corner * 2, corner * 2)), + (w - corner, h - corner), + ) + + from PIL import Image, ImageDraw + + circle = Image.new("L", (corner * 2, corner * 2), 0) + draw = ImageDraw.Draw(circle) + draw.ellipse((0, 0, corner * 2, corner * 2), fill=255) + alpha = Image.new("L", im.size, 255) + w, h = im.size + + if corner_mode == "all": + add_top_corners() + add_bottom_corners() + elif corner_mode == "top": + add_top_corners() + if corner_mode == "bottom": + add_bottom_corners() + im.putalpha(alpha) + + return im + + +def prepare_mask(size, antialias=2): + from PIL import Image, ImageDraw + + mask = Image.new("L", (size[0] * antialias, size[1] * antialias), 0) + ImageDraw.Draw(mask).ellipse((0, 0) + mask.size, fill=255) + return mask.resize(size, Image.ANTIALIAS) + + +def _crop_round_image(im, s): + from PIL import Image + + w, h = im.size + k = w // s[0] - h // s[1] + if k > 0: + im = im.crop(((w - h) // 2, 0, (w + h) // 2, h)) + elif k < 0: + im = im.crop((0, (h - w) // 2, w, (h + w) // 2)) + return im.resize(s, Image.ANTIALIAS) + + +def crop_round_image(cutting_size, path_to_image, path_to_new_image): + from PIL import Image + + im = Image.open(path_to_image) + im = _crop_round_image(im, cutting_size) + im.putalpha(prepare_mask(cutting_size, 4)) + im.save(path_to_new_image) diff --git a/kivymd/utils/fitimage.py b/kivymd/utils/fitimage.py new file mode 100644 index 0000000..0073148 --- /dev/null +++ b/kivymd/utils/fitimage.py @@ -0,0 +1,173 @@ +""" +Fit Image +========= + +Feature to automatically crop a `Kivy` image to fit your layout +Write by Benedikt Zwölfer + +Referene - https://gist.github.com/benni12er/95a45eb168fc33a4fcd2d545af692dad + + +Example: +======== + +.. code-block:: kv + + BoxLayout: + size_hint_y: None + height: "200dp" + orientation: 'vertical' + + FitImage: + size_hint_y: 3 + source: 'images/img1.jpg' + + FitImage: + size_hint_y: 1 + source: 'images/img2.jpg' + +Example with round corners: +========================== + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/fitimage-round-corners.png + :align: center + +.. code-block:: python + + from kivy.uix.modalview import ModalView + from kivy.lang import Builder + + from kivymd import images_path + from kivymd.app import MDApp + from kivymd.uix.card import MDCard + + Builder.load_string( + ''' + : + elevation: 10 + radius: [36, ] + + FitImage: + id: bg_image + source: "images/bg.png" + size_hint_y: .35 + pos_hint: {"top": 1} + radius: [36, 36, 0, 0, ] + ''') + + + class Card(MDCard): + pass + + + class Example(MDApp): + def build(self): + modal = ModalView( + size_hint=(0.4, 0.8), + background=f"{images_path}/transparent.png", + overlay_color=(0, 0, 0, 0), + ) + modal.add_widget(Card()) + modal.open() + + + Example().run() +""" + +__all__ = ("FitImage",) + +from kivy.clock import Clock +from kivy.graphics.context_instructions import Color +from kivy.graphics.vertex_instructions import Rectangle +from kivy.lang import Builder +from kivy.properties import ListProperty, ObjectProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.image import AsyncImage +from kivy.uix.widget import Widget + +Builder.load_string( + """ + + canvas.before: + StencilPush + RoundedRectangle: + size: self.size + pos: self.pos + radius: root.radius + StencilUse + + canvas.after: + StencilUnUse + RoundedRectangle: + size: self.size + pos: self.pos + radius: root.radius + StencilPop +""" +) + + +class FitImage(BoxLayout): + source = ObjectProperty() + container = ObjectProperty() + radius = ListProperty([0, 0, 0, 0]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self._late_init) + + def _late_init(self, *args): + self.container = Container(self.source) + self.bind(source=self.container.setter("source")) + self.add_widget(self.container) + + +class Container(Widget): + source = ObjectProperty() + image = ObjectProperty() + + def __init__(self, source, **kwargs): + super().__init__(**kwargs) + self.image = AsyncImage() + self.image.bind(on_load=self.adjust_size) + self.source = source + self.bind(size=self.adjust_size, pos=self.adjust_size) + + def on_source(self, instance, value): + if isinstance(value, str): + self.image.source = value + else: + self.image.texture = value + self.adjust_size() + + def adjust_size(self, *args): + if not self.parent or not self.image.texture: + return + + (par_x, par_y) = self.parent.size + + if par_x == 0 or par_y == 0: + with self.canvas: + self.canvas.clear() + return + + par_scale = par_x / par_y + (img_x, img_y) = self.image.texture.size + img_scale = img_x / img_y + + if par_scale > img_scale: + (img_x_new, img_y_new) = (img_x, img_x / par_scale) + else: + (img_x_new, img_y_new) = (img_y * par_scale, img_y) + + crop_pos_x = (img_x - img_x_new) / 2 + crop_pos_y = (img_y - img_y_new) / 2 + + subtexture = self.image.texture.get_region( + crop_pos_x, crop_pos_y, img_x_new, img_y_new + ) + + with self.canvas: + self.canvas.clear() + Color(1, 1, 1) + Rectangle(texture=subtexture, pos=self.pos, size=(par_x, par_y)) diff --git a/kivymd/utils/fpsmonitor.py b/kivymd/utils/fpsmonitor.py new file mode 100644 index 0000000..2284ba8 --- /dev/null +++ b/kivymd/utils/fpsmonitor.py @@ -0,0 +1,45 @@ +""" +Monitor module +============== + +The Monitor module is a toolbar that shows the activity of your current +application : + +* FPS + +""" + +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import NumericProperty, StringProperty +from kivy.uix.label import Label + +Builder.load_string( + """ +: + size_hint_y: None + height: self.texture_size[1] + text: root._fsp_value + pos_hint: {"top": 1} + + canvas.before: + Color: + rgba: app.theme_cls.primary_dark + Rectangle: + pos: self.pos + size: self.size +""" +) + + +class FpsMonitor(Label): + updated_interval = NumericProperty(0.5) + """FPS refresh rate.""" + + _fsp_value = StringProperty() + + def start(self): + Clock.schedule_interval(self.update_fps, self.updated_interval) + + def update_fps(self, *args): + self._fsp_value = "FPS: %f" % Clock.get_fps() diff --git a/kivymd/utils/hot_reload_viewer.py b/kivymd/utils/hot_reload_viewer.py new file mode 100644 index 0000000..2f85a38 --- /dev/null +++ b/kivymd/utils/hot_reload_viewer.py @@ -0,0 +1,222 @@ +""" +HotReloadViewer +=============== + +.. Note:: The :class:`~HotReloadViewer` class is based on + the `KvViewerApp `_ class + +:class:`~HotReloadViewer`, for KV-Viewer, is a simple tool allowing you to +dynamically display a KV file, taking its changes into account +(thanks to watchdog). The idea is to facilitate design using the KV language. + +Usage +----- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + #:import KivyLexer kivy.extras.highlight.KivyLexer + #:import HotReloadViewer kivymd.utils.hot_reload_viewer.HotReloadViewer + + + BoxLayout: + + CodeInput: + lexer: KivyLexer() + style_name: "native" + on_text: app.update_kv_file(self.text) + size_hint_x: .7 + + HotReloadViewer: + size_hint_x: .3 + path: app.path_to_kv_file + errors: True + errors_text_color: 1, 1, 0, 1 + errors_background_color: app.theme_cls.bg_dark + ''' + + + class Example(MDApp): + path_to_kv_file = "kv_file.kv" + + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + def update_kv_file(self, text): + with open(self.path_to_kv_file, "w") as kv_file: + kv_file.write(text) + + + Example().run() + +This will display the test.kv and automatically update the display when the +file changes. + +.. raw:: html + +
+ +
+ + +.. rubric:: This scripts uses watchdog to listen for file changes. To install + watchdog. + +.. code-block:: bash + + pip install watchdog +""" + +import os + +from kivy.clock import Clock, mainthread +from kivy.lang import Builder +from kivy.properties import BooleanProperty, ListProperty, StringProperty +from kivy.uix.scrollview import ScrollView +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer + +from kivymd.theming import ThemableBehavior +from kivymd.uix.boxlayout import MDBoxLayout + +Builder.load_string( + """ + + + MDLabel: + size_hint_y: None + height: self.texture_size[1] + theme_text_color: "Custom" + text_color: + root.errors_text_color if root.errors_text_color \ + else root.theme_cls.text_color + text: root.text +""" +) + + +class HotReloadErrorText(ThemableBehavior, ScrollView): + text = StringProperty() + """Text errors. + + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + errors_text_color = ListProperty() + """ + Error text color. + + :attr:`errors_text_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + +class HotReloadHandler(FileSystemEventHandler): + def __init__(self, callback, target, **kwargs): + super().__init__(**kwargs) + self.callback = callback + self.target = target + + def on_any_event(self, event): + self.callback() + + +class HotReloadViewer(ThemableBehavior, MDBoxLayout): + """ + :Events: + :attr:`on_error` + Called when an error occurs in the KV-file that the user is editing. + """ + + path = StringProperty() + """Path to KV file. + + :attr:`path` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + errors = BooleanProperty(False) + """ + Show errors while editing KV-file. + + :attr:`errors` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + errors_background_color = ListProperty() + """ + Error background color. + + :attr:`errors_background_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + errors_text_color = ListProperty() + """ + Error text color. + + :attr:`errors_text_color` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + _temp_widget = None + + def __init__(self, **kwargs): + self.observer = Observer() + self.error_text = HotReloadErrorText() + super().__init__(**kwargs) + self.register_event_type("on_error") + + @mainthread + def update(self, *args): + """Updates and displays the KV-file that the user edits.""" + + Builder.unload_file(self.path) + self.clear_widgets() + try: + self.padding = (0, 0, 0, 0) + self.md_bg_color = (0, 0, 0, 0) + self._temp_widget = Builder.load_file(self.path) + self.add_widget(self._temp_widget) + except Exception as error: + self.show_error(error) + self.dispatch("on_error", error) + + def show_error(self, error): + """Displays text with a current error.""" + + if self._temp_widget and not self.errors: + self.add_widget(self._temp_widget) + return + else: + if self.errors_background_color: + self.md_bg_color = self.errors_background_color + self.padding = ("4dp", "4dp", "4dp", "4dp") + self.error_text.text = ( + error.message + if getattr(error, r"message", None) + else str(error) + ) + self.add_widget(self.error_text) + + def on_error(self, *args): + """ + Called when an error occurs in the KV-file that the user is editing. + """ + + def on_errors_text_color(self, instance, value): + self.error_text.errors_text_color = value + + def on_path(self, instance, value): + value = os.path.abspath(value) + self.observer.schedule( + HotReloadHandler(self.update, value), os.path.dirname(value) + ) + self.observer.start() + Clock.schedule_once(self.update, 1) diff --git a/kivymd/vendor/__init__.py b/kivymd/vendor/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kivymd/vendor/circleLayout/LICENSE b/kivymd/vendor/circleLayout/LICENSE new file mode 100644 index 0000000..9d6e5b5 --- /dev/null +++ b/kivymd/vendor/circleLayout/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Davide Depau + +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. + diff --git a/kivymd/vendor/circleLayout/README.md b/kivymd/vendor/circleLayout/README.md new file mode 100644 index 0000000..6cf54bb --- /dev/null +++ b/kivymd/vendor/circleLayout/README.md @@ -0,0 +1,21 @@ +CircularLayout +============== + +CircularLayout is a special layout that places widgets around a circle. + +See the widget's documentation and the example for more information. + +![Screenshot](screenshot.png) + +size_hint +--------- + +size_hint_x is used as an angle-quota hint (widget with higher +size_hint_x will be farther from each other, and viceversa), while +size_hint_y is used as a widget size hint (widgets with a higher size +hint will be bigger).size_hint_x cannot be None. + +Widgets are all squares, unless you set size_hint_y to None (in that +case you'll be able to specify your own size), and their size is the +difference between the outer and the inner circle's radii. To make the +widgets bigger you can just decrease inner_radius_hint. \ No newline at end of file diff --git a/kivymd/vendor/circleLayout/__init__.py b/kivymd/vendor/circleLayout/__init__.py new file mode 100644 index 0000000..9e65e18 --- /dev/null +++ b/kivymd/vendor/circleLayout/__init__.py @@ -0,0 +1,220 @@ +""" +CircularLayout +============== + +CircularLayout is a special layout that places widgets around a circle. + +size_hint +--------- + +size_hint_x is used as an angle-quota hint (widget with higher +size_hint_x will be farther from each other, and vice versa), while +size_hint_y is used as a widget size hint (widgets with a higher size +hint will be bigger).size_hint_x cannot be None. + +Widgets are all squares, unless you set size_hint_y to None (in that +case you'll be able to specify your own size), and their size is the +difference between the outer and the inner circle's radii. To make the +widgets bigger you can just decrease inner_radius_hint. +""" + +__all__ = ("CircularLayout",) + +from math import cos, pi, radians, sin + +from kivy.properties import ( + AliasProperty, + BoundedNumericProperty, + NumericProperty, + OptionProperty, + ReferenceListProperty, + VariableListProperty, +) +from kivy.uix.layout import Layout + +try: + xrange(1, 2) +except NameError: + + def xrange(first, second, third=None): + if third: + return range(first, second, third) + else: + return range(first, second) + + +class CircularLayout(Layout): + """ + Circular layout class. See module documentation for more information. + """ + + padding = VariableListProperty([0, 0, 0, 0]) + """Padding between the layout box and it's children: [padding_left, + padding_top, padding_right, padding_bottom]. + + padding also accepts a two argument form [padding_horizontal, + padding_vertical] and a one argument form [padding]. + + .. version changed:: 1.7.0 + Replaced NumericProperty with VariableListProperty. + + :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and + defaults to [0, 0, 0, 0]. + """ + + start_angle = NumericProperty(0) + """Angle (in degrees) at which the first widget will be placed. + Start counting angles from the X axis, going counterclockwise. + + :attr:`start_angle` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0 (start from the right). + """ + + circle_quota = BoundedNumericProperty(360, min=0, max=360) + """Size (in degrees) of the part of the circumference that will actually + be used to place widgets. + + :attr:`circle_quota` is a :class:`~kivy.properties.BoundedNumericProperty` + and defaults to 360 (all the circumference). + """ + + direction = OptionProperty("ccw", options=("cw", "ccw")) + """Direction of widgets in the circle. + + :attr:`direction` is an :class:`~kivy.properties.OptionProperty` and + defaults to 'ccw'. Can be 'ccw' (counterclockwise) or 'cw' (clockwise). + """ + + outer_radius_hint = NumericProperty(1) + """Sets the size of the outer circle. A number greater than 1 will make the + widgets larger than the actual widget, a number smaller than 1 will leave + a gap. + + :attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` + and defaults to 1. + """ + + inner_radius_hint = NumericProperty(0.6) + """Sets the size of the inner circle. A number greater than + :attr:`outer_radius_hint` will cause glitches. The closest it is to + :attr:`outer_radius_hint`, the smallest will be the widget in the layout. + + :attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` + and defaults to 1. + """ + + radius_hint = ReferenceListProperty(inner_radius_hint, outer_radius_hint) + """Combined :attr:`outer_radius_hint` and :attr:`inner_radius_hint` + in a list for convenience. See their documentation for more details. + + :attr:`radius_hint` is a :class:`~kivy.properties.ReferenceListProperty`. + """ + + def _get_delta_radii(self): + radius = ( + min( + self.width - self.padding[0] - self.padding[2], + self.height - self.padding[1] - self.padding[3], + ) + / 2.0 + ) + outer_r = radius * self.outer_radius_hint + inner_r = radius * self.inner_radius_hint + return outer_r - inner_r + + delta_radii = AliasProperty( + _get_delta_radii, None, bind=("radius_hint", "padding", "size") + ) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self.bind( + start_angle=self._trigger_layout, + parent=self._trigger_layout, + # padding=self._trigger_layout, + children=self._trigger_layout, + size=self._trigger_layout, + radius_hint=self._trigger_layout, + pos=self._trigger_layout, + ) + + def do_layout(self, *largs): + # optimize layout by preventing looking at the same attribute in a loop + len_children = len(self.children) + if len_children == 0: + return + selfcx = self.center_x + selfcy = self.center_y + direction = self.direction + cquota = radians(self.circle_quota) + start_angle_r = radians(self.start_angle) + padding_left = self.padding[0] + padding_top = self.padding[1] + padding_right = self.padding[2] + padding_bottom = self.padding[3] + padding_x = padding_left + padding_right + padding_y = padding_top + padding_bottom + + radius = min(self.width - padding_x, self.height - padding_y) / 2.0 + outer_r = radius * self.outer_radius_hint + inner_r = radius * self.inner_radius_hint + middle_r = radius * sum(self.radius_hint) / 2.0 + delta_r = outer_r - inner_r + + stretch_weight_angle = 0.0 + for w in self.children: + sha = w.size_hint_x + if sha is None: + raise ValueError( + "size_hint_x cannot be None in a CircularLayout" + ) + else: + stretch_weight_angle += sha + + sign = +1.0 + angle_offset = start_angle_r + if direction == "cw": + angle_offset = 2 * pi - start_angle_r + sign = -1.0 + + for c in reversed(self.children): + sha = c.size_hint_x + shs = c.size_hint_y + + angle_quota = cquota / stretch_weight_angle * sha + angle = angle_offset + (sign * angle_quota / 2) + angle_offset += sign * angle_quota + + # kived: looking it up, yes. x = cos(angle) * radius + centerx; + # y = sin(angle) * radius + centery + ccx = cos(angle) * middle_r + selfcx + padding_left - padding_right + ccy = sin(angle) * middle_r + selfcy + padding_bottom - padding_top + + c.center_x = ccx + c.center_y = ccy + if shs: + s = delta_r * shs + c.width = s + c.height = s + + +if __name__ == "__main__": + from kivymd.app import MDApp + from kivy.uix.button import Button + + class CircLayoutApp(MDApp): + def build(self): + cly = CircularLayout( + direction="cw", + start_angle=-75, + inner_radius_hint=0.7, + padding="20dp", + ) + + for i in xrange(1, 13): + cly.add_widget(Button(text=str(i), font_size="30dp")) + + return cly + + CircLayoutApp().run() diff --git a/kivymd/vendor/circularTimePicker/LICENSE b/kivymd/vendor/circularTimePicker/LICENSE new file mode 100644 index 0000000..9d6e5b5 --- /dev/null +++ b/kivymd/vendor/circularTimePicker/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Davide Depau + +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. + diff --git a/kivymd/vendor/circularTimePicker/README.md b/kivymd/vendor/circularTimePicker/README.md new file mode 100644 index 0000000..20ac2de --- /dev/null +++ b/kivymd/vendor/circularTimePicker/README.md @@ -0,0 +1,43 @@ +Circular Date & Time Picker for Kivy +==================================== + +(currently only time, date coming soon) + +Based on [CircularLayout](https://github.com/kivy-garden/garden.circularlayout). +The main aim is to provide a date and time selector similar to the +one found in Android KitKat+. + +![Screenshot](screenshot.png) + +Simple usage +------------ + +Import the widget with + +```python +from kivy.garden.circulardatetimepicker import CircularTimePicker +``` + +then use it! That's it! + +```python +c = CircularTimePicker() +c.bind(time=self.set_time) +root.add_widget(c) +``` + +in Kv language: + +``` +: + BoxLayout: + orientation: "vertical" + + CircularTimePicker + + Button: + text: "Dismiss" + size_hint_y: None + height: "40dp" + on_release: root.dismiss() +``` \ No newline at end of file diff --git a/kivymd/vendor/circularTimePicker/__init__.py b/kivymd/vendor/circularTimePicker/__init__.py new file mode 100644 index 0000000..98b3548 --- /dev/null +++ b/kivymd/vendor/circularTimePicker/__init__.py @@ -0,0 +1,881 @@ +""" +Circular Date & Time Picker for Kivy +==================================== + +(currently only time, date coming soon) + +Based on [CircularLayout](https://github.com/kivy-garden/garden.circularlayout). +The main aim is to provide a date and time selector similar to the +one found in Android KitKat+. + +Simple usage +------------ + +Import the widget with + +.. code-block:: python + + from kivy.garden.circulardatetimepicker import CircularTimePicker + +then use it! That's it! + +.. code-block:: python + + from kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + + + class Example(MDApp): + def build(self): + box = MDScreen(md_bg_color=self.theme_cls.bg_darkest) + box.add_widget(CircularTimePicker()) + return box + + + Example().run() + +in Kv language: + +.. code-block:: kv + + : + + MDBoxLayout: + orientation: "vertical" + + CircularTimePicker: + + Button: + text: "Dismiss" + size_hint_y: None + height: "40dp" + on_release: root.dismiss() +""" + +import datetime +import sys +from math import atan, cos, pi, radians, sin + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.graphics import Color, Ellipse, Line +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + BoundedNumericProperty, + DictProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + ReferenceListProperty, + StringProperty, +) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.label import Label + +from kivymd.theming import ThemableBehavior +from kivymd.vendor.circleLayout import CircularLayout + +if sys.version_info[0] > 2: + + def xrange(first=None, second=None, third=None): + if third: + return range(first, second, third) + else: + return range(first, second) + + +def map_number(x, in_min, in_max, out_min, out_max): + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min + + +def rgb_to_hex(*color): + tor = "#" + for col in color: + tor += "{:>02}".format(hex(int(col * 255))[2:]) + return tor + + +Builder.load_string( + """ +: + text_size: self.size + valign: "middle" + halign: "center" + font_size: self.height * self.size_factor + + +: + canvas.before: + PushMatrix + Scale: + origin: + self.center_x + self.padding[0] - self.padding[2], \ + self.center_y + self.padding[3] - self.padding[1] + x: self.scale + y: self.scale + canvas.after: + PopMatrix + + +: + orientation: "vertical" + spacing: "20dp" + + FloatLayout: + anchor_x: "center" + anchor_y: "center" + size_hint_y: 1./3 + size_hint_x: 1 + size: root.size + pos: root.pos + + GridLayout: + cols: 2 + spacing: "10dp" + size_hint_x: None + width: self.minimum_width + pos_hint: {'center_x': .5, 'center_y': .5} + + Label: + id: timelabel + text: root.time_text + markup: True + halign: "right" + valign: "middle" + # text_size: self.size + size_hint_x: None #.6 + width: self.texture_size[0] + font_size: self.height * .75 + + Label: + id: ampmlabel + text: root.ampm_text + markup: True + halign: "left" + valign: "middle" + # text_size: self.size + size_hint_x: None #.4 + width: self.texture_size[0] + font_size: self.height * .3 + + FloatLayout: + id: picker_container + #size_hint_y: 2./3 + _bound: {} +""" +) + + +class Number(Label): + """The class used to show the numbers in the selector.""" + + size_factor = NumericProperty(0.5) + """Font size scale. + + :attr:`size_factor` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0.5. + """ + + +class CircularNumberPicker(CircularLayout): + """A circular number picker based on CircularLayout. A selector will + help you pick a number. You can also set :attr:`multiples_of` to make + it show only some numbers and use the space in between for the other + numbers. + """ + + min = NumericProperty(0) + """The first value of the range. + + :attr:`min` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + """ + + max = NumericProperty(0) + """The last value of the range. Note that it behaves like xrange, so + the actual last displayed value will be :attr:`max` - 1. + + :attr:`max` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + """ + + range = ReferenceListProperty(min, max) + """Packs :attr:`min` and :attr:`max` into a list for convenience. See + their documentation for further information. + + :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty`. + """ + + multiples_of = NumericProperty(1) + """Only show numbers that are multiples of this number. The other numbers + will be selectable, but won't have their own label. + + :attr:`multiples_of` is a :class:`~kivy.properties.NumericProperty` and + defaults to 1. + """ + + # selector_color = ListProperty([.337, .439, .490]) + selector_color = ListProperty([1, 1, 1]) + """Color of the number selector. RGB. + + :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and + defaults to [.337, .439, .490] (material green). + """ + + color = ListProperty([0, 0, 0]) + """Color of the number labels and of the center dot. RGB. + + :attr:`color` is a :class:`~kivy.properties.ListProperty` and + defaults to [1, 1, 1] (white). + """ + + selector_alpha = BoundedNumericProperty(0.3, min=0, max=1) + """Alpha value for the transparent parts of the selector. + + :attr:`selector_alpha` + is a :class:`~kivy.properties.BoundedNumericProperty` + and defaults to 0.3 (min=0, max=1). + """ + + selected = NumericProperty(None) + """Currently selected number. + + :attr:`selected` is a :class:`~kivy.properties.NumericProperty` and + defaults to :attr:`min`. + """ + + number_size_factor = NumericProperty(0.5) + """Font size scale factor for the :class:`~Number`. + + :attr:`number_size_factor` + is a :class:`~kivy.properties.NumericProperty` and defaults to 0.5. + """ + + number_format_string = StringProperty("{}") + """String that will be formatted with the selected number as the + first argument. + Can be anything supported by :meth:`str.format` (es. "{:02d}"). + + :attr:`number_format_string` + is a :class:`~kivy.properties.StringProperty` and defaults to "{}". + """ + + scale = NumericProperty(1) + """Canvas scale factor. Used in :class:`CircularTimePicker` transitions. + + :attr:`scale` is a :class:`~kivy.properties.NumericProperty` and + defaults to 1. + """ + + _selection_circle = ObjectProperty(None) + _selection_line = ObjectProperty(None) + _selection_dot = ObjectProperty(None) + _selection_dot_color = ObjectProperty(None) + _selection_color = ObjectProperty(None) + _center_dot = ObjectProperty(None) + _center_color = ObjectProperty(None) + + def _get_items(self): + return self.max - self.min + + items = AliasProperty(_get_items, None) + + def _get_shown_items(self): + sh = 0 + for i in xrange(*self.range): + if i % self.multiples_of == 0: + sh += 1 + return sh + + shown_items = AliasProperty(_get_shown_items, None) + + def __init__(self, **kw): + self._trigger_genitems = Clock.create_trigger(self._genitems, -1) + self.bind( + min=self._trigger_genitems, + max=self._trigger_genitems, + multiples_of=self._trigger_genitems, + ) + super().__init__(**kw) + self.selected = self.min + self.bind( + selected=self.on_selected, + pos=self.on_selected, + size=self.on_selected, + ) + + cx = self.center_x + self.padding[0] - self.padding[2] + cy = self.center_y + self.padding[3] - self.padding[1] + sx, sy = self.pos_for_number(self.selected) + epos = [ + i - (self.delta_radii * self.number_size_factor) for i in (sx, sy) + ] + esize = [self.delta_radii * self.number_size_factor * 2] * 2 + dsize = [i * 0.3 for i in esize] + dpos = [i + esize[0] / 2.0 - dsize[0] / 2.0 for i in epos] + csize = [i * 0.05 for i in esize] + cpos = [i - csize[0] / 2.0 for i in (cx, cy)] + dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 + color = list(self.selector_color) + + with self.canvas: + self._selection_color = Color(*(color + [self.selector_alpha])) + self._selection_circle = Ellipse(pos=epos, size=esize) + self._selection_line = Line(points=[cx, cy, sx, sy], width=dp(1.25)) + self._selection_dot_color = Color(*(color + [dot_alpha])) + self._selection_dot = Ellipse(pos=dpos, size=dsize) + self._center_color = Color(*self.color) + self._center_dot = Ellipse(pos=cpos, size=csize) + + self.bind( + selector_color=lambda ign, u: setattr( + self._selection_color, "rgba", u + [self.selector_alpha] + ) + ) + self.bind( + selector_color=lambda ign, u: setattr( + self._selection_dot_color, "rgb", u + ) + ) + self.bind(selector_color=lambda ign, u: self.dot_is_none()) + self.bind(color=lambda ign, u: setattr(self._center_color, "rgb", u)) + Clock.schedule_once(self._genitems) + # Just to make sure pos/size are set + Clock.schedule_once(self.on_selected) + + def dot_is_none(self, *args): + dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 + if self._selection_dot_color: + self._selection_dot_color.a = dot_alpha + + def _genitems(self, *a): + self.clear_widgets() + for i in xrange(*self.range): + if i % self.multiples_of != 0: + continue + n = Number( + text=self.number_format_string.format(i), + size_factor=self.number_size_factor, + color=self.color, + ) + self.bind(color=n.setter("color")) + self.add_widget(n) + + def on_touch_down(self, touch): + if not self.collide_point(*touch.pos): + return + touch.grab(self) + self.selected = self.number_at_pos(*touch.pos) + if self.selected == 60: + self.selected = 0 + + def on_touch_move(self, touch): + if touch.grab_current is not self: + return super().on_touch_move(touch) + self.selected = self.number_at_pos(*touch.pos) + if self.selected == 60: + self.selected = 0 + + def on_touch_up(self, touch): + if touch.grab_current is not self: + return super().on_touch_up(touch) + touch.ungrab(self) + + def on_selected(self, *a): + cx = self.center_x + self.padding[0] - self.padding[2] + cy = self.center_y + self.padding[3] - self.padding[1] + sx, sy = self.pos_for_number(self.selected) + epos = [ + i - (self.delta_radii * self.number_size_factor) for i in (sx, sy) + ] + esize = [self.delta_radii * self.number_size_factor * 2] * 2 + dsize = [i * 0.3 for i in esize] + dpos = [i + esize[0] / 2.0 - dsize[0] / 2.0 for i in epos] + csize = [i * 0.05 for i in esize] + cpos = [i - csize[0] / 2.0 for i in (cx, cy)] + dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 + + if self._selection_circle: + self._selection_circle.pos = epos + self._selection_circle.size = esize + if self._selection_line: + self._selection_line.points = [cx, cy, sx, sy] + if self._selection_dot: + self._selection_dot.pos = dpos + self._selection_dot.size = dsize + if self._selection_dot_color: + self._selection_dot_color.a = dot_alpha + if self._center_dot: + self._center_dot.pos = cpos + self._center_dot.size = csize + + def pos_for_number(self, n): + """Returns the center x, y coordinates for a given number.""" + + if self.items == 0: + return 0, 0 + radius = ( + min( + self.width - self.padding[0] - self.padding[2], + self.height - self.padding[1] - self.padding[3], + ) + / 2.0 + ) + middle_r = radius * sum(self.radius_hint) / 2.0 + cx = self.center_x + self.padding[0] - self.padding[2] + cy = self.center_y + self.padding[3] - self.padding[1] + sign = +1.0 + angle_offset = radians(self.start_angle) + if self.direction == "cw": + angle_offset = 2 * pi - angle_offset + sign = -1.0 + quota = 2 * pi / self.items + mult_quota = 2 * pi / self.shown_items + angle = angle_offset + n * sign * quota + + if self.items == self.shown_items: + angle += quota / 2 + else: + angle -= mult_quota / 2 + + # kived: looking it up, yes. x = cos(angle) * radius + centerx; + # y = sin(angle) * radius + centery + x = cos(angle) * middle_r + cx + y = sin(angle) * middle_r + cy + + return x, y + + def number_at_pos(self, x, y): + """Returns the number at a given x, y position. The number is found + using the widget's center as a starting point for angle calculations. + + Not thoroughly tested, may yield wrong results. + """ + if self.items == 0: + return self.min + cx = self.center_x + self.padding[0] - self.padding[2] + cy = self.center_y + self.padding[3] - self.padding[1] + lx = x - cx + ly = y - cy + quota = 2 * pi / self.items + mult_quota = 2 * pi / self.shown_items + if lx == 0 and ly > 0: + angle = pi / 2 + elif lx == 0 and ly < 0: + angle = 3 * pi / 2 + else: + angle = atan(ly / lx) + if lx < 0 < ly: + angle += pi + if lx > 0 > ly: + angle += 2 * pi + if lx < 0 and ly < 0: + angle += pi + angle += radians(self.start_angle) + if self.direction == "cw": + angle = 2 * pi - angle + if mult_quota != quota: + angle -= mult_quota / 2 + if angle < 0: + angle += 2 * pi + elif angle > 2 * pi: + angle -= 2 * pi + + return int(angle / quota) + self.min + + +class CircularMinutePicker(CircularNumberPicker): + """:class:`CircularNumberPicker` implementation for minutes.""" + + def __init__(self, **kw): + super().__init__(**kw) + self.min = 0 + self.max = 60 + self.multiples_of = 5 + self.number_format_string = "{:02d}" + self.direction = "cw" + self.bind(shown_items=self._update_start_angle) + Clock.schedule_once(self._update_start_angle) + Clock.schedule_once(self.on_selected) + + def _update_start_angle(self, *a): + self.start_angle = -(360.0 / self.shown_items / 2) - 90 + + +class CircularHourPicker(CircularNumberPicker): + """:class:`CircularNumberPicker` implementation for hours.""" + + # military = BooleanProperty(False) + + def __init__(self, **kw): + super().__init__(**kw) + self.min = 1 + self.max = 13 + # 25 if self.military else 13 + # self.inner_radius_hint = .8 if self.military else .6 + self.multiples_of = 1 + self.number_format_string = "{}" + self.direction = "cw" + self.bind(shown_items=self._update_start_angle) + # self.bind(military=lambda v: setattr(self, "max", 25 if v else 13)) + # self.bind(military=lambda v: setattr( + # self, "inner_radius_hint", .8 if self.military else .6)) + # Clock.schedule_once(self._genitems) + Clock.schedule_once(self._update_start_angle) + Clock.schedule_once(self.on_selected) + + def _update_start_angle(self, *a): + self.start_angle = (360.0 / self.shown_items / 2) - 90 + + +class CircularTimePicker(BoxLayout, ThemableBehavior): + """Widget that makes use of :class:`CircularHourPicker` and + :class:`CircularMinutePicker` to create a user-friendly, animated + time picker like the one seen on Android. + + See module documentation for more details. + """ + + primary_dark = ListProperty([1, 1, 1]) + + hours = NumericProperty(0) + """The hours, in military format (0-23). + + :attr:`hours` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0 (12am). + """ + + minutes = NumericProperty(0) + """The minutes. + + :attr:`minutes` is a :class:`~kivy.properties.NumericProperty` and + defaults to 0. + """ + + time_list = ReferenceListProperty(hours, minutes) + """Packs :attr:`hours` and :attr:`minutes` in a list for convenience. + + :attr:`time_list` is a :class:`~kivy.properties.ReferenceListProperty`. + """ + + # military = BooleanProperty(False) + time_format = StringProperty( + "[color={hours_color}][ref=hours]{hours}[/ref][/color][color={primary_dark}][ref=colon]:[/ref][/color]\ +[color={minutes_color}][ref=minutes]{minutes:02d}[/ref][/color]" + ) + """String that will be formatted with the time and shown in the time label. + Can be anything supported by :meth:`str.format`. Make sure you don't + remove the refs. See the default for the arguments passed to format. + :attr:`time_format` is a :class:`~kivy.properties.StringProperty` and + defaults to "[color={hours_color}][ref=hours]{hours}[/ref][/color]:[color={minutes_color}][ref=minutes]\ + {minutes:02d}[/ref][/color]". + """ + + ampm_format = StringProperty( + "[color={am_color}][ref=am]AM[/ref][/color]\n" + "[color={pm_color}][ref=pm]PM[/ref][/color]" + ) + """String that will be formatted and shown in the AM/PM label. + Can be anything supported by :meth:`str.format`. Make sure you don't + remove the refs. See the default for the arguments passed to format. + + :attr:`ampm_format` is a :class:`~kivy.properties.StringProperty` and + defaults to "[color={am_color}][ref=am]AM[/ref][/color]\n + [color={pm_color}][ref=pm]PM[/ref][/color]". + """ + + picker = OptionProperty("hours", options=("minutes", "hours")) + """Currently shown time picker. Can be one of "minutes", "hours". + + :attr:`picker` is a :class:`~kivy.properties.OptionProperty` and + defaults to "hours". + """ + + # selector_color = ListProperty([.337, .439, .490]) + selector_color = ListProperty([0, 0, 0]) + """Color of the number selector and of the highlighted text. RGB. + + :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and + defaults to [.337, .439, .490] (material green). + """ + + color = ListProperty([1, 1, 1]) + """Color of the number labels and of the center dot. RGB. + + :attr:`color` is a :class:`~kivy.properties.ListProperty` and + defaults to [1, 1, 1] (white). + """ + + selector_alpha = BoundedNumericProperty(0.3, min=0, max=1) + """Alpha value for the transparent parts of the selector. + + :attr:`selector_alpha` + is a :class:`~kivy.properties.BoundedNumericProperty` and + defaults to 0.3 (min=0, max=1). + """ + + _am = BooleanProperty(True) + _h_picker = ObjectProperty(None) + _m_picker = ObjectProperty(None) + _bound = DictProperty({}) + + def _get_time(self): + try: + return datetime.time(*self.time_list) + except ValueError: + self.time_list = [self.hours, 0] + return datetime.time(*self.time_list) + + def set_time(self, dt): + if dt.hour >= 12: + dt.strftime("%I:%M") + self._am = False + self.time_list = [dt.hour, dt.minute] + + time = AliasProperty(_get_time, set_time, bind=("time_list",)) + """Selected time as a datetime.time object. + + :attr:`time` is an :class:`~kivy.properties.AliasProperty`. + """ + + def _get_picker(self): + if self.picker == "hours": + return self._h_picker + return self._m_picker + + _picker = AliasProperty(_get_picker, None) + + def _get_time_text(self): + hc = ( + rgb_to_hex(0, 0, 0) + if self.picker == "hours" + else rgb_to_hex(*self.primary_dark) + ) + mc = ( + rgb_to_hex(0, 0, 0) + if self.picker == "minutes" + else rgb_to_hex(*self.primary_dark) + ) + h = ( + self.hours == 0 + and 12 + or self.hours <= 12 + and self.hours + or self.hours - 12 + ) + m = self.minutes + primary_dark = rgb_to_hex(*self.primary_dark) + return self.time_format.format( + hours_color=hc, + minutes_color=mc, + hours=h, + minutes=m, + primary_dark=primary_dark, + ) + + time_text = AliasProperty( + _get_time_text, None, bind=("hours", "minutes", "time_format", "picker") + ) + + def _get_ampm_text(self, *args): + amc = ( + rgb_to_hex(0, 0, 0) if self._am else rgb_to_hex(*self.primary_dark) + ) + pmc = ( + rgb_to_hex(0, 0, 0) + if not self._am + else rgb_to_hex(*self.primary_dark) + ) + return self.ampm_format.format(am_color=amc, pm_color=pmc) + + ampm_text = AliasProperty( + _get_ampm_text, None, bind=("hours", "ampm_format", "_am") + ) + + def __init__(self, **kw): + super().__init__(**kw) + self.selector_color = ( + self.theme_cls.primary_color[0], + self.theme_cls.primary_color[1], + self.theme_cls.primary_color[2], + ) + self.color = self.theme_cls.text_color + self.primary_dark = ( + self.theme_cls.primary_dark[0] / 2, + self.theme_cls.primary_dark[1] / 2, + self.theme_cls.primary_dark[2] / 2, + ) + self.on_ampm() + if self.hours >= 12: + self._am = False + self.bind( + time_list=self.on_time_list, + picker=self._switch_picker, + _am=self.on_ampm, + primary_dark=self._get_ampm_text, + ) + self._h_picker = CircularHourPicker() + self.h_picker_touch = False + self._m_picker = CircularMinutePicker() + self.animating = False + Clock.schedule_once(self.on_selected) + Clock.schedule_once(self.on_time_list) + Clock.schedule_once(self._init_later) + Clock.schedule_once(lambda *a: self._switch_picker(noanim=True)) + + def _init_later(self, *args): + self.ids.timelabel.bind(on_ref_press=self.on_ref_press) + self.ids.ampmlabel.bind(on_ref_press=self.on_ref_press) + + def on_ref_press(self, ign, ref): + if not self.animating: + if ref == "hours": + self.picker = "hours" + elif ref == "minutes": + self.picker = "minutes" + if ref == "am": + self._am = True + elif ref == "pm": + self._am = False + + def on_selected(self, *a): + if not self._picker: + return + if self.picker == "hours": + hours = ( + self._picker.selected + if self._am + else self._picker.selected + 12 + ) + if hours == 24 and not self._am: + hours = 12 + elif hours == 12 and self._am: + hours = 0 + self.hours = hours + elif self.picker == "minutes": + self.minutes = self._picker.selected + + def on_time_list(self, *a): + if not self._picker: + return + self._h_picker.selected = ( + self.hours == 0 and 12 or self._am and self.hours or self.hours - 12 + ) + self._m_picker.selected = self.minutes + self.on_selected() + + def on_ampm(self, *a): + if self._am: + self.hours = self.hours if self.hours < 12 else self.hours - 12 + else: + self.hours = self.hours if self.hours >= 12 else self.hours + 12 + + def is_animating(self, *args): + self.animating = True + + def is_not_animating(self, *args): + self.animating = False + + def on_touch_down(self, touch): + if not self._h_picker.collide_point(*touch.pos): + self.h_picker_touch = False + else: + self.h_picker_touch = True + super().on_touch_down(touch) + + def on_touch_up(self, touch): + try: + if not self.h_picker_touch: + return + if not self.animating: + if touch.grab_current is not self: + if self.picker == "hours": + self.picker = "minutes" + except AttributeError: + pass + super().on_touch_up(touch) + + def _switch_picker(self, *a, **kw): + noanim = "noanim" in kw + if noanim: + noanim = kw["noanim"] + + try: + container = self.ids.picker_container + except (AttributeError, NameError): + Clock.schedule_once(lambda *a: self._switch_picker(noanim=noanim)) + + if self.picker == "hours": + picker = self._h_picker + prevpicker = self._m_picker + elif self.picker == "minutes": + picker = self._m_picker + prevpicker = self._h_picker + + if len(self._bound) > 0: + prevpicker.unbind(selected=self.on_selected) + self.unbind(**self._bound) + picker.bind(selected=self.on_selected) + self._bound = { + "selector_color": picker.setter("selector_color"), + "color": picker.setter("color"), + "selector_alpha": picker.setter("selector_alpha"), + } + self.bind(**self._bound) + + if len(container._bound) > 0: + container.unbind(**container._bound) + container._bound = { + "size": picker.setter("size"), + "pos": picker.setter("pos"), + } + container.bind(**container._bound) + + picker.pos = container.pos + picker.size = container.size + picker.selector_color = self.selector_color + picker.color = self.color + picker.selector_alpha = self.selector_alpha + if noanim: + if prevpicker in container.children: + container.remove_widget(prevpicker) + if picker.parent: + picker.parent.remove_widget(picker) + container.add_widget(picker) + else: + self.is_animating() + if prevpicker in container.children: + anim = Animation(scale=1.5, d=0.5, t="in_back") & Animation( + opacity=0, d=0.5, t="in_cubic" + ) + anim.start(prevpicker) + Clock.schedule_once( + lambda *y: container.remove_widget(prevpicker), 0.5 + ) # .31) + picker.scale = 1.5 + picker.opacity = 0 + if picker.parent: + picker.parent.remove_widget(picker) + container.add_widget(picker) + anim = Animation(scale=1, d=0.5, t="out_back") & Animation( + opacity=1, d=0.5, t="out_cubic" + ) + anim.bind(on_complete=self.is_not_animating) + Clock.schedule_once(lambda *y: anim.start(picker), 0.3) + + +if __name__ == "__main__": + from kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + + class Example(MDApp): + def build(self): + box = MDScreen(md_bg_color=self.theme_cls.bg_darkest) + box.add_widget(CircularTimePicker()) + return box + + Example().run() diff --git a/main.py b/main.py index 82a7be6..108c8f3 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ import sqlite3 # Import Kivy modules #from kivy.app import App -from kivy.app import App +from kivymd.app import MDApp from kivy.lang import Builder from kivy.uix.screenmanager import Screen, ScreenManager @@ -47,12 +47,12 @@ class ScreenOne(Screen): class ScreenTwo(Screen): pass -class GlobalForest(App): +class GlobalForest(MDApp): def build(self): #return Label(text='Hello world') return GameWidget() -class MainApp(App): +class MainApp(MDApp): def build(self): pass #Window.clearcolor = (0, 0, 0, 0) diff --git a/requierments.txt b/requierments.txt index 0351b11..726f39c 100644 --- a/requierments.txt +++ b/requierments.txt @@ -1,6 +1,4 @@ ffpyplayer sqlite_utils Kivy -Kivy-examples -kivymd mapview diff --git a/treemarker.py b/treemarker.py new file mode 100644 index 0000000..9795d2f --- /dev/null +++ b/treemarker.py @@ -0,0 +1,11 @@ +from kivy_garden.mapview import MapMarkerPopup +from treepopupmenu import TreePopupMenu + +class TreeMarker(MapMarkerPopup): + tree_data = [] + + def on_release(self): + # Open the TreePopupMenu + menu = TreePopupMenu(self.tree_data) + menu.size_hint = [.8, .9] + menu.open() \ No newline at end of file diff --git a/treepopupmenu.py b/treepopupmenu.py new file mode 100644 index 0000000..dc5a03e --- /dev/null +++ b/treepopupmenu.py @@ -0,0 +1,14 @@ +from kivymd.uix.dialog2 import ListMDDialog + +class TreePopupMenu(ListMDDialog): + def __init__(self, tree_data): + super().__init__() + + # Set all of the fields of tree data + headers = "Name,Lat,Lon,Description" + headers = headers.split(',') + + for i in range(len(headers)): + attribute_name = headers[i] + attribute_value = tree_data[i] + setattr(self, attribute_name, attribute_value) \ No newline at end of file