diff --git a/.gitignore b/.gitignore index cdf3e1c..dc0790b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,19 @@ +sbapp/.buildozer +sbapp/buildozer.spec +sbapp/requirements.txt +sbapp/venv +sbapp/bin +sbapp/app_storage +sbapp/RNS +sbapp/LXMF +sbapp/precompiled +sbapp/*.DS_Store +sbapp/*.pyc +sbapp/build +sbapp/dist +sbapp/docs/build +sbapp/sideband*.egg-info + .buildozer buildozer.spec requirements.txt diff --git a/Makefile b/Makefile index 25f6b78..63ceaef 100644 --- a/Makefile +++ b/Makefile @@ -1,35 +1,21 @@ -all: prepare debug +devapk: + make -C sbapp devapk -prepare: activate - -clean: - buildozer android clean - -activate: - (. venv/bin/activate) - (mv setup.py setup.disabled) - -debug: - buildozer android debug - -release: - buildozer android release - -postbuild: - (mv setup.disabled setup.py) - -apk: prepare release postbuild - -devapk: prepare debug postbuild +apk: + make -C sbapp apk install: - adb install bin/sideband-0.1.6-arm64-v8a-debug.apk - -install-release: - adb install bin/sideband-0.1.6-arm64-v8a-release.apk + make -C sbapp install console: - (adb logcat | grep python) + make -C sbapp conole -getrns: - (rm ./RNS -r;cp -rv ../Reticulum/RNS ./;rm ./RNS/Utilities/RNS;rm ./RNS/__pycache__ -r) +clean: + @echo Cleaning... + -rm -r ./build + -rm -r ./dist + +build_wheel: + python3 setup.py sdist bdist_wheel + +release: build_wheel apk \ No newline at end of file diff --git a/sbapp/Makefile b/sbapp/Makefile new file mode 100644 index 0000000..4a08167 --- /dev/null +++ b/sbapp/Makefile @@ -0,0 +1,38 @@ +all: prepare debug + +prepare: activate cleanrns getrns + +clean: + buildozer android clean + -(rm ./__pycache__ -r) + -(rm ./app_storage -r) + -(rm ./bin -r) + +activate: + (. venv/bin/activate) + +debug: + buildozer android debug + +release: + buildozer android release + +postbuild: + cleanrns + @echo Done + +apk: prepare release postbuild + +devapk: prepare debug postbuild + +install: + adb install bin/sideband-0.1.6-arm64-v8a-release.apk + +console: + (adb logcat | grep python) + +getrns: + (cp -rv ../../Reticulum/RNS ./;rm ./RNS/Utilities/RNS;rm ./RNS/__pycache__ -r) + +cleanrns: + -(rm ./RNS -r) diff --git a/sbapp/__init__.py b/sbapp/__init__.py new file mode 100644 index 0000000..f49e360 --- /dev/null +++ b/sbapp/__init__.py @@ -0,0 +1,5 @@ +import os +import glob + +modules = glob.glob(os.path.dirname(__file__)+"/*.py") +__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')] \ No newline at end of file diff --git a/assets/icon.png b/sbapp/assets/icon.png similarity index 100% rename from assets/icon.png rename to sbapp/assets/icon.png diff --git a/assets/presplash.png b/sbapp/assets/presplash.png similarity index 100% rename from assets/presplash.png rename to sbapp/assets/presplash.png diff --git a/assets/presplash_small.png b/sbapp/assets/presplash_small.png similarity index 100% rename from assets/presplash_small.png rename to sbapp/assets/presplash_small.png diff --git a/sbapp/kivymd/__init__.py b/sbapp/kivymd/__init__.py new file mode 100644 index 0000000..f94c63c --- /dev/null +++ b/sbapp/kivymd/__init__.py @@ -0,0 +1,71 @@ +""" +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 + +import kivy +from kivy.logger import Logger + +__version__ = "1.0.0.dev0" +"""KivyMD version.""" + +release = False +kivy.require("2.0.0") + +try: + from kivymd._version import __date__, __hash__, __short_hash__ +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.""" + +uix_path = os.path.join(path, "uix") +"""Path to uix 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/sbapp/kivymd/_version.py b/sbapp/kivymd/_version.py new file mode 100644 index 0000000..5faa449 --- /dev/null +++ b/sbapp/kivymd/_version.py @@ -0,0 +1,5 @@ +# THIS FILE IS GENERATED FROM KIVYMD SETUP.PY +__version__ = '1.0.0.dev0' +__hash__ = '68ec8626a93b0e7f69e48d9755c4af70028f66a2' +__short_hash__ = '68ec862' +__date__ = '2022-07-07' diff --git a/sbapp/kivymd/app.py b/sbapp/kivymd/app.py new file mode 100644 index 0000000..abae5f0 --- /dev/null +++ b/sbapp/kivymd/app.py @@ -0,0 +1,133 @@ +""" +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 = ''' + MDScreen: + + 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",) + +import os + +from kivy.app import App +from kivy.lang import Builder +from kivy.logger import Logger +from kivy.properties import ObjectProperty + +from kivymd.theming import ThemeManager + + +class FpsMonitoring: + """Implements a monitor to display the current FPS in the toolbar.""" + + def fps_monitor_start(self) -> None: + """Adds a monitor to the main application window.""" + + from kivy.core.window import Window + + from kivymd.utils.fpsmonitor import FpsMonitor + + monitor = FpsMonitor() + monitor.start() + Window.add_widget(monitor) + + +class MDApp(App, FpsMonitoring): + """ + Application class, see :class:`~kivy.app.App` class documentation for more + information. + """ + + 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() + + def load_all_kv_files(self, path_to_directory: str) -> None: + """ + Recursively loads KV files from the selected directory. + + .. versionadded:: 1.0.0 + """ + + for path_to_dir, dirs, files in os.walk(path_to_directory): + # When using the `load_all_kv_files` method, all KV files + # from the `KivyMD` library were loaded twice, which leads to + # failures when using application built using `PyInstaller`. + if "kivymd" in path_to_directory: + Logger.critical( + "KivyMD: " + "Do not use the word 'kivymd' in the name of the directory " + "from where you download KV files" + ) + if ( + "venv" in path_to_dir + or ".buildozer" in path_to_dir + or os.path.join("kivymd") in path_to_dir + ): + continue + for name_file in files: + if ( + os.path.splitext(name_file)[1] == ".kv" + and name_file != "style.kv" # if use PyInstaller + and "__MACOS" not in path_to_dir # if use Mac OS + ): + path_to_kv_file = os.path.join(path_to_dir, name_file) + Builder.load_file(path_to_kv_file) diff --git a/sbapp/kivymd/color_definitions.py b/sbapp/kivymd/color_definitions.py new file mode 100755 index 0000000..47a2edf --- /dev/null +++ b/sbapp/kivymd/color_definitions.py @@ -0,0 +1,954 @@ +""" +Themes/Color Definitions +======================== + +.. seealso:: + + `Material Design spec, The color system `_ + + `Material Design spec, The color tool `_ + +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": "D500F9", + "A700": "AA00FF", + }, + "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.properties import ListProperty, StringProperty + + from kivymd.color_definitions import colors + from kivymd.uix.tab import MDTabsBase + from kivymd.uix.boxlayout import MDBoxLayout + + demo = ''' + + orientation: 'vertical' + + MDTopAppBar: + 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 + + RecycleView: + id: rv + key_viewclass: "viewclass" + key_size: "height" + + RecycleBoxLayout: + default_size: None, dp(48) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: "vertical" + + + + size_hint_y: None + height: "42dp" + + MDLabel: + text: root.text + halign: "center" + + + + ''' + + from kivy.factory import Factory + + from kivymd.app import MDApp + + + class Tab(MDBoxLayout, MDTabsBase): + pass + + + class ItemColor(MDBoxLayout): + 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.rv.data = [] + if not tab_text: + tab_text = 'Red' + for value_color in colors[tab_text]: + self.screen.ids.rv.data.append( + { + "viewclass": "ItemColor", + "md_bg_color": 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": ["50", "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/sbapp/kivymd/effects/__init__.py b/sbapp/kivymd/effects/__init__.py new file mode 100644 index 0000000..6da0851 --- /dev/null +++ b/sbapp/kivymd/effects/__init__.py @@ -0,0 +1,4 @@ +""" +Effects +======= +""" diff --git a/sbapp/kivymd/effects/fadingedge/__init__.py b/sbapp/kivymd/effects/fadingedge/__init__.py new file mode 100644 index 0000000..d2905b0 --- /dev/null +++ b/sbapp/kivymd/effects/fadingedge/__init__.py @@ -0,0 +1 @@ +from .fadingedge import FadingEdgeEffect diff --git a/sbapp/kivymd/effects/fadingedge/fadingedge.py b/sbapp/kivymd/effects/fadingedge/fadingedge.py new file mode 100644 index 0000000..1af776a --- /dev/null +++ b/sbapp/kivymd/effects/fadingedge/fadingedge.py @@ -0,0 +1,197 @@ +""" +Effects/FadingEdgeEffect +======================== + +.. versionadded:: 1.0.0 + +The `FadingEdgeEffect` class implements a fade effect for `KivyMD` widgets: + +.. code-block:: python + + from kivy.lang import Builder + from kivy.uix.scrollview import ScrollView + + from kivymd.app import MDApp + from kivymd.effects.fadingedge.fadingedge import FadingEdgeEffect + from kivymd.uix.list import OneLineListItem + + KV = ''' + MDScreen: + + FadeScrollView: + fade_height: self.height / 2 + fade_color: root.md_bg_color + + MDList: + id: container + ''' + + + class FadeScrollView(FadingEdgeEffect, ScrollView): + pass + + + 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/fading-edge-effect-white.gif + :align: center + +.. note:: Use the same color value for the fade_color parameter as for the + parent widget. +""" + +from typing import Union + +from kivy.clock import Clock +from kivy.graphics.context_instructions import Color +from kivy.graphics.vertex_instructions import Rectangle +from kivy.metrics import dp +from kivy.properties import BooleanProperty, ColorProperty, NumericProperty + +from kivymd.theming import ThemableBehavior + +__all_ = ("FadingEdgeEffect",) + + +class FadingEdgeEffect(ThemableBehavior): + """ + The class implements the fade effect. + + .. versionadded:: 1.0.0 + """ + + fade_color = ColorProperty(None) + """ + Fade color. + + :attr:`fade_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + fade_height = NumericProperty(0) + """ + Fade height. + + :attr:`fade_height` is an :class:`~kivy.properties.ColorProperty` + and defaults to `0`. + """ + + edge_top = BooleanProperty(True) + """ + Display fade edge top. + + :attr:`edge_top` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + edge_bottom = BooleanProperty(True) + """ + Display fade edge bottom. + + :attr:`edge_bottom` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + _height_segment = 10 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self.set_fade) + + # TODO: Perhaps it would be better if we used a Shader for the fade effect. + # But, I think the canvas instructions shouldn't affect performance + def set_fade(self, interval: Union[int, float]) -> None: + """Draws a bottom and top fade border on the canvas.""" + + fade_color = ( + self.theme_cls.primary_color + if not self.fade_color + else self.fade_color + ) + height_segment = ( + self.fade_height if self.fade_height else dp(100) + ) // self._height_segment + alpha = 1.1 + + with self.canvas: + for i in range(self._height_segment): + alpha -= 0.1 + + Color(rgba=(fade_color[:-1] + [round(alpha, 1)])) + rectangle_top = ( + Rectangle( + pos=(self.x, self.height - (i * height_segment)), + size=(self.width, height_segment), + ) + if self.edge_top + else None + ) + rectangle_bottom = ( + Rectangle( + pos=(self.x, i * height_segment), + size=(self.width, height_segment), + ) + if self.edge_bottom + else None + ) + # How I hate lambda functions because of their length :( + # But I don’t want to call the arguments by short, + # incomprehensible names 'a', 'b', 'c'. + self.bind( + pos=lambda instance_fadind_edge_effect, window_size, rectangle_top=rectangle_top, rectangle_bottom=rectangle_bottom, index=i: self.update_canvas( + instance_fadind_edge_effect, + window_size, + rectangle_top, + rectangle_bottom, + index, + ), + size=lambda instance_fadind_edge_effect, window_size, rectangle_top=rectangle_top, rectangle_bottom=rectangle_bottom, index=i: self.update_canvas( + instance_fadind_edge_effect, + window_size, + rectangle_top, + rectangle_bottom, + index, + ), + ) + + def update_canvas( + self, + instance_fadind_edge_effect, + size: list[int, int], + rectangle_top: Rectangle, + rectangle_bottom: Rectangle, + index: int, + ) -> None: + """ + Updates the position and size of the fade border on the canvas. + Called when the application screen is resized. + """ + + height_segment = ( + self.fade_height if self.fade_height else dp(100) + ) // self._height_segment + + if rectangle_top: + rectangle_top.pos = ( + instance_fadind_edge_effect.x, + size[1] + - (index * height_segment - instance_fadind_edge_effect.y), + ) + rectangle_top.size = (size[0], height_segment) + if rectangle_bottom: + rectangle_bottom.pos = ( + instance_fadind_edge_effect.x, + index * height_segment + instance_fadind_edge_effect.y, + ) + rectangle_bottom.size = (size[0], height_segment) diff --git a/sbapp/kivymd/effects/roulettescroll/LICENSE b/sbapp/kivymd/effects/roulettescroll/LICENSE new file mode 100644 index 0000000..6ba990b --- /dev/null +++ b/sbapp/kivymd/effects/roulettescroll/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2010-2021 Kivy Team and other contributors + +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/sbapp/kivymd/effects/roulettescroll/README.md b/sbapp/kivymd/effects/roulettescroll/README.md new file mode 100644 index 0000000..299404f --- /dev/null +++ b/sbapp/kivymd/effects/roulettescroll/README.md @@ -0,0 +1,58 @@ +RouletteScrollEffect +=================== + +This is a subclass of `kivy.effects.ScrollEffect` that simulates the +motion of a roulette, or a notched wheel (think Wheel of Fortune). It is +primarily designed for emulating the effect of the iOS and android date pickers. + +Usage +----- + +Here's an example of using `RouletteScrollEffect` for a `kivy.uix.scrollview.ScrollView`: + +```python + from kivy.uix.gridlayout import GridLayout + from kivy.uix.button import Button + from kivy.uix.scrollview import ScrollView + + # Preparing a `GridLayout` inside a `ScrollView`. + layout = GridLayout(cols=1, padding=10, size_hint=(None, None), width=500) + layout.bind(minimum_height=layout.setter('height')) + + for i in range(30): + btn = Button(text=str(i), size=(480, 40), size_hint=(None, None)) + layout.add_widget(btn) + + root = ScrollView( + size_hint=(None, None), + size=(500, 320), + pos_hint={'center_x': .5, 'center_y': .5}, + do_scroll_x=False, + ) + root.add_widget(layout) + + # Preparation complete. Now add the new scroll effect. + root.effect_y = RouletteScrollEffect(anchor=20, interval=40) + runTouchApp(root) +``` + +Here the `ScrollView` scrolls through a series of buttons with height `40`. We then attached a `RouletteScrollEffect` with interval 40, +corresponding to the button heights. This allows the scrolling to stop at +the same offset no matter where it stops. The `RouletteScrollEffect.anchor` +adjusts this offset. + +Customizations +-------------- + +Other settings that can be played with include: + +- `RouletteScrollEffect.pull_duration` +- `RouletteScrollEffect.coasting_alpha` +- `RouletteScrollEffect.pull_back_velocity` +- `RouletteScrollEffect.terminal_velocity` + +See their module documentations for details. + +`RouletteScrollEffect` has one event ``on_coasted_to_stop`` that +is fired when the roulette stops, "making a selection". It can be listened to +for handling or cleaning up choice making. \ No newline at end of file diff --git a/sbapp/kivymd/effects/roulettescroll/__init__.py b/sbapp/kivymd/effects/roulettescroll/__init__.py new file mode 100644 index 0000000..2fbddae --- /dev/null +++ b/sbapp/kivymd/effects/roulettescroll/__init__.py @@ -0,0 +1 @@ +from .roulettescroll import RouletteScrollEffect diff --git a/sbapp/kivymd/effects/roulettescroll/roulettescroll.py b/sbapp/kivymd/effects/roulettescroll/roulettescroll.py new file mode 100644 index 0000000..528f5ac --- /dev/null +++ b/sbapp/kivymd/effects/roulettescroll/roulettescroll.py @@ -0,0 +1,251 @@ +""" +Effects/RouletteScrollEffect +============================ + +This is a subclass of :class:`kivy.effects.ScrollEffect` that simulates the +motion of a roulette, or a notched wheel (think Wheel of Fortune). It is +primarily designed for emulating the effect of the iOS and android date pickers. + +Usage +----- + +Here's an example of using :class:`RouletteScrollEffect` for a +:class:`kivy.uix.scrollview.ScrollView`: + +.. code-block:: python + + from kivy.uix.gridlayout import GridLayout + from kivy.uix.button import Button + from kivy.uix.scrollview import ScrollView + + # Preparing a `GridLayout` inside a `ScrollView`. + layout = GridLayout(cols=1, padding=10, size_hint=(None, None), width=500) + layout.bind(minimum_height=layout.setter('height')) + + for i in range(30): + btn = Button(text=str(i), size=(480, 40), size_hint=(None, None)) + layout.add_widget(btn) + + root = ScrollView( + size_hint=(None, None), + size=(500, 320), + pos_hint={'center_x': .5, 'center_y': .5}, + do_scroll_x=False, + ) + root.add_widget(layout) + + # Preparation complete. Now add the new scroll effect. + root.effect_y = RouletteScrollEffect(anchor=20, interval=40) + runTouchApp(root) + +Here the :class:`ScrollView` scrolls through a series of buttons with height +40. We then attached a :class:`RouletteScrollEffect` with interval 40, +corresponding to the button heights. This allows the scrolling to stop at +the same offset no matter where it stops. The :attr:`RouletteScrollEffect.anchor` +adjusts this offset. + +Customizations +-------------- + +Other settings that can be played with include: + +:attr:`RouletteScrollEffect.pull_duration`, +:attr:`RouletteScrollEffect.coasting_alpha`, +:attr:`RouletteScrollEffect.pull_back_velocity`, and +:attr:`RouletteScrollEffect.terminal_velocity`. + +See their module documentations for details. + +:class:`RouletteScrollEffect` has one event ``on_coasted_to_stop`` that +is fired when the roulette stops, "making a selection". It can be listened to +for handling or cleaning up choice making. +""" + +from math import ceil, exp, floor + +from kivy.animation import Animation +from kivy.effects.scroll import ScrollEffect +from kivy.properties import AliasProperty, NumericProperty, ObjectProperty + +__all_ = ("RouletteScrollEffect",) + + +class RouletteScrollEffect(ScrollEffect): + """ + This is a subclass of :class:`kivy.effects.ScrollEffect` that simulates the + motion of a roulette, or a notched wheel (think Wheel of Fortune). It is + primarily designed for emulating the effect of the iOS and android date pickers. + + .. versionadded:: 0.104.2 + """ + + __events__ = ("on_coasted_to_stop",) + + drag_threshold = NumericProperty(0) + """ + Overrides :attr:`ScrollEffect.drag_threshold` to abolish drag threshold. + + .. note:: + If using this with a :class:`Roulette` or other :class:`Tickline` + subclasses, what matters is :attr:`Tickline.drag_threshold`, which + is passed to this attribute in the end. + + :attr:`drag_threshold` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + min = NumericProperty(-float("inf")) + max = NumericProperty(float("inf")) + + interval = NumericProperty(50) + """ + The interval of the values of the "roulette". + + :attr:`interval` is an :class:`~kivy.properties.NumericProperty` + and defaults to `50`. + """ + + anchor = NumericProperty(0) + """ + One of the valid stopping values. + + :attr:`anchor` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + pull_duration = NumericProperty(0.2) + """ + When movement slows around a stopping value, an animation is used + to pull it toward the nearest value. :attr:`pull_duration` is the duration + used for such an animation. + + :attr:`pull_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + coasting_alpha = NumericProperty(0.5) + """ + When within :attr:`coasting_alpha` * :attr:`interval` of the + next notch and velocity is below :attr:`terminal_velocity`, + coasting begins and will end on the next notch. + + :attr:`coasting_alpha` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.5`. + """ + + pull_back_velocity = NumericProperty("50sp") + """ + The velocity below which the scroll value will be drawn to the + *nearest* notch instead of the *next* notch in the direction travelled. + + :attr:`pull_back_velocity` is an :class:`~kivy.properties.NumericProperty` + and defaults to `50sp`. + """ + + _anim = ObjectProperty(None) + + def get_term_vel(self): + return ( + exp(self.friction) + * self.interval + * self.coasting_alpha + / self.pull_duration + ) + + def set_term_vel(self, val): + self.pull_duration = ( + exp(self.friction) * self.interval * self.coasting_alpha / val + ) + + terminal_velocity = AliasProperty( + get_term_vel, + set_term_vel, + bind=["interval", "coasting_alpha", "pull_duration", "friction"], + cache=True, + ) + """ + If velocity falls between :attr:`pull_back_velocity` and + :attr:`terminal velocity` then the movement will start to coast + to the next coming stopping value. + + :attr:`terminal_velocity` is computed from a set formula given + :attr:`interval`, :attr:`coasting_alpha`, :attr:`pull_duration`, + and :attr:`friction`. Setting :attr:`terminal_velocity` has the + effect of setting :attr:`pull_duration`. + """ + + def start(self, val, t=None): + if self._anim: + self._anim.stop(self) + return ScrollEffect.start(self, val, t=t) + + def on_notch(self, *args): + return (self.scroll - self.anchor) % self.interval == 0 + + def nearest_notch(self, *args): + interval = float(self.interval) + anchor = self.anchor + n = round((self.scroll - anchor) / interval) + return anchor + n * interval + + def next_notch(self, *args): + interval = float(self.interval) + anchor = self.anchor + round_ = ceil if self.velocity > 0 else floor + n = round_((self.scroll - anchor) / interval) + return anchor + n * interval + + def near_notch(self, d=0.01): + nearest = self.nearest_notch() + if abs((nearest - self.scroll) / self.interval) % 1 < d: + return nearest + else: + return None + + def near_next_notch(self, d=None): + d = d or self.coasting_alpha + next_ = self.next_notch() + if abs((next_ - self.scroll) / self.interval) % 1 < d: + return next_ + else: + return None + + def update_velocity(self, dt): + if self.is_manual: + return + velocity = self.velocity + t_velocity = self.terminal_velocity + next_ = self.near_next_notch() + pull_back_velocity = self.pull_back_velocity + if pull_back_velocity < abs(velocity) < t_velocity and next_: + duration = abs((next_ - self.scroll) / self.velocity) + anim = Animation( + scroll=next_, + duration=duration, + ) + self._anim = anim + anim.on_complete = self._coasted_to_stop + anim.start(self) + return + if abs(velocity) < pull_back_velocity and not self.on_notch(): + anim = Animation( + scroll=self.nearest_notch(), + duration=self.pull_duration, + t="in_out_circ", + ) + self._anim = anim + anim.on_complete = self._coasted_to_stop + anim.start(self) + else: + self.velocity -= self.velocity * self.friction + self.apply_distance(self.velocity * dt) + self.trigger_velocity_update() + + def on_coasted_to_stop(self, *args): + """ + This event fires when the roulette has stopped, `making a selection`. + """ + + def _coasted_to_stop(self, *args): + self.velocity = 0 + self.dispatch("on_coasted_to_stop") diff --git a/sbapp/kivymd/effects/stiffscroll/LICENSE b/sbapp/kivymd/effects/stiffscroll/LICENSE new file mode 100644 index 0000000..a23e5d8 --- /dev/null +++ b/sbapp/kivymd/effects/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/sbapp/kivymd/effects/stiffscroll/README.md b/sbapp/kivymd/effects/stiffscroll/README.md new file mode 100644 index 0000000..6730324 --- /dev/null +++ b/sbapp/kivymd/effects/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/sbapp/kivymd/effects/stiffscroll/__init__.py b/sbapp/kivymd/effects/stiffscroll/__init__.py new file mode 100644 index 0000000..909e3f1 --- /dev/null +++ b/sbapp/kivymd/effects/stiffscroll/__init__.py @@ -0,0 +1 @@ +from .stiffscroll import StiffScrollEffect diff --git a/sbapp/kivymd/effects/stiffscroll/stiffscroll.py b/sbapp/kivymd/effects/stiffscroll/stiffscroll.py new file mode 100644 index 0000000..7dfb3c4 --- /dev/null +++ b/sbapp/kivymd/effects/stiffscroll/stiffscroll.py @@ -0,0 +1,215 @@ +""" +Effects/StiffScrollEffect +========================= + +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/sbapp/kivymd/factory_registers.py b/sbapp/kivymd/factory_registers.py new file mode 100644 index 0000000..57efb1a --- /dev/null +++ b/sbapp/kivymd/factory_registers.py @@ -0,0 +1,110 @@ +""" +Register KivyMD widgets to use without import. +""" + +from kivy.factory import Factory + +register = Factory.register +register("MDSegmentedControl", module="kivymd.uix.segmentedcontrol") +register("MDSegmentedControlItem", module="kivymd.uix.segmentedcontrol") +register("MDSliverAppbar", module="kivymd.uix.sliverappbar") +register("MDSliverAppbarContent", module="kivymd.uix.sliverappbar") +register("MDSliverAppbarHeader", module="kivymd.uix.sliverappbar") +register("MDNavigationRail", module="kivymd.uix.navigationrail") +register("MDNavigationRailFabButton", module="kivymd.uix.navigationrail") +register("MDNavigationRailMenuButton", module="kivymd.uix.navigationrail") +register("MDSwiper", module="kivymd.uix.swiper") +register("MDCarousel", module="kivymd.uix.carousel") +register("MDWidget", module="kivymd.uix.widget") +register("MDFloatLayout", module="kivymd.uix.floatlayout") +register("MDAnchorLayout", module="kivymd.uix.anchorlayout") +register("MDScreen", module="kivymd.uix.screen") +register("MDScreenManager", module="kivymd.uix.screenmanager") +register("MDRecycleGridLayout", module="kivymd.uix.recyclegridlayout") +register("MDBoxLayout", module="kivymd.uix.boxlayout") +register("MDRelativeLayout", module="kivymd.uix.relativelayout") +register("MDGridLayout", module="kivymd.uix.gridlayout") +register("MDStackLayout", module="kivymd.uix.stacklayout") +register("MDExpansionPanel", module="kivymd.uix.expansionpanel") +register("MDExpansionPanelOneLine", module="kivymd.uix.expansionpanel") +register("MDExpansionPanelTwoLine", module="kivymd.uix.expansionpanel") +register("MDExpansionPanelThreeLine", module="kivymd.uix.expansionpanel") +register("FitImage", module="kivymd.utils.fitimage") +register("MDBackdrop", module="kivymd.uix.backdrop") +register("MDBanner", module="kivymd.uix.banner") +register("MDTooltip", module="kivymd.uix.tooltip") +register("MDBottomNavigation", module="kivymd.uix.bottomnavigation") +register("MDBottomNavigationItem", module="kivymd.uix.bottomnavigation") +register("MDToggleButton", module="kivymd.uix.behaviors.toggle_behavior") +register("MDFloatingActionButtonSpeedDial", module="kivymd.uix.button") +register("MDIconButton", module="kivymd.uix.button") +register("MDRoundImageButton", module="kivymd.uix.button") +register("MDFlatButton", module="kivymd.uix.button") +register("MDRaisedButton", module="kivymd.uix.button") +register("MDFloatingActionButton", module="kivymd.uix.button") +register("MDRectangleFlatButton", module="kivymd.uix.button") +register("MDTextButton", module="kivymd.uix.button") +register("MDCustomRoundIconButton", module="kivymd.uix.button") +register("MDRoundFlatButton", module="kivymd.uix.button") +register("MDFillRoundFlatButton", module="kivymd.uix.button") +register("MDRectangleFlatIconButton", module="kivymd.uix.button") +register("MDRoundFlatIconButton", module="kivymd.uix.button") +register("MDFillRoundFlatIconButton", module="kivymd.uix.button") +register("MDCard", module="kivymd.uix.card") +register("MDSeparator", module="kivymd.uix.card") +register("MDSelectionList", module="kivymd.uix.selection") +register("MDChip", module="kivymd.uix.chip") +register("MDChooseChip", module="kivymd.uix.chip") +register("MDSmartTile", module="kivymd.uix.imagelist") +register("SmartTileWithLabel", module="kivymd.uix.imagelist") +register("SmartTileWithStar", module="kivymd.uix.imagelist") +register("MDLabel", module="kivymd.uix.label") +register("MDIcon", module="kivymd.uix.label") +register("MDList", module="kivymd.uix.list") +register("ILeftBody", module="kivymd.uix.list") +register("ILeftBodyTouch", module="kivymd.uix.list") +register("IRightBody", module="kivymd.uix.list") +register("IRightBodyTouch", module="kivymd.uix.list") +register("ContainerSupport", module="kivymd.uix.list") +register("OneLineListItem", module="kivymd.uix.list") +register("TwoLineListItem", module="kivymd.uix.list") +register("ThreeLineListItem", module="kivymd.uix.list") +register("OneLineAvatarListItem", module="kivymd.uix.list") +register("TwoLineAvatarListItem", module="kivymd.uix.list") +register("ThreeLineAvatarListItem", module="kivymd.uix.list") +register("OneLineIconListItem", module="kivymd.uix.list") +register("TwoLineIconListItem", module="kivymd.uix.list") +register("ThreeLineIconListItem", module="kivymd.uix.list") +register("OneLineRightIconListItem", module="kivymd.uix.list") +register("TwoLineRightIconListItem", module="kivymd.uix.list") +register("ThreeLineRightIconListItem", module="kivymd.uix.list") +register("OneLineAvatarIconListItem", module="kivymd.uix.list") +register("TwoLineAvatarIconListItem", module="kivymd.uix.list") +register("ThreeLineAvatarIconListItem", module="kivymd.uix.list") +register("HoverBehavior", module="kivymd.uix.behaviors.hover_behavior") +register("FocusBehavior", module="kivymd.uix.behaviors.focus_behavior") +register("MagicBehavior", module="kivymd.uix.behaviors.magic_behavior") +register("MDNavigationDrawer", module="kivymd.uix.navigationdrawer") +register("MDNavigationLayout", module="kivymd.uix.navigationdrawer") +register("MDNavigationDrawerMenu", module="kivymd.uix.navigationdrawer") +register("MDNavigationDrawerHeader", module="kivymd.uix.navigationdrawer") +register("MDNavigationDrawerItem", module="kivymd.uix.navigationdrawer") +register("MDNavigationDrawerLabel", module="kivymd.uix.navigationdrawer") +register("MDNavigationDrawerDivider", module="kivymd.uix.navigationdrawer") +register("MDProgressBar", module="kivymd.uix.progressbar") +register("MDScrollViewRefreshLayout", module="kivymd.uix.refreshlayout") +register("MDCheckbox", module="kivymd.uix.selectioncontrol") +register("MDSwitch", module="kivymd.uix.selectioncontrol") +register("MDSlider", module="kivymd.uix.slider") +register("MDSpinner", module="kivymd.uix.spinner") +register("MDTabs", module="kivymd.uix.tab") +register("MDTextField", module="kivymd.uix.textfield") +register("MDTextFieldRound", module="kivymd.uix.textfield") +register("MDTextFieldRect", module="kivymd.uix.textfield") +register("MDToolbar", module="kivymd.uix.toolbar") +register("MDTopAppBar", module="kivymd.uix.toolbar") +register("MDBottomAppBar", module="kivymd.uix.toolbar") +register("MDDropDownItem", module="kivymd.uix.dropdownitem") +register("MDCircularLayout", module="kivymd.uix.circularlayout") +register("MDHeroFrom", module="kivymd.uix.hero") +register("MDHeroTo", module="kivymd.uix.hero") diff --git a/sbapp/kivymd/font_definitions.py b/sbapp/kivymd/font_definitions.py new file mode 100755 index 0000000..e7cc8e2 --- /dev/null +++ b/sbapp/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/sbapp/kivymd/fonts/Roboto-Black.ttf b/sbapp/kivymd/fonts/Roboto-Black.ttf new file mode 100644 index 0000000..689fe5c Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-Black.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-BlackItalic.ttf b/sbapp/kivymd/fonts/Roboto-BlackItalic.ttf new file mode 100644 index 0000000..0b4e0ee Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-BlackItalic.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-Bold.ttf b/sbapp/kivymd/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000..d3f01ad Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-Bold.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-BoldItalic.ttf b/sbapp/kivymd/fonts/Roboto-BoldItalic.ttf new file mode 100644 index 0000000..41cc1e7 Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-BoldItalic.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-Italic.ttf b/sbapp/kivymd/fonts/Roboto-Italic.ttf new file mode 100644 index 0000000..6a1cee5 Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-Italic.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-Light.ttf b/sbapp/kivymd/fonts/Roboto-Light.ttf new file mode 100644 index 0000000..219063a Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-Light.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-LightItalic.ttf b/sbapp/kivymd/fonts/Roboto-LightItalic.ttf new file mode 100644 index 0000000..0e81e87 Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-LightItalic.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-Medium.ttf b/sbapp/kivymd/fonts/Roboto-Medium.ttf new file mode 100644 index 0000000..1a7f3b0 Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-Medium.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-MediumItalic.ttf b/sbapp/kivymd/fonts/Roboto-MediumItalic.ttf new file mode 100644 index 0000000..0030295 Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-MediumItalic.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-Regular.ttf b/sbapp/kivymd/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000..2c97eea Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-Regular.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-Thin.ttf b/sbapp/kivymd/fonts/Roboto-Thin.ttf new file mode 100644 index 0000000..b74a4fd Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-Thin.ttf differ diff --git a/sbapp/kivymd/fonts/Roboto-ThinItalic.ttf b/sbapp/kivymd/fonts/Roboto-ThinItalic.ttf new file mode 100644 index 0000000..dd0ddb8 Binary files /dev/null and b/sbapp/kivymd/fonts/Roboto-ThinItalic.ttf differ diff --git a/sbapp/kivymd/fonts/materialdesignicons-webfont.ttf b/sbapp/kivymd/fonts/materialdesignicons-webfont.ttf new file mode 100644 index 0000000..162b7b6 Binary files /dev/null and b/sbapp/kivymd/fonts/materialdesignicons-webfont.ttf differ diff --git a/sbapp/kivymd/icon_definitions.py b/sbapp/kivymd/icon_definitions.py new file mode 100755 index 0000000..14cd8eb --- /dev/null +++ b/sbapp/kivymd/icon_definitions.py @@ -0,0 +1,7121 @@ +""" +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 6.9.96 + +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 + + + + + MDBoxLayout: + orientation: 'vertical' + spacing: dp(10) + padding: dp(20) + + MDBoxLayout: + adaptive_height: True + + 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 + +""" + +md_icons = { + "ab-testing": "\U000F01C9", + "abacus": "\U000F16E0", + "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-down": "\U000F1868", + "account-arrow-down-outline": "\U000F1869", + "account-arrow-left": "\U000F0B51", + "account-arrow-left-outline": "\U000F0B52", + "account-arrow-right": "\U000F0B53", + "account-arrow-right-outline": "\U000F0B54", + "account-arrow-up": "\U000F1867", + "account-arrow-up-outline": "\U000F186A", + "account-badge": "\U000F1B0A", + "account-badge-outline": "\U000F1B0B", + "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-cowboy-hat-outline": "\U000F17F3", + "account-details": "\U000F0631", + "account-details-outline": "\U000F1372", + "account-edit": "\U000F06BC", + "account-edit-outline": "\U000F0FFB", + "account-eye": "\U000F0420", + "account-eye-outline": "\U000F127B", + "account-filter": "\U000F0936", + "account-filter-outline": "\U000F0F9D", + "account-group": "\U000F0849", + "account-group-outline": "\U000F0B58", + "account-hard-hat": "\U000F05B5", + "account-hard-hat-outline": "\U000F1A1F", + "account-heart": "\U000F0899", + "account-heart-outline": "\U000F0BE3", + "account-injury": "\U000F1815", + "account-injury-outline": "\U000F1816", + "account-key": "\U000F000B", + "account-key-outline": "\U000F0BE4", + "account-lock": "\U000F115E", + "account-lock-open": "\U000F1960", + "account-lock-open-outline": "\U000F1961", + "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-off": "\U000F1AF1", + "account-network-off-outline": "\U000F1AF2", + "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-school": "\U000F1A20", + "account-school-outline": "\U000F1A21", + "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-sync": "\U000F191B", + "account-sync-outline": "\U000F191C", + "account-tie": "\U000F0CE3", + "account-tie-hat": "\U000F1898", + "account-tie-hat-outline": "\U000F1899", + "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-tie-woman": "\U000F1A8C", + "account-voice": "\U000F05CB", + "account-voice-off": "\U000F0ED4", + "account-wrench": "\U000F189A", + "account-wrench-outline": "\U000F189B", + "adjust": "\U000F001A", + "advertisements": "\U000F192A", + "advertisements-off": "\U000F192B", + "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-alert": "\U000F187A", + "airplane-check": "\U000F187B", + "airplane-clock": "\U000F187C", + "airplane-cog": "\U000F187D", + "airplane-edit": "\U000F187E", + "airplane-landing": "\U000F05D4", + "airplane-marker": "\U000F187F", + "airplane-minus": "\U000F1880", + "airplane-off": "\U000F001E", + "airplane-plus": "\U000F1881", + "airplane-remove": "\U000F1882", + "airplane-search": "\U000F1883", + "airplane-settings": "\U000F1884", + "airplane-takeoff": "\U000F05D5", + "airport": "\U000F084B", + "alarm": "\U000F0020", + "alarm-bell": "\U000F078E", + "alarm-check": "\U000F0021", + "alarm-light": "\U000F078F", + "alarm-light-off": "\U000F171E", + "alarm-light-off-outline": "\U000F171F", + "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-distribute": "\U000F1962", + "align-horizontal-left": "\U000F11C2", + "align-horizontal-right": "\U000F11C4", + "align-vertical-bottom": "\U000F11C5", + "align-vertical-center": "\U000F11C6", + "align-vertical-distribute": "\U000F1963", + "align-vertical-top": "\U000F11C7", + "all-inclusive": "\U000F06BE", + "all-inclusive-box": "\U000F188D", + "all-inclusive-box-outline": "\U000F188E", + "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", + "ambulance": "\U000F002F", + "ammunition": "\U000F0CE8", + "ampersand": "\U000F0A8D", + "amplifier": "\U000F0030", + "amplifier-off": "\U000F11B5", + "anchor": "\U000F0031", + "android": "\U000F0032", + "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-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": "\U000F08C6", + "application-array": "\U000F10F5", + "application-array-outline": "\U000F10F6", + "application-braces": "\U000F10F7", + "application-braces-outline": "\U000F10F8", + "application-brackets": "\U000F0C8B", + "application-brackets-outline": "\U000F0C8C", + "application-cog": "\U000F0675", + "application-cog-outline": "\U000F1577", + "application-edit": "\U000F00AE", + "application-edit-outline": "\U000F0619", + "application-export": "\U000F0DAD", + "application-import": "\U000F0DAE", + "application-outline": "\U000F0614", + "application-parentheses": "\U000F10F9", + "application-parentheses-outline": "\U000F10FA", + "application-settings": "\U000F0B60", + "application-settings-outline": "\U000F1555", + "application-variable": "\U000F10FB", + "application-variable-outline": "\U000F10FC", + "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-cancel": "\U000F174B", + "archive-cancel-outline": "\U000F174C", + "archive-check": "\U000F174D", + "archive-check-outline": "\U000F174E", + "archive-clock": "\U000F174F", + "archive-clock-outline": "\U000F1750", + "archive-cog": "\U000F1751", + "archive-cog-outline": "\U000F1752", + "archive-edit": "\U000F1753", + "archive-edit-outline": "\U000F1754", + "archive-eye": "\U000F1755", + "archive-eye-outline": "\U000F1756", + "archive-lock": "\U000F1757", + "archive-lock-open": "\U000F1758", + "archive-lock-open-outline": "\U000F1759", + "archive-lock-outline": "\U000F175A", + "archive-marker": "\U000F175B", + "archive-marker-outline": "\U000F175C", + "archive-minus": "\U000F175D", + "archive-minus-outline": "\U000F175E", + "archive-music": "\U000F175F", + "archive-music-outline": "\U000F1760", + "archive-off": "\U000F1761", + "archive-off-outline": "\U000F1762", + "archive-outline": "\U000F120E", + "archive-plus": "\U000F1763", + "archive-plus-outline": "\U000F1764", + "archive-refresh": "\U000F1765", + "archive-refresh-outline": "\U000F1766", + "archive-remove": "\U000F1767", + "archive-remove-outline": "\U000F1768", + "archive-search": "\U000F1769", + "archive-search-outline": "\U000F176A", + "archive-settings": "\U000F176B", + "archive-settings-outline": "\U000F176C", + "archive-star": "\U000F176D", + "archive-star-outline": "\U000F176E", + "archive-sync": "\U000F176F", + "archive-sync-outline": "\U000F1770", + "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-box": "\U000F1964", + "arrow-bottom-left-bold-box-outline": "\U000F1965", + "arrow-bottom-left-bold-outline": "\U000F09B7", + "arrow-bottom-left-thick": "\U000F09B8", + "arrow-bottom-left-thin": "\U000F19B6", + "arrow-bottom-left-thin-circle-outline": "\U000F1596", + "arrow-bottom-right": "\U000F0043", + "arrow-bottom-right-bold-box": "\U000F1966", + "arrow-bottom-right-bold-box-outline": "\U000F1967", + "arrow-bottom-right-bold-outline": "\U000F09B9", + "arrow-bottom-right-thick": "\U000F09BA", + "arrow-bottom-right-thin": "\U000F19B7", + "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-left": "\U000F17A1", + "arrow-down-left-bold": "\U000F17A2", + "arrow-down-right": "\U000F17A3", + "arrow-down-right-bold": "\U000F17A4", + "arrow-down-thick": "\U000F0046", + "arrow-down-thin": "\U000F19B3", + "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-bottom": "\U000F17A5", + "arrow-left-bottom-bold": "\U000F17A6", + "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": "\U000F19B1", + "arrow-left-thin-circle-outline": "\U000F159A", + "arrow-left-top": "\U000F17A7", + "arrow-left-top-bold": "\U000F17A8", + "arrow-projectile": "\U000F1840", + "arrow-projectile-multiple": "\U000F183F", + "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-bottom": "\U000F17A9", + "arrow-right-bottom-bold": "\U000F17AA", + "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": "\U000F19B0", + "arrow-right-thin-circle-outline": "\U000F1598", + "arrow-right-top": "\U000F17AB", + "arrow-right-top-bold": "\U000F17AC", + "arrow-split-horizontal": "\U000F093B", + "arrow-split-vertical": "\U000F093C", + "arrow-top-left": "\U000F005B", + "arrow-top-left-bold-box": "\U000F1968", + "arrow-top-left-bold-box-outline": "\U000F1969", + "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": "\U000F19B5", + "arrow-top-left-thin-circle-outline": "\U000F1593", + "arrow-top-right": "\U000F005C", + "arrow-top-right-bold-box": "\U000F196A", + "arrow-top-right-bold-box-outline": "\U000F196B", + "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": "\U000F19B4", + "arrow-top-right-thin-circle-outline": "\U000F1594", + "arrow-u-down-left": "\U000F17AD", + "arrow-u-down-left-bold": "\U000F17AE", + "arrow-u-down-right": "\U000F17AF", + "arrow-u-down-right-bold": "\U000F17B0", + "arrow-u-left-bottom": "\U000F17B1", + "arrow-u-left-bottom-bold": "\U000F17B2", + "arrow-u-left-top": "\U000F17B3", + "arrow-u-left-top-bold": "\U000F17B4", + "arrow-u-right-bottom": "\U000F17B5", + "arrow-u-right-bottom-bold": "\U000F17B6", + "arrow-u-right-top": "\U000F17B7", + "arrow-u-right-top-bold": "\U000F17B8", + "arrow-u-up-left": "\U000F17B9", + "arrow-u-up-left-bold": "\U000F17BA", + "arrow-u-up-right": "\U000F17BB", + "arrow-u-up-right-bold": "\U000F17BC", + "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-left": "\U000F17BD", + "arrow-up-left-bold": "\U000F17BE", + "arrow-up-right": "\U000F17BF", + "arrow-up-right-bold": "\U000F17C0", + "arrow-up-thick": "\U000F005E", + "arrow-up-thin": "\U000F19B2", + "arrow-up-thin-circle-outline": "\U000F1597", + "arrow-vertical-lock": "\U000F115C", + "artstation": "\U000F0B5B", + "aspect-ratio": "\U000F0A24", + "assistant": "\U000F0064", + "asterisk": "\U000F06C4", + "asterisk-circle-outline": "\U000F1A27", + "at": "\U000F0065", + "atlassian": "\U000F0804", + "atm": "\U000F0D47", + "atom": "\U000F0768", + "atom-variant": "\U000F0E7B", + "attachment": "\U000F0066", + "attachment-check": "\U000F1AC1", + "attachment-lock": "\U000F19C4", + "attachment-minus": "\U000F1AC2", + "attachment-off": "\U000F1AC3", + "attachment-plus": "\U000F1AC4", + "attachment-remove": "\U000F1AC5", + "audio-input-rca": "\U000F186B", + "audio-input-stereo-minijack": "\U000F186C", + "audio-input-xlr": "\U000F186D", + "audio-video": "\U000F093D", + "audio-video-off": "\U000F11B6", + "augmented-reality": "\U000F0850", + "auto-download": "\U000F137E", + "auto-fix": "\U000F0068", + "auto-upload": "\U000F0069", + "autorenew": "\U000F006A", + "autorenew-off": "\U000F19E7", + "av-timer": "\U000F006B", + "aws": "\U000F0E0F", + "axe": "\U000F08C8", + "axe-battle": "\U000F1842", + "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-buggy-off": "\U000F1AF3", + "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-personal-tag": "\U000F1B0C", + "bag-personal-tag-outline": "\U000F1B0D", + "bag-suitcase": "\U000F158B", + "bag-suitcase-off": "\U000F158D", + "bag-suitcase-off-outline": "\U000F158E", + "bag-suitcase-outline": "\U000F158C", + "baguette": "\U000F0F3E", + "balcony": "\U000F1817", + "balloon": "\U000F0A26", + "ballot": "\U000F09C9", + "ballot-outline": "\U000F09CA", + "ballot-recount": "\U000F0C39", + "ballot-recount-outline": "\U000F0C3A", + "bandage": "\U000F0DAF", + "bank": "\U000F0070", + "bank-check": "\U000F1655", + "bank-minus": "\U000F0DB0", + "bank-off": "\U000F1656", + "bank-off-outline": "\U000F1657", + "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", + "barrel-outline": "\U000F1A28", + "baseball": "\U000F0852", + "baseball-bat": "\U000F0853", + "baseball-diamond": "\U000F15EC", + "baseball-diamond-outline": "\U000F15ED", + "bash": "\U000F1183", + "basket": "\U000F0076", + "basket-check": "\U000F18E5", + "basket-check-outline": "\U000F18E6", + "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", + "bathtub": "\U000F1818", + "bathtub-outline": "\U000F1819", + "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-arrow-down": "\U000F17DE", + "battery-arrow-down-outline": "\U000F17DF", + "battery-arrow-up": "\U000F17E0", + "battery-arrow-up-outline": "\U000F17E1", + "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-check": "\U000F17E2", + "battery-check-outline": "\U000F17E3", + "battery-clock": "\U000F19E5", + "battery-clock-outline": "\U000F19E6", + "battery-heart": "\U000F120F", + "battery-heart-outline": "\U000F1210", + "battery-heart-variant": "\U000F1211", + "battery-high": "\U000F12A3", + "battery-lock": "\U000F179C", + "battery-lock-open": "\U000F179D", + "battery-low": "\U000F12A1", + "battery-medium": "\U000F12A2", + "battery-minus": "\U000F17E4", + "battery-minus-outline": "\U000F17E5", + "battery-minus-variant": "\U000F008C", + "battery-negative": "\U000F008D", + "battery-off": "\U000F125D", + "battery-off-outline": "\U000F125E", + "battery-outline": "\U000F008E", + "battery-plus": "\U000F17E6", + "battery-plus-outline": "\U000F17E7", + "battery-plus-variant": "\U000F008F", + "battery-positive": "\U000F0090", + "battery-remove": "\U000F17E8", + "battery-remove-outline": "\U000F17E9", + "battery-sync": "\U000F1834", + "battery-sync-outline": "\U000F1835", + "battery-unknown": "\U000F0091", + "battery-unknown-bluetooth": "\U000F094A", + "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-badge": "\U000F116B", + "bell-badge-outline": "\U000F0178", + "bell-cancel": "\U000F13E7", + "bell-cancel-outline": "\U000F13E8", + "bell-check": "\U000F11E5", + "bell-check-outline": "\U000F11E6", + "bell-circle": "\U000F0D5A", + "bell-circle-outline": "\U000F0D5B", + "bell-cog": "\U000F1A29", + "bell-cog-outline": "\U000F1A2A", + "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-cargo": "\U000F189C", + "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-outline": "\U000F181A", + "blender-software": "\U000F00AB", + "blinds": "\U000F00AC", + "blinds-horizontal": "\U000F1A2B", + "blinds-horizontal-closed": "\U000F1A2C", + "blinds-open": "\U000F1011", + "blinds-vertical": "\U000F1A2D", + "blinds-vertical-closed": "\U000F1A2E", + "block-helper": "\U000F00AD", + "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", + "bolt": "\U000F0DB3", + "bomb": "\U000F0691", + "bomb-off": "\U000F06C5", + "bone": "\U000F00B9", + "bone-off": "\U000F19E0", + "book": "\U000F00BA", + "book-account": "\U000F13AD", + "book-account-outline": "\U000F13AE", + "book-alert": "\U000F167C", + "book-alert-outline": "\U000F167D", + "book-alphabet": "\U000F061D", + "book-arrow-down": "\U000F167E", + "book-arrow-down-outline": "\U000F167F", + "book-arrow-left": "\U000F1680", + "book-arrow-left-outline": "\U000F1681", + "book-arrow-right": "\U000F1682", + "book-arrow-right-outline": "\U000F1683", + "book-arrow-up": "\U000F1684", + "book-arrow-up-outline": "\U000F1685", + "book-cancel": "\U000F1686", + "book-cancel-outline": "\U000F1687", + "book-check": "\U000F14F3", + "book-check-outline": "\U000F14F4", + "book-clock": "\U000F1688", + "book-clock-outline": "\U000F1689", + "book-cog": "\U000F168A", + "book-cog-outline": "\U000F168B", + "book-cross": "\U000F00A2", + "book-edit": "\U000F168C", + "book-edit-outline": "\U000F168D", + "book-education": "\U000F16C9", + "book-education-outline": "\U000F16CA", + "book-heart": "\U000F1A1D", + "book-heart-outline": "\U000F1A1E", + "book-information-variant": "\U000F106F", + "book-lock": "\U000F079A", + "book-lock-open": "\U000F079B", + "book-lock-open-outline": "\U000F168E", + "book-lock-outline": "\U000F168F", + "book-marker": "\U000F1690", + "book-marker-outline": "\U000F1691", + "book-minus": "\U000F05D9", + "book-minus-multiple": "\U000F0A94", + "book-minus-multiple-outline": "\U000F090B", + "book-minus-outline": "\U000F1692", + "book-multiple": "\U000F00BB", + "book-multiple-outline": "\U000F0436", + "book-music": "\U000F0067", + "book-music-outline": "\U000F1693", + "book-off": "\U000F1694", + "book-off-outline": "\U000F1695", + "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-plus-outline": "\U000F1696", + "book-refresh": "\U000F1697", + "book-refresh-outline": "\U000F1698", + "book-remove": "\U000F0A97", + "book-remove-multiple": "\U000F0A96", + "book-remove-multiple-outline": "\U000F04CA", + "book-remove-outline": "\U000F1699", + "book-search": "\U000F0E84", + "book-search-outline": "\U000F0E85", + "book-settings": "\U000F169A", + "book-settings-outline": "\U000F169B", + "book-sync": "\U000F169C", + "book-sync-outline": "\U000F16C8", + "book-variant": "\U000F00BF", + "book-variant-multiple": "\U000F00BC", + "bookmark": "\U000F00C0", + "bookmark-box-multiple": "\U000F196C", + "bookmark-box-multiple-outline": "\U000F196D", + "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-arrow-down": "\U000F0E89", + "boom-gate-arrow-down-outline": "\U000F0E8A", + "boom-gate-arrow-up": "\U000F0E8C", + "boom-gate-arrow-up-outline": "\U000F0E8D", + "boom-gate-outline": "\U000F0E8B", + "boom-gate-up": "\U000F17F9", + "boom-gate-up-outline": "\U000F17FA", + "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-radius": "\U000F1AF4", + "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-arrow": "\U000F1841", + "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-arrow-left-right": "\U000F1A8D", + "briefcase-arrow-left-right-outline": "\U000F1A8E", + "briefcase-arrow-up-down": "\U000F1A8F", + "briefcase-arrow-up-down-outline": "\U000F1A90", + "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-eye": "\U000F17D9", + "briefcase-eye-outline": "\U000F17DA", + "briefcase-minus": "\U000F0A2A", + "briefcase-minus-outline": "\U000F0C3F", + "briefcase-off": "\U000F1658", + "briefcase-off-outline": "\U000F1659", + "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-off": "\U000F165A", + "briefcase-variant-off-outline": "\U000F165B", + "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", + "broadcast": "\U000F1720", + "broadcast-off": "\U000F1721", + "broom": "\U000F00E2", + "brush": "\U000F00E3", + "brush-off": "\U000F1771", + "brush-outline": "\U000F1A0D", + "brush-variant": "\U000F1813", + "bucket": "\U000F1415", + "bucket-outline": "\U000F1416", + "buffet": "\U000F0578", + "bug": "\U000F00E4", + "bug-check": "\U000F0A2E", + "bug-check-outline": "\U000F0A2F", + "bug-outline": "\U000F0A30", + "bug-pause": "\U000F1AF5", + "bug-pause-outline": "\U000F1AF6", + "bug-play": "\U000F1AF7", + "bug-play-outline": "\U000F1AF8", + "bug-stop": "\U000F1AF9", + "bug-stop-outline": "\U000F1AFA", + "bugle": "\U000F0DB4", + "bulkhead-light": "\U000F1A2F", + "bulldozer": "\U000F0B22", + "bullet": "\U000F0CF3", + "bulletin-board": "\U000F00E5", + "bullhorn": "\U000F00E6", + "bullhorn-outline": "\U000F0B23", + "bullhorn-variant": "\U000F196E", + "bullhorn-variant-outline": "\U000F196F", + "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-electric": "\U000F191D", + "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", + "button-cursor": "\U000F1B4F", + "button-pointer": "\U000F1B50", + "cabin-a-frame": "\U000F188C", + "cable-data": "\U000F1394", + "cached": "\U000F00E8", + "cactus": "\U000F0DB5", + "cake": "\U000F00E9", + "cake-layered": "\U000F00EA", + "cake-variant": "\U000F00EB", + "cake-variant-outline": "\U000F17F0", + "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-clock-outline": "\U000F16E1", + "calendar-collapse-horizontal": "\U000F189D", + "calendar-cursor": "\U000F157B", + "calendar-edit": "\U000F08A7", + "calendar-end": "\U000F166C", + "calendar-expand-horizontal": "\U000F189E", + "calendar-export": "\U000F0B24", + "calendar-filter": "\U000F1A32", + "calendar-filter-outline": "\U000F1A33", + "calendar-heart": "\U000F09D2", + "calendar-import": "\U000F0B25", + "calendar-lock": "\U000F1641", + "calendar-lock-outline": "\U000F1642", + "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-star-outline": "\U000F1B53", + "calendar-start": "\U000F166D", + "calendar-sync": "\U000F0E8E", + "calendar-sync-outline": "\U000F0E8F", + "calendar-text": "\U000F00F5", + "calendar-text-outline": "\U000F0C46", + "calendar-today": "\U000F00F6", + "calendar-today-outline": "\U000F1A30", + "calendar-week": "\U000F0A33", + "calendar-week-begin": "\U000F0A34", + "calendar-week-begin-outline": "\U000F1A31", + "calendar-week-outline": "\U000F1A34", + "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-document": "\U000F1871", + "camera-document-off": "\U000F1872", + "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-lock": "\U000F1A14", + "camera-lock-outline": "\U000F1A15", + "camera-marker": "\U000F19A7", + "camera-marker-outline": "\U000F19A8", + "camera-metering-center": "\U000F07A2", + "camera-metering-matrix": "\U000F07A3", + "camera-metering-partial": "\U000F07A4", + "camera-metering-spot": "\U000F07A5", + "camera-off": "\U000F05DF", + "camera-off-outline": "\U000F19BF", + "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", + "candelabra": "\U000F17D2", + "candelabra-fire": "\U000F17D3", + "candle": "\U000F05E2", + "candy": "\U000F1970", + "candy-off": "\U000F1971", + "candy-off-outline": "\U000F1972", + "candy-outline": "\U000F1973", + "candycane": "\U000F010A", + "cannabis": "\U000F07A6", + "cannabis-off": "\U000F166E", + "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-fluid-level": "\U000F1909", + "car-brake-hold": "\U000F0D5E", + "car-brake-low-pressure": "\U000F190A", + "car-brake-parking": "\U000F0D5F", + "car-brake-retarder": "\U000F1017", + "car-brake-temperature": "\U000F190B", + "car-brake-worn-linings": "\U000F190C", + "car-child-seat": "\U000F0FA3", + "car-clock": "\U000F1974", + "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-alert": "\U000F190D", + "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-select": "\U000F1879", + "car-settings": "\U000F13CD", + "car-shift-pattern": "\U000F0F40", + "car-side": "\U000F07AB", + "car-speed-limiter": "\U000F190E", + "car-sports": "\U000F07AC", + "car-tire-alert": "\U000F0C4D", + "car-traction-control": "\U000F0D64", + "car-turbocharger": "\U000F101A", + "car-wash": "\U000F010E", + "car-windshield": "\U000F101B", + "car-windshield-outline": "\U000F101C", + "car-wireless": "\U000F1878", + "car-wrench": "\U000F1814", + "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-multiple": "\U000F17F1", + "card-multiple-outline": "\U000F17F2", + "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-club-outline": "\U000F189F", + "cards-diamond": "\U000F08CF", + "cards-diamond-outline": "\U000F101D", + "cards-heart": "\U000F08D0", + "cards-heart-outline": "\U000F18A0", + "cards-outline": "\U000F0639", + "cards-playing": "\U000F18A1", + "cards-playing-club": "\U000F18A2", + "cards-playing-club-multiple": "\U000F18A3", + "cards-playing-club-multiple-outline": "\U000F18A4", + "cards-playing-club-outline": "\U000F18A5", + "cards-playing-diamond": "\U000F18A6", + "cards-playing-diamond-multiple": "\U000F18A7", + "cards-playing-diamond-multiple-outline": "\U000F18A8", + "cards-playing-diamond-outline": "\U000F18A9", + "cards-playing-heart": "\U000F18AA", + "cards-playing-heart-multiple": "\U000F18AB", + "cards-playing-heart-multiple-outline": "\U000F18AC", + "cards-playing-heart-outline": "\U000F18AD", + "cards-playing-outline": "\U000F063A", + "cards-playing-spade": "\U000F18AE", + "cards-playing-spade-multiple": "\U000F18AF", + "cards-playing-spade-multiple-outline": "\U000F18B0", + "cards-playing-spade-outline": "\U000F18B1", + "cards-spade": "\U000F08D1", + "cards-spade-outline": "\U000F18B2", + "cards-variant": "\U000F06C7", + "carrot": "\U000F010F", + "cart": "\U000F0110", + "cart-arrow-down": "\U000F0D66", + "cart-arrow-right": "\U000F0C4E", + "cart-arrow-up": "\U000F0D67", + "cart-check": "\U000F15EA", + "cart-heart": "\U000F18E0", + "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-clock": "\U000F1A91", + "cash-fast": "\U000F185C", + "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-sync": "\U000F1A92", + "cassette": "\U000F09D4", + "cast": "\U000F0118", + "cast-audio": "\U000F101E", + "cast-audio-variant": "\U000F1749", + "cast-connected": "\U000F0119", + "cast-education": "\U000F0E1D", + "cast-off": "\U000F078A", + "cast-variant": "\U000F001F", + "castle": "\U000F011A", + "cat": "\U000F011B", + "cctv": "\U000F07AE", + "cctv-off": "\U000F185F", + "ceiling-fan": "\U000F1797", + "ceiling-fan-light": "\U000F1798", + "ceiling-light": "\U000F0769", + "ceiling-light-multiple": "\U000F18DD", + "ceiling-light-multiple-outline": "\U000F18DE", + "ceiling-light-outline": "\U000F17C7", + "cellphone": "\U000F011C", + "cellphone-arrow-down": "\U000F09D5", + "cellphone-arrow-down-variant": "\U000F19C5", + "cellphone-basic": "\U000F011E", + "cellphone-charging": "\U000F1397", + "cellphone-check": "\U000F17FD", + "cellphone-cog": "\U000F0951", + "cellphone-dock": "\U000F011F", + "cellphone-information": "\U000F0F41", + "cellphone-key": "\U000F094E", + "cellphone-link": "\U000F0121", + "cellphone-link-off": "\U000F0122", + "cellphone-lock": "\U000F094F", + "cellphone-marker": "\U000F183A", + "cellphone-message": "\U000F08D3", + "cellphone-message-off": "\U000F10D2", + "cellphone-nfc": "\U000F0E90", + "cellphone-nfc-off": "\U000F12D8", + "cellphone-off": "\U000F0950", + "cellphone-play": "\U000F101F", + "cellphone-remove": "\U000F094D", + "cellphone-screenshot": "\U000F0A35", + "cellphone-settings": "\U000F0123", + "cellphone-sound": "\U000F0952", + "cellphone-text": "\U000F08D2", + "cellphone-wireless": "\U000F0815", + "centos": "\U000F111A", + "certificate": "\U000F0124", + "certificate-outline": "\U000F1188", + "chair-rolling": "\U000F0F48", + "chair-school": "\U000F0125", + "chandelier": "\U000F1793", + "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", + "chart-waterfall": "\U000F1918", + "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-question": "\U000F1738", + "chat-question-outline": "\U000F1739", + "chat-remove": "\U000F1411", + "chat-remove-outline": "\U000F1414", + "chat-sleep": "\U000F12D1", + "chat-sleep-outline": "\U000F12D2", + "check": "\U000F012C", + "check-all": "\U000F012D", + "check-bold": "\U000F0E1E", + "check-circle": "\U000F05E0", + "check-circle-outline": "\U000F05E1", + "check-decagram": "\U000F0791", + "check-decagram-outline": "\U000F1740", + "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-badge": "\U000F1176", + "checkbox-blank-badge-outline": "\U000F0117", + "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-intermediate-variant": "\U000F1B54", + "checkbox-marked": "\U000F0132", + "checkbox-marked-circle": "\U000F0133", + "checkbox-marked-circle-outline": "\U000F0134", + "checkbox-marked-circle-plus-outline": "\U000F1927", + "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", + "checkbox-multiple-outline": "\U000F0C51", + "checkbox-outline": "\U000F0C52", + "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-alert": "\U000F17EA", + "chili-alert-outline": "\U000F17EB", + "chili-hot": "\U000F07B2", + "chili-hot-outline": "\U000F17EC", + "chili-medium": "\U000F07B3", + "chili-medium-outline": "\U000F17ED", + "chili-mild": "\U000F07B4", + "chili-mild-outline": "\U000F17EE", + "chili-off": "\U000F1467", + "chili-off-outline": "\U000F17EF", + "chip": "\U000F061A", + "church": "\U000F0144", + "church-outline": "\U000F1B02", + "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-opacity": "\U000F1853", + "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-clock": "\U000F16E2", + "clipboard-clock-outline": "\U000F16E3", + "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-minus": "\U000F1618", + "clipboard-minus-outline": "\U000F1619", + "clipboard-multiple": "\U000F1267", + "clipboard-multiple-outline": "\U000F1268", + "clipboard-off": "\U000F161A", + "clipboard-off-outline": "\U000F161B", + "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-remove": "\U000F161C", + "clipboard-remove-outline": "\U000F161D", + "clipboard-search": "\U000F161E", + "clipboard-search-outline": "\U000F161F", + "clipboard-text": "\U000F014D", + "clipboard-text-clock": "\U000F18F9", + "clipboard-text-clock-outline": "\U000F18FA", + "clipboard-text-multiple": "\U000F126B", + "clipboard-text-multiple-outline": "\U000F126C", + "clipboard-text-off": "\U000F1620", + "clipboard-text-off-outline": "\U000F1621", + "clipboard-text-outline": "\U000F0A38", + "clipboard-text-play": "\U000F0C5B", + "clipboard-text-play-outline": "\U000F0C5C", + "clipboard-text-search": "\U000F1622", + "clipboard-text-search-outline": "\U000F1623", + "clippy": "\U000F014F", + "clock": "\U000F0954", + "clock-alert": "\U000F0955", + "clock-alert-outline": "\U000F05CE", + "clock-check": "\U000F0FA8", + "clock-check-outline": "\U000F0FA9", + "clock-digital": "\U000F0E97", + "clock-edit": "\U000F19BA", + "clock-edit-outline": "\U000F19BB", + "clock-end": "\U000F0151", + "clock-fast": "\U000F0152", + "clock-in": "\U000F0153", + "clock-minus": "\U000F1863", + "clock-minus-outline": "\U000F1864", + "clock-out": "\U000F0154", + "clock-outline": "\U000F0150", + "clock-plus": "\U000F1861", + "clock-plus-outline": "\U000F1862", + "clock-remove": "\U000F1865", + "clock-remove-outline": "\U000F1866", + "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-percent": "\U000F1A35", + "cloud-percent-outline": "\U000F1A36", + "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", + "coach-lamp-variant": "\U000F1A37", + "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-maker-check": "\U000F1931", + "coffee-maker-check-outline": "\U000F1932", + "coffee-maker-outline": "\U000F181B", + "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-pause": "\U000F1933", + "cog-pause-outline": "\U000F1934", + "cog-play": "\U000F1935", + "cog-play-outline": "\U000F1936", + "cog-refresh": "\U000F145E", + "cog-refresh-outline": "\U000F145F", + "cog-stop": "\U000F1937", + "cog-stop-outline": "\U000F1938", + "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-remove": "\U000F18B3", + "compare-vertical": "\U000F1493", + "compass": "\U000F018B", + "compass-off": "\U000F0B80", + "compass-off-outline": "\U000F0B81", + "compass-outline": "\U000F018C", + "compass-rose": "\U000F1382", + "compost": "\U000F1A38", + "cone": "\U000F194C", + "cone-off": "\U000F194D", + "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-check": "\U000F18EA", + "content-save-check-outline": "\U000F18EB", + "content-save-cog": "\U000F145B", + "content-save-cog-outline": "\U000F145C", + "content-save-edit": "\U000F0CFB", + "content-save-edit-outline": "\U000F0CFC", + "content-save-minus": "\U000F1B43", + "content-save-minus-outline": "\U000F1B44", + "content-save-move": "\U000F0E27", + "content-save-move-outline": "\U000F0E28", + "content-save-off": "\U000F1643", + "content-save-off-outline": "\U000F1644", + "content-save-outline": "\U000F0818", + "content-save-plus": "\U000F1B41", + "content-save-plus-outline": "\U000F1B42", + "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", + "cookie-alert": "\U000F16D0", + "cookie-alert-outline": "\U000F16D1", + "cookie-check": "\U000F16D2", + "cookie-check-outline": "\U000F16D3", + "cookie-clock": "\U000F16E4", + "cookie-clock-outline": "\U000F16E5", + "cookie-cog": "\U000F16D4", + "cookie-cog-outline": "\U000F16D5", + "cookie-edit": "\U000F16E6", + "cookie-edit-outline": "\U000F16E7", + "cookie-lock": "\U000F16E8", + "cookie-lock-outline": "\U000F16E9", + "cookie-minus": "\U000F16DA", + "cookie-minus-outline": "\U000F16DB", + "cookie-off": "\U000F16EA", + "cookie-off-outline": "\U000F16EB", + "cookie-outline": "\U000F16DE", + "cookie-plus": "\U000F16D6", + "cookie-plus-outline": "\U000F16D7", + "cookie-refresh": "\U000F16EC", + "cookie-refresh-outline": "\U000F16ED", + "cookie-remove": "\U000F16D8", + "cookie-remove-outline": "\U000F16D9", + "cookie-settings": "\U000F16DC", + "cookie-settings-outline": "\U000F16DD", + "coolant-temperature": "\U000F03C8", + "copyleft": "\U000F1939", + "copyright": "\U000F05E6", + "cordova": "\U000F0958", + "corn": "\U000F07B8", + "corn-off": "\U000F13EF", + "cosine-wave": "\U000F1479", + "counter": "\U000F0199", + "countertop": "\U000F181C", + "countertop-outline": "\U000F181D", + "cow": "\U000F019A", + "cow-off": "\U000F18FC", + "cpu-32-bit": "\U000F0EDF", + "cpu-64-bit": "\U000F0EE0", + "cradle": "\U000F198B", + "cradle-outline": "\U000F1991", + "crane": "\U000F0862", + "creation": "\U000F0674", + "creative-commons": "\U000F0D6B", + "credit-card": "\U000F0FEF", + "credit-card-check": "\U000F13D0", + "credit-card-check-outline": "\U000F13D1", + "credit-card-chip": "\U000F190F", + "credit-card-chip-outline": "\U000F1910", + "credit-card-clock": "\U000F0EE1", + "credit-card-clock-outline": "\U000F0EE2", + "credit-card-edit": "\U000F17D7", + "credit-card-edit-outline": "\U000F17D8", + "credit-card-fast": "\U000F1911", + "credit-card-fast-outline": "\U000F1912", + "credit-card-lock": "\U000F18E7", + "credit-card-lock-outline": "\U000F18E8", + "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-refresh": "\U000F1645", + "credit-card-refresh-outline": "\U000F1646", + "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-search": "\U000F1647", + "credit-card-search-outline": "\U000F1648", + "credit-card-settings": "\U000F0FF5", + "credit-card-settings-outline": "\U000F08D7", + "credit-card-sync": "\U000F1649", + "credit-card-sync-outline": "\U000F164A", + "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", + "cross": "\U000F0953", + "cross-bolnisi": "\U000F0CED", + "cross-celtic": "\U000F0CF5", + "cross-outline": "\U000F0CF6", + "crosshairs": "\U000F01A3", + "crosshairs-gps": "\U000F01A4", + "crosshairs-off": "\U000F0F45", + "crosshairs-question": "\U000F1136", + "crowd": "\U000F1975", + "crown": "\U000F01A5", + "crown-circle": "\U000F17DC", + "crown-circle-outline": "\U000F17DD", + "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-fra": "\U000F1A39", + "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-rupee": "\U000F1976", + "currency-sign": "\U000F07BE", + "currency-try": "\U000F01B2", + "currency-twd": "\U000F07BF", + "currency-usd": "\U000F01C1", + "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", + "curtains": "\U000F1846", + "curtains-closed": "\U000F1847", + "cylinder": "\U000F194E", + "cylinder-off": "\U000F194F", + "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-alert": "\U000F163A", + "database-alert-outline": "\U000F1624", + "database-arrow-down": "\U000F163B", + "database-arrow-down-outline": "\U000F1625", + "database-arrow-left": "\U000F163C", + "database-arrow-left-outline": "\U000F1626", + "database-arrow-right": "\U000F163D", + "database-arrow-right-outline": "\U000F1627", + "database-arrow-up": "\U000F163E", + "database-arrow-up-outline": "\U000F1628", + "database-check": "\U000F0AA9", + "database-check-outline": "\U000F1629", + "database-clock": "\U000F163F", + "database-clock-outline": "\U000F162A", + "database-cog": "\U000F164B", + "database-cog-outline": "\U000F164C", + "database-edit": "\U000F0B86", + "database-edit-outline": "\U000F162B", + "database-export": "\U000F095E", + "database-export-outline": "\U000F162C", + "database-eye": "\U000F191F", + "database-eye-off": "\U000F1920", + "database-eye-off-outline": "\U000F1921", + "database-eye-outline": "\U000F1922", + "database-import": "\U000F095D", + "database-import-outline": "\U000F162D", + "database-lock": "\U000F0AAA", + "database-lock-outline": "\U000F162E", + "database-marker": "\U000F12F6", + "database-marker-outline": "\U000F162F", + "database-minus": "\U000F01BB", + "database-minus-outline": "\U000F1630", + "database-off": "\U000F1640", + "database-off-outline": "\U000F1631", + "database-outline": "\U000F1632", + "database-plus": "\U000F01BA", + "database-plus-outline": "\U000F1633", + "database-refresh": "\U000F05C2", + "database-refresh-outline": "\U000F1634", + "database-remove": "\U000F0D00", + "database-remove-outline": "\U000F1635", + "database-search": "\U000F0866", + "database-search-outline": "\U000F1636", + "database-settings": "\U000F0D01", + "database-settings-outline": "\U000F1637", + "database-sync": "\U000F0CFF", + "database-sync-outline": "\U000F1638", + "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", + "desk-lamp-off": "\U000F1B1F", + "desk-lamp-on": "\U000F1B20", + "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", + "dharmachakra": "\U000F094B", + "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", + "diversify": "\U000F1877", + "diving": "\U000F1977", + "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", + "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", + "dog-side-off": "\U000F16EE", + "dolby": "\U000F06B3", + "dolly": "\U000F0E9E", + "dolphin": "\U000F18B4", + "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", + "door-sliding": "\U000F181E", + "door-sliding-lock": "\U000F181F", + "door-sliding-open": "\U000F1820", + "doorbell": "\U000F12E6", + "doorbell-video": "\U000F0869", + "dot-net": "\U000F0AAE", + "dots-circle": "\U000F1978", + "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", + "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", + "draw-pen": "\U000F19B9", + "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-loop": "\U000F1AEE", + "ear-hearing-off": "\U000F0A45", + "earbuds": "\U000F184F", + "earbuds-off": "\U000F1850", + "earbuds-off-outline": "\U000F1851", + "earbuds-outline": "\U000F1852", + "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-fried": "\U000F184A", + "egg-off": "\U000F13F0", + "egg-off-outline": "\U000F13F1", + "egg-outline": "\U000F13F2", + "eiffel-tower": "\U000F156B", + "eight-track": "\U000F09EA", + "eject": "\U000F01EA", + "eject-circle": "\U000F1B23", + "eject-circle-outline": "\U000F1B24", + "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-passenger-off": "\U000F1979", + "elevator-passenger-off-outline": "\U000F197A", + "elevator-passenger-outline": "\U000F197B", + "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-fast": "\U000F186F", + "email-fast-outline": "\U000F1870", + "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-remove": "\U000F1661", + "email-remove-outline": "\U000F1662", + "email-seal": "\U000F195B", + "email-seal-outline": "\U000F195C", + "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-arrow-left": "\U000F18FD", + "eye-arrow-left-outline": "\U000F18FE", + "eye-arrow-right": "\U000F18FF", + "eye-arrow-right-outline": "\U000F1900", + "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-refresh": "\U000F197C", + "eye-refresh-outline": "\U000F197D", + "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-agent": "\U000F0D70", + "face-man": "\U000F0643", + "face-man-outline": "\U000F0B96", + "face-man-profile": "\U000F0644", + "face-man-shimmer": "\U000F15CC", + "face-man-shimmer-outline": "\U000F15CD", + "face-mask": "\U000F1586", + "face-mask-outline": "\U000F1587", + "face-recognition": "\U000F0C7B", + "face-woman": "\U000F1077", + "face-woman-outline": "\U000F1078", + "face-woman-profile": "\U000F1076", + "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-auto": "\U000F171D", + "fan-chevron-down": "\U000F146D", + "fan-chevron-up": "\U000F146E", + "fan-clock": "\U000F1A3A", + "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-15": "\U000F193A", + "fast-forward-30": "\U000F0D06", + "fast-forward-45": "\U000F1B12", + "fast-forward-5": "\U000F11F8", + "fast-forward-60": "\U000F160B", + "fast-forward-outline": "\U000F06D2", + "faucet": "\U000F1B29", + "faucet-variant": "\U000F1B2A", + "fax": "\U000F0212", + "feather": "\U000F06D3", + "feature-search": "\U000F0A49", + "feature-search-outline": "\U000F0A4A", + "fedora": "\U000F08DB", + "fence": "\U000F179A", + "fence-electric": "\U000F17F6", + "fencing": "\U000F14C1", + "ferris-wheel": "\U000F0EA4", + "ferry": "\U000F0213", + "file": "\U000F0214", + "file-account": "\U000F073B", + "file-account-outline": "\U000F1028", + "file-alert": "\U000F0A4B", + "file-alert-outline": "\U000F0A4C", + "file-arrow-left-right": "\U000F1A93", + "file-arrow-left-right-outline": "\U000F1A94", + "file-arrow-up-down": "\U000F1A95", + "file-arrow-up-down-outline": "\U000F1A96", + "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-check": "\U000F19C6", + "file-chart-check-outline": "\U000F19C7", + "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-alert": "\U000F1A97", + "file-document-alert-outline": "\U000F1A98", + "file-document-check": "\U000F1A99", + "file-document-check-outline": "\U000F1A9A", + "file-document-edit": "\U000F0DC8", + "file-document-edit-outline": "\U000F0DC9", + "file-document-minus": "\U000F1A9B", + "file-document-minus-outline": "\U000F1A9C", + "file-document-multiple": "\U000F1517", + "file-document-multiple-outline": "\U000F1518", + "file-document-outline": "\U000F09EE", + "file-document-plus": "\U000F1A9D", + "file-document-plus-outline": "\U000F1A9E", + "file-document-remove": "\U000F1A9F", + "file-document-remove-outline": "\U000F1AA0", + "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-gif-box": "\U000F0D78", + "file-hidden": "\U000F0613", + "file-image": "\U000F021F", + "file-image-marker": "\U000F1772", + "file-image-marker-outline": "\U000F1773", + "file-image-minus": "\U000F193B", + "file-image-minus-outline": "\U000F193C", + "file-image-outline": "\U000F0EB0", + "file-image-plus": "\U000F193D", + "file-image-plus-outline": "\U000F193E", + "file-image-remove": "\U000F193F", + "file-image-remove-outline": "\U000F1940", + "file-import": "\U000F0220", + "file-import-outline": "\U000F102F", + "file-jpg-box": "\U000F0225", + "file-key": "\U000F1184", + "file-key-outline": "\U000F1185", + "file-link": "\U000F1177", + "file-link-outline": "\U000F1178", + "file-lock": "\U000F0221", + "file-lock-open": "\U000F19C8", + "file-lock-open-outline": "\U000F19C9", + "file-lock-outline": "\U000F1030", + "file-marker": "\U000F1774", + "file-marker-outline": "\U000F1775", + "file-minus": "\U000F1AA1", + "file-minus-outline": "\U000F1AA2", + "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-box": "\U000F0226", + "file-percent": "\U000F081E", + "file-percent-outline": "\U000F1033", + "file-phone": "\U000F1179", + "file-phone-outline": "\U000F117A", + "file-plus": "\U000F0752", + "file-plus-outline": "\U000F0EED", + "file-png-box": "\U000F0E2D", + "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-rotate-left": "\U000F1A3B", + "file-rotate-left-outline": "\U000F1A3C", + "file-rotate-right": "\U000F1A3D", + "file-rotate-right-outline": "\U000F1A3E", + "file-search": "\U000F0C7C", + "file-search-outline": "\U000F0C7D", + "file-send": "\U000F022A", + "file-send-outline": "\U000F1039", + "file-settings": "\U000F1079", + "file-settings-outline": "\U000F107A", + "file-sign": "\U000F19C3", + "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", + "file-xml-box": "\U000F1B4B", + "film": "\U000F022F", + "filmstrip": "\U000F0230", + "filmstrip-box": "\U000F0332", + "filmstrip-box-multiple": "\U000F0D18", + "filmstrip-off": "\U000F0231", + "filter": "\U000F0232", + "filter-check": "\U000F18EC", + "filter-check-outline": "\U000F18ED", + "filter-cog": "\U000F1AA3", + "filter-cog-outline": "\U000F1AA4", + "filter-menu": "\U000F10E5", + "filter-menu-outline": "\U000F10E6", + "filter-minus": "\U000F0EEE", + "filter-minus-outline": "\U000F0EEF", + "filter-multiple": "\U000F1A3F", + "filter-multiple-outline": "\U000F1A40", + "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-settings": "\U000F1AA5", + "filter-settings-outline": "\U000F1AA6", + "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-circle": "\U000F1807", + "fire-extinguisher": "\U000F0EF2", + "fire-hydrant": "\U000F1137", + "fire-hydrant-alert": "\U000F1138", + "fire-hydrant-off": "\U000F1139", + "fire-off": "\U000F1722", + "fire-truck": "\U000F08AB", + "firebase": "\U000F0967", + "firefox": "\U000F0239", + "fireplace": "\U000F0E2E", + "fireplace-off": "\U000F0E2F", + "firewire": "\U000F05BE", + "firework": "\U000F0E30", + "firework-off": "\U000F1723", + "fish": "\U000F023A", + "fish-off": "\U000F13F3", + "fishbowl": "\U000F0EF3", + "fishbowl-outline": "\U000F0EF4", + "fit-to-page": "\U000F0EF5", + "fit-to-page-outline": "\U000F0EF6", + "fit-to-screen": "\U000F18F4", + "fit-to-screen-outline": "\U000F18F5", + "flag": "\U000F023B", + "flag-checkered": "\U000F023C", + "flag-minus": "\U000F0B99", + "flag-minus-outline": "\U000F10B2", + "flag-off": "\U000F18EE", + "flag-off-outline": "\U000F18EF", + "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-off": "\U000F0243", + "flash-off-outline": "\U000F1B45", + "flash-outline": "\U000F06D5", + "flash-red-eye": "\U000F067B", + "flash-triangle": "\U000F1B1D", + "flash-triangle-outline": "\U000F1B1E", + "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-dual-outline": "\U000F17CE", + "floor-lamp-outline": "\U000F17C8", + "floor-lamp-torchiere": "\U000F1747", + "floor-lamp-torchiere-outline": "\U000F17D6", + "floor-lamp-torchiere-variant": "\U000F1041", + "floor-lamp-torchiere-variant-outline": "\U000F17CF", + "floor-plan": "\U000F0821", + "floppy": "\U000F0249", + "floppy-variant": "\U000F09EF", + "flower": "\U000F024A", + "flower-outline": "\U000F09F0", + "flower-pollen": "\U000F1885", + "flower-pollen-outline": "\U000F1886", + "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-arrow-down": "\U000F19E8", + "folder-arrow-down-outline": "\U000F19E9", + "folder-arrow-left": "\U000F19EA", + "folder-arrow-left-outline": "\U000F19EB", + "folder-arrow-left-right": "\U000F19EC", + "folder-arrow-left-right-outline": "\U000F19ED", + "folder-arrow-right": "\U000F19EE", + "folder-arrow-right-outline": "\U000F19EF", + "folder-arrow-up": "\U000F19F0", + "folder-arrow-up-down": "\U000F19F1", + "folder-arrow-up-down-outline": "\U000F19F2", + "folder-arrow-up-outline": "\U000F19F3", + "folder-cancel": "\U000F19F4", + "folder-cancel-outline": "\U000F19F5", + "folder-check": "\U000F197E", + "folder-check-outline": "\U000F197F", + "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-eye": "\U000F178A", + "folder-eye-outline": "\U000F178B", + "folder-file": "\U000F19F6", + "folder-file-outline": "\U000F19F7", + "folder-google-drive": "\U000F024E", + "folder-heart": "\U000F10EA", + "folder-heart-outline": "\U000F10EB", + "folder-hidden": "\U000F179E", + "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-lock-open-outline": "\U000F1AA7", + "folder-lock-outline": "\U000F1AA8", + "folder-marker": "\U000F126D", + "folder-marker-outline": "\U000F126E", + "folder-minus": "\U000F1B49", + "folder-minus-outline": "\U000F1B4A", + "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-off": "\U000F19F8", + "folder-off-outline": "\U000F19F9", + "folder-open": "\U000F0770", + "folder-open-outline": "\U000F0DCF", + "folder-outline": "\U000F0256", + "folder-play": "\U000F19FA", + "folder-play-outline": "\U000F19FB", + "folder-plus": "\U000F0257", + "folder-plus-outline": "\U000F0B9D", + "folder-pound": "\U000F0D09", + "folder-pound-outline": "\U000F0D0A", + "folder-question": "\U000F19CA", + "folder-question-outline": "\U000F19CB", + "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-wrench": "\U000F19FC", + "folder-wrench-outline": "\U000F19FD", + "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-hot-dog": "\U000F184B", + "food-kosher": "\U000F1573", + "food-off": "\U000F05F3", + "food-off-outline": "\U000F1915", + "food-outline": "\U000F1916", + "food-steak": "\U000F146A", + "food-steak-off": "\U000F146B", + "food-takeout-box": "\U000F1836", + "food-takeout-box-outline": "\U000F1837", + "food-turkey": "\U000F171C", + "food-variant": "\U000F025C", + "food-variant-off": "\U000F13E5", + "foot-print": "\U000F0F52", + "football": "\U000F025D", + "football-australian": "\U000F025E", + "football-helmet": "\U000F025F", + "forest": "\U000F1897", + "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-spacing": "\U000F1956", + "format-letter-spacing-variant": "\U000F1AFB", + "format-letter-starts-with": "\U000F0FBA", + "format-line-height": "\U000F1AFC", + "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-group": "\U000F1860", + "format-list-numbered": "\U000F027B", + "format-list-numbered-rtl": "\U000F0D0D", + "format-list-text": "\U000F126F", + "format-overline": "\U000F0EB3", + "format-page-break": "\U000F06D7", + "format-page-split": "\U000F1917", + "format-paint": "\U000F027C", + "format-paragraph": "\U000F027D", + "format-paragraph-spacing": "\U000F1AFD", + "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-underline-wavy": "\U000F18E9", + "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-minus": "\U000F1AA9", + "forum-minus-outline": "\U000F1AAA", + "forum-outline": "\U000F0822", + "forum-plus": "\U000F1AAB", + "forum-plus-outline": "\U000F1AAC", + "forum-remove": "\U000F1AAD", + "forum-remove-outline": "\U000F1AAE", + "forward": "\U000F028D", + "forwardburger": "\U000F0D75", + "fountain": "\U000F096B", + "fountain-pen": "\U000F0D12", + "fountain-pen-tip": "\U000F0D13", + "fraction-one-half": "\U000F1992", + "freebsd": "\U000F08E0", + "french-fries": "\U000F1957", + "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-pear": "\U000F1A0E", + "fruit-pineapple": "\U000F1046", + "fruit-watermelon": "\U000F1047", + "fuel": "\U000F07CA", + "fuel-cell": "\U000F18B5", + "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-outline": "\U000F1919", + "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-lock": "\U000F17FB", + "garage-open": "\U000F06DA", + "garage-open-variant": "\U000F12D4", + "garage-variant": "\U000F12D3", + "garage-variant-lock": "\U000F17FC", + "gas-burner": "\U000F1A1B", + "gas-cylinder": "\U000F0647", + "gas-station": "\U000F0298", + "gas-station-off": "\U000F1409", + "gas-station-off-outline": "\U000F140A", + "gas-station-outline": "\U000F0EB8", + "gate": "\U000F0299", + "gate-alert": "\U000F17F8", + "gate-and": "\U000F08E1", + "gate-arrow-left": "\U000F17F7", + "gate-arrow-right": "\U000F1169", + "gate-buffer": "\U000F1AFE", + "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", + "ghost-off-outline": "\U000F165C", + "ghost-outline": "\U000F165D", + "gift": "\U000F0E44", + "gift-off": "\U000F16EF", + "gift-off-outline": "\U000F16F0", + "gift-open": "\U000F16F1", + "gift-open-outline": "\U000F16F2", + "gift-outline": "\U000F02A1", + "git": "\U000F02A2", + "github": "\U000F02A4", + "gitlab": "\U000F0BA0", + "glass-cocktail": "\U000F0356", + "glass-cocktail-off": "\U000F15E6", + "glass-flute": "\U000F02A5", + "glass-fragile": "\U000F1873", + "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-play": "\U000F02BC", + "google-plus": "\U000F02BD", + "google-podcast": "\U000F0EB9", + "google-spreadsheet": "\U000F09F7", + "google-street-view": "\U000F0C88", + "google-translate": "\U000F02BF", + "gradient-horizontal": "\U000F174A", + "gradient-vertical": "\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", + "greenhouse": "\U000F002D", + "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", + "gymnastics": "\U000F1A41", + "hail": "\U000F0AC1", + "hair-dryer": "\U000F10EF", + "hair-dryer-outline": "\U000F10F0", + "halloween": "\U000F0BA3", + "hamburger": "\U000F0685", + "hamburger-check": "\U000F1776", + "hamburger-minus": "\U000F1777", + "hamburger-off": "\U000F1778", + "hamburger-plus": "\U000F1779", + "hamburger-remove": "\U000F177A", + "hammer": "\U000F08EA", + "hammer-screwdriver": "\U000F1322", + "hammer-sickle": "\U000F1887", + "hammer-wrench": "\U000F1323", + "hand-back-left": "\U000F0E46", + "hand-back-left-off": "\U000F1830", + "hand-back-left-off-outline": "\U000F1832", + "hand-back-left-outline": "\U000F182C", + "hand-back-right": "\U000F0E47", + "hand-back-right-off": "\U000F1831", + "hand-back-right-off-outline": "\U000F1833", + "hand-back-right-outline": "\U000F182D", + "hand-clap": "\U000F194B", + "hand-clap-off": "\U000F1A42", + "hand-coin": "\U000F188F", + "hand-coin-outline": "\U000F1890", + "hand-extended": "\U000F18B6", + "hand-extended-outline": "\U000F18B7", + "hand-front-left": "\U000F182B", + "hand-front-left-outline": "\U000F182E", + "hand-front-right": "\U000F0A4F", + "hand-front-right-outline": "\U000F182F", + "hand-heart": "\U000F10F1", + "hand-heart-outline": "\U000F157E", + "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-saw": "\U000F0E48", + "hand-wash": "\U000F157F", + "hand-wash-outline": "\U000F1580", + "hand-water": "\U000F139F", + "hand-wave": "\U000F1821", + "hand-wave-outline": "\U000F1822", + "handball": "\U000F0F53", + "handcuffs": "\U000F113E", + "hands-pray": "\U000F0579", + "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-cog": "\U000F1663", + "heart-cog-outline": "\U000F1664", + "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", + "heart-settings": "\U000F1665", + "heart-settings-outline": "\U000F1666", + "heat-pump": "\U000F1A43", + "heat-pump-outline": "\U000F1A44", + "heat-wave": "\U000F1A45", + "heating-coil": "\U000F1AAF", + "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", + "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-battery": "\U000F1901", + "home-battery-outline": "\U000F1902", + "home-circle": "\U000F07D2", + "home-circle-outline": "\U000F104D", + "home-city": "\U000F0D15", + "home-city-outline": "\U000F0D16", + "home-clock": "\U000F1A12", + "home-clock-outline": "\U000F1A13", + "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-group-minus": "\U000F19C1", + "home-group-plus": "\U000F19C0", + "home-group-remove": "\U000F19C2", + "home-heart": "\U000F0827", + "home-import-outline": "\U000F0F9C", + "home-lightbulb": "\U000F1251", + "home-lightbulb-outline": "\U000F1252", + "home-lightning-bolt": "\U000F1903", + "home-lightning-bolt-outline": "\U000F1904", + "home-lock": "\U000F08EB", + "home-lock-open": "\U000F08EC", + "home-map-marker": "\U000F05F8", + "home-minus": "\U000F0974", + "home-minus-outline": "\U000F13D5", + "home-modern": "\U000F02DD", + "home-off": "\U000F1A46", + "home-off-outline": "\U000F1A47", + "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-switch": "\U000F1794", + "home-switch-outline": "\U000F1795", + "home-thermometer": "\U000F0F54", + "home-thermometer-outline": "\U000F0F55", + "home-variant": "\U000F02DE", + "home-variant-outline": "\U000F0BA7", + "hook": "\U000F06E2", + "hook-off": "\U000F06E3", + "hoop-house": "\U000F0E56", + "hops": "\U000F02DF", + "horizontal-rotate-clockwise": "\U000F10F3", + "horizontal-rotate-counterclockwise": "\U000F10F4", + "horse": "\U000F15BF", + "horse-human": "\U000F15C0", + "horse-variant": "\U000F15C1", + "horse-variant-fast": "\U000F186E", + "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-dolly": "\U000F1980", + "human-edit": "\U000F14E8", + "human-female": "\U000F0649", + "human-female-boy": "\U000F0A59", + "human-female-dance": "\U000F15C9", + "human-female-female": "\U000F0A5A", + "human-female-girl": "\U000F0A5B", + "human-greeting": "\U000F17C4", + "human-greeting-proximity": "\U000F159D", + "human-greeting-variant": "\U000F064A", + "human-handsdown": "\U000F064B", + "human-handsup": "\U000F064C", + "human-male": "\U000F064D", + "human-male-board": "\U000F0890", + "human-male-board-poll": "\U000F0846", + "human-male-boy": "\U000F0A5C", + "human-male-child": "\U000F138C", + "human-male-female": "\U000F02E8", + "human-male-female-child": "\U000F1823", + "human-male-girl": "\U000F0A5D", + "human-male-height": "\U000F0EFB", + "human-male-height-variant": "\U000F0EFC", + "human-male-male": "\U000F0A5E", + "human-non-binary": "\U000F1848", + "human-pregnant": "\U000F05CF", + "human-queue": "\U000F1571", + "human-scooter": "\U000F11E9", + "human-wheelchair": "\U000F138D", + "human-white-cane": "\U000F1981", + "humble-bundle": "\U000F0744", + "hvac": "\U000F1352", + "hvac-off": "\U000F159E", + "hydraulic-oil-level": "\U000F1324", + "hydraulic-oil-temperature": "\U000F1325", + "hydro-power": "\U000F12E5", + "hydrogen-station": "\U000F1894", + "ice-cream": "\U000F082A", + "ice-cream-off": "\U000F0E52", + "ice-pop": "\U000F0EFD", + "id-card": "\U000F0FC0", + "identifier": "\U000F0EFE", + "ideogram-cjk": "\U000F1331", + "ideogram-cjk-variant": "\U000F1332", + "image": "\U000F02E9", + "image-album": "\U000F02EA", + "image-area": "\U000F02EB", + "image-area-close": "\U000F02EC", + "image-auto-adjust": "\U000F0FC1", + "image-broken": "\U000F02ED", + "image-broken-variant": "\U000F02EE", + "image-check": "\U000F1B25", + "image-check-outline": "\U000F1B26", + "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-lock": "\U000F1AB0", + "image-lock-outline": "\U000F1AB1", + "image-marker": "\U000F177B", + "image-marker-outline": "\U000F177C", + "image-minus": "\U000F1419", + "image-minus-outline": "\U000F1B47", + "image-move": "\U000F09F8", + "image-multiple": "\U000F02F9", + "image-multiple-outline": "\U000F02EF", + "image-off": "\U000F082B", + "image-off-outline": "\U000F11D1", + "image-outline": "\U000F0976", + "image-plus": "\U000F087C", + "image-plus-outline": "\U000F1B46", + "image-refresh": "\U000F19FE", + "image-refresh-outline": "\U000F19FF", + "image-remove": "\U000F1418", + "image-remove-outline": "\U000F1B48", + "image-search": "\U000F0977", + "image-search-outline": "\U000F0978", + "image-size-select-actual": "\U000F0C8D", + "image-size-select-large": "\U000F0C8E", + "image-size-select-small": "\U000F0C8F", + "image-sync": "\U000F1A00", + "image-sync-outline": "\U000F1A01", + "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", + "induction": "\U000F184C", + "infinity": "\U000F06E4", + "information": "\U000F02FC", + "information-off": "\U000F178C", + "information-off-outline": "\U000F178D", + "information-outline": "\U000F02FD", + "information-variant": "\U000F064E", + "instagram": "\U000F02FE", + "instrument-triangle": "\U000F104E", + "integrated-circuit-chip": "\U000F1913", + "invert-colors": "\U000F0301", + "invert-colors-off": "\U000F0E4A", + "iobroker": "\U000F12E8", + "ip": "\U000F0A5F", + "ip-network": "\U000F0A60", + "ip-network-outline": "\U000F0C90", + "ip-outline": "\U000F1982", + "ipod": "\U000F0C91", + "iron": "\U000F1824", + "iron-board": "\U000F1838", + "iron-outline": "\U000F1825", + "island": "\U000F104F", + "iv-bag": "\U000F10B9", + "jabber": "\U000F0DD5", + "jeepney": "\U000F0302", + "jellyfish": "\U000F0F01", + "jellyfish-outline": "\U000F0F02", + "jira": "\U000F0303", + "jquery": "\U000F087D", + "jsfiddle": "\U000F0304", + "jump-rope": "\U000F12FF", + "kabaddi": "\U000F0D87", + "kangaroo": "\U000F1558", + "karate": "\U000F082C", + "kayaking": "\U000F08AF", + "keg": "\U000F0305", + "kettle": "\U000F05FA", + "kettle-alert": "\U000F1317", + "kettle-alert-outline": "\U000F1318", + "kettle-off": "\U000F131B", + "kettle-off-outline": "\U000F131C", + "kettle-outline": "\U000F0F56", + "kettle-pour-over": "\U000F173C", + "kettle-steam": "\U000F1319", + "kettle-steam-outline": "\U000F131A", + "kettlebell": "\U000F1300", + "key": "\U000F0306", + "key-alert": "\U000F1983", + "key-alert-outline": "\U000F1984", + "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-tab-reverse": "\U000F0325", + "keyboard-variant": "\U000F0313", + "khanda": "\U000F10FD", + "kickstarter": "\U000F0745", + "kite": "\U000F1985", + "kite-outline": "\U000F1986", + "kitesurfing": "\U000F1744", + "klingon": "\U000F135B", + "knife": "\U000F09FB", + "knife-military": "\U000F09FC", + "koala": "\U000F173F", + "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", + "lamp-outline": "\U000F17D0", + "lamps": "\U000F1576", + "lamps-outline": "\U000F17D1", + "lan": "\U000F0317", + "lan-check": "\U000F12AA", + "lan-connect": "\U000F0318", + "lan-disconnect": "\U000F0319", + "lan-pending": "\U000F031A", + "land-fields": "\U000F1AB2", + "land-plots": "\U000F1AB3", + "land-plots-circle": "\U000F1AB4", + "land-plots-circle-variant": "\U000F1AB5", + "land-rows-horizontal": "\U000F1AB6", + "land-rows-vertical": "\U000F1AB7", + "landslide": "\U000F1A48", + "landslide-outline": "\U000F1A49", + "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-account": "\U000F1A4A", + "laptop-off": "\U000F06E7", + "laravel": "\U000F0AD0", + "laser-pointer": "\U000F1484", + "lasso": "\U000F0F03", + "lastpass": "\U000F0446", + "latitude": "\U000F0F57", + "launch": "\U000F0327", + "lava-lamp": "\U000F07D5", + "layers": "\U000F0328", + "layers-edit": "\U000F1892", + "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-circle": "\U000F1905", + "leaf-circle-outline": "\U000F1906", + "leaf-maple": "\U000F0C93", + "leaf-maple-off": "\U000F12DA", + "leaf-off": "\U000F12D9", + "leak": "\U000F0DD7", + "leak-off": "\U000F0DD8", + "lecturn": "\U000F1AF0", + "led-off": "\U000F032B", + "led-on": "\U000F032C", + "led-outline": "\U000F032D", + "led-strip": "\U000F07D6", + "led-strip-variant": "\U000F1051", + "led-strip-variant-off": "\U000F1A4B", + "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-outline": "\U000F1A22", + "library-shelves": "\U000F0BA9", + "license": "\U000F0FC3", + "lifebuoy": "\U000F087E", + "light-flood-down": "\U000F1987", + "light-flood-up": "\U000F1988", + "light-recessed": "\U000F179B", + "light-switch": "\U000F097E", + "light-switch-off": "\U000F1A24", + "lightbulb": "\U000F0335", + "lightbulb-alert": "\U000F19E1", + "lightbulb-alert-outline": "\U000F19E2", + "lightbulb-auto": "\U000F1800", + "lightbulb-auto-outline": "\U000F1801", + "lightbulb-cfl": "\U000F1208", + "lightbulb-cfl-off": "\U000F1209", + "lightbulb-cfl-spiral": "\U000F1275", + "lightbulb-cfl-spiral-off": "\U000F12C3", + "lightbulb-fluorescent-tube": "\U000F1804", + "lightbulb-fluorescent-tube-outline": "\U000F1805", + "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-night": "\U000F1A4C", + "lightbulb-night-outline": "\U000F1A4D", + "lightbulb-off": "\U000F0E4F", + "lightbulb-off-outline": "\U000F0E50", + "lightbulb-on": "\U000F06E8", + "lightbulb-on-10": "\U000F1A4E", + "lightbulb-on-20": "\U000F1A4F", + "lightbulb-on-30": "\U000F1A50", + "lightbulb-on-40": "\U000F1A51", + "lightbulb-on-50": "\U000F1A52", + "lightbulb-on-60": "\U000F1A53", + "lightbulb-on-70": "\U000F1A54", + "lightbulb-on-80": "\U000F1A55", + "lightbulb-on-90": "\U000F1A56", + "lightbulb-on-outline": "\U000F06E9", + "lightbulb-outline": "\U000F0336", + "lightbulb-question": "\U000F19E3", + "lightbulb-question-outline": "\U000F19E4", + "lightbulb-spot": "\U000F17F4", + "lightbulb-spot-off": "\U000F17F5", + "lightbulb-variant": "\U000F1802", + "lightbulb-variant-outline": "\U000F1803", + "lighthouse": "\U000F09FF", + "lighthouse-on": "\U000F0A00", + "lightning-bolt": "\U000F140B", + "lightning-bolt-circle": "\U000F0820", + "lightning-bolt-outline": "\U000F140C", + "line-scan": "\U000F0624", + "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", + "liquid-spot": "\U000F1826", + "liquor": "\U000F191E", + "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-check-outline": "\U000F16A8", + "lock-clock": "\U000F097F", + "lock-minus": "\U000F16A9", + "lock-minus-outline": "\U000F16AA", + "lock-off": "\U000F1671", + "lock-off-outline": "\U000F1672", + "lock-open": "\U000F033F", + "lock-open-alert": "\U000F139B", + "lock-open-alert-outline": "\U000F15D2", + "lock-open-check": "\U000F139C", + "lock-open-check-outline": "\U000F16AB", + "lock-open-minus": "\U000F16AC", + "lock-open-minus-outline": "\U000F16AD", + "lock-open-outline": "\U000F0340", + "lock-open-plus": "\U000F16AE", + "lock-open-plus-outline": "\U000F16AF", + "lock-open-remove": "\U000F16B0", + "lock-open-remove-outline": "\U000F16B1", + "lock-open-variant": "\U000F0FC6", + "lock-open-variant-outline": "\U000F0FC7", + "lock-outline": "\U000F0341", + "lock-pattern": "\U000F06EA", + "lock-plus": "\U000F05FB", + "lock-plus-outline": "\U000F16B2", + "lock-question": "\U000F08EF", + "lock-remove": "\U000F16B3", + "lock-remove-outline": "\U000F16B4", + "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", + "mace": "\U000F1843", + "magazine-pistol": "\U000F0324", + "magazine-rifle": "\U000F0323", + "magic-staff": "\U000F1844", + "magnet": "\U000F0347", + "magnet-on": "\U000F0348", + "magnify": "\U000F0349", + "magnify-close": "\U000F0980", + "magnify-expand": "\U000F1874", + "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-account": "\U000F18E3", + "map-marker-account-outline": "\U000F18E4", + "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", + "medical-cotton-swab": "\U000F1AB8", + "medication": "\U000F1B14", + "medication-outline": "\U000F1B15", + "meditation": "\U000F117B", + "memory": "\U000F035B", + "menorah": "\U000F17D4", + "menorah-fire": "\U000F17D5", + "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-badge": "\U000F1941", + "message-badge-outline": "\U000F1942", + "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-fast": "\U000F19CC", + "message-fast-outline": "\U000F19CD", + "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-off": "\U000F164D", + "message-off-outline": "\U000F164E", + "message-outline": "\U000F0365", + "message-plus": "\U000F0653", + "message-plus-outline": "\U000F10BB", + "message-processing": "\U000F0366", + "message-processing-outline": "\U000F1170", + "message-question": "\U000F173A", + "message-question-outline": "\U000F173B", + "message-reply": "\U000F0367", + "message-reply-outline": "\U000F173D", + "message-reply-text": "\U000F0368", + "message-reply-text-outline": "\U000F173E", + "message-settings": "\U000F06F0", + "message-settings-outline": "\U000F1171", + "message-star": "\U000F069A", + "message-star-outline": "\U000F1250", + "message-text": "\U000F0369", + "message-text-clock": "\U000F1173", + "message-text-clock-outline": "\U000F1174", + "message-text-fast": "\U000F19CE", + "message-text-fast-outline": "\U000F19CF", + "message-text-lock": "\U000F0FCD", + "message-text-lock-outline": "\U000F1175", + "message-text-outline": "\U000F036A", + "message-video": "\U000F036B", + "meteor": "\U000F0629", + "meter-electric": "\U000F1A57", + "meter-electric-outline": "\U000F1A58", + "meter-gas": "\U000F1A59", + "meter-gas-outline": "\U000F1A5A", + "metronome": "\U000F07DA", + "metronome-tick": "\U000F07DB", + "micro-sd": "\U000F07DC", + "microphone": "\U000F036C", + "microphone-minus": "\U000F08B3", + "microphone-off": "\U000F036D", + "microphone-outline": "\U000F036E", + "microphone-plus": "\U000F08B4", + "microphone-question": "\U000F1989", + "microphone-question-outline": "\U000F198A", + "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-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", + "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", + "minus-thick": "\U000F1639", + "mirror": "\U000F11FD", + "mirror-rectangle": "\U000F179F", + "mirror-variant": "\U000F17A0", + "mixed-martial-arts": "\U000F0D8F", + "mixed-reality": "\U000F087F", + "molecule": "\U000F0BAC", + "molecule-co": "\U000F12FE", + "molecule-co2": "\U000F07E4", + "monitor": "\U000F0379", + "monitor-account": "\U000F1A5B", + "monitor-arrow-down": "\U000F19D0", + "monitor-arrow-down-variant": "\U000F19D1", + "monitor-cellphone": "\U000F0989", + "monitor-cellphone-star": "\U000F098A", + "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-shimmer": "\U000F1104", + "monitor-small": "\U000F1876", + "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", + "mortar-pestle": "\U000F1748", + "mortar-pestle-plus": "\U000F03F1", + "mosque": "\U000F1827", + "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", + "motorbike-off": "\U000F1B16", + "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-check": "\U000F16F3", + "movie-check-outline": "\U000F16F4", + "movie-cog": "\U000F16F5", + "movie-cog-outline": "\U000F16F6", + "movie-edit": "\U000F1122", + "movie-edit-outline": "\U000F1123", + "movie-filter": "\U000F1124", + "movie-filter-outline": "\U000F1125", + "movie-minus": "\U000F16F7", + "movie-minus-outline": "\U000F16F8", + "movie-off": "\U000F16F9", + "movie-off-outline": "\U000F16FA", + "movie-open": "\U000F0FCE", + "movie-open-check": "\U000F16FB", + "movie-open-check-outline": "\U000F16FC", + "movie-open-cog": "\U000F16FD", + "movie-open-cog-outline": "\U000F16FE", + "movie-open-edit": "\U000F16FF", + "movie-open-edit-outline": "\U000F1700", + "movie-open-minus": "\U000F1701", + "movie-open-minus-outline": "\U000F1702", + "movie-open-off": "\U000F1703", + "movie-open-off-outline": "\U000F1704", + "movie-open-outline": "\U000F0FCF", + "movie-open-play": "\U000F1705", + "movie-open-play-outline": "\U000F1706", + "movie-open-plus": "\U000F1707", + "movie-open-plus-outline": "\U000F1708", + "movie-open-remove": "\U000F1709", + "movie-open-remove-outline": "\U000F170A", + "movie-open-settings": "\U000F170B", + "movie-open-settings-outline": "\U000F170C", + "movie-open-star": "\U000F170D", + "movie-open-star-outline": "\U000F170E", + "movie-outline": "\U000F0DDD", + "movie-play": "\U000F170F", + "movie-play-outline": "\U000F1710", + "movie-plus": "\U000F1711", + "movie-plus-outline": "\U000F1712", + "movie-remove": "\U000F1713", + "movie-remove-outline": "\U000F1714", + "movie-roll": "\U000F07DE", + "movie-search": "\U000F11D2", + "movie-search-outline": "\U000F11D3", + "movie-settings": "\U000F1715", + "movie-settings-outline": "\U000F1716", + "movie-star": "\U000F1717", + "movie-star-outline": "\U000F1718", + "mower": "\U000F166F", + "mower-bag": "\U000F1670", + "muffin": "\U000F098C", + "multicast": "\U000F1893", + "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", + "navigation-variant": "\U000F18F0", + "navigation-variant-outline": "\U000F18F1", + "near-me": "\U000F05CD", + "necklace": "\U000F0F0B", + "needle": "\U000F0391", + "needle-off": "\U000F19D2", + "netflix": "\U000F0746", + "network": "\U000F06F3", + "network-off": "\U000F0C9B", + "network-off-outline": "\U000F0C9C", + "network-outline": "\U000F0C9D", + "network-pos": "\U000F1ACB", + "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-4-cog": "\U000F191A", + "network-strength-off": "\U000F08FC", + "network-strength-off-outline": "\U000F08FD", + "network-strength-outline": "\U000F08FE", + "new-box": "\U000F0394", + "newspaper": "\U000F0395", + "newspaper-check": "\U000F1943", + "newspaper-minus": "\U000F0F0C", + "newspaper-plus": "\U000F0F0D", + "newspaper-remove": "\U000F1944", + "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-alert": "\U000F177D", + "note-alert-outline": "\U000F177E", + "note-check": "\U000F177F", + "note-check-outline": "\U000F1780", + "note-edit": "\U000F1781", + "note-edit-outline": "\U000F1782", + "note-minus": "\U000F164F", + "note-minus-outline": "\U000F1650", + "note-multiple": "\U000F06B8", + "note-multiple-outline": "\U000F06B9", + "note-off": "\U000F1783", + "note-off-outline": "\U000F1784", + "note-outline": "\U000F039B", + "note-plus": "\U000F039C", + "note-plus-outline": "\U000F039D", + "note-remove": "\U000F1651", + "note-remove-outline": "\U000F1652", + "note-search": "\U000F1653", + "note-search-outline": "\U000F1654", + "note-text": "\U000F039E", + "note-text-outline": "\U000F11D7", + "notebook": "\U000F082E", + "notebook-check": "\U000F14F5", + "notebook-check-outline": "\U000F14F6", + "notebook-edit": "\U000F14E7", + "notebook-edit-outline": "\U000F14E9", + "notebook-heart": "\U000F1A0B", + "notebook-heart-outline": "\U000F1A0C", + "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-off": "\U000F19D3", + "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", + "octahedron": "\U000F1950", + "octahedron-off": "\U000F1951", + "odnoklassniki": "\U000F03C5", + "offer": "\U000F121B", + "office-building": "\U000F0991", + "office-building-cog": "\U000F1949", + "office-building-cog-outline": "\U000F194A", + "office-building-marker": "\U000F1520", + "office-building-marker-outline": "\U000F1521", + "office-building-outline": "\U000F151F", + "oil": "\U000F03C7", + "oil-lamp": "\U000F0F19", + "oil-level": "\U000F1053", + "oil-temperature": "\U000F0FF8", + "om": "\U000F0973", + "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-check": "\U000F1B51", + "package-down": "\U000F03D4", + "package-up": "\U000F03D5", + "package-variant": "\U000F03D6", + "package-variant-closed": "\U000F03D7", + "package-variant-closed-check": "\U000F1B52", + "package-variant-closed-minus": "\U000F19D4", + "package-variant-closed-plus": "\U000F19D5", + "package-variant-closed-remove": "\U000F19D6", + "package-variant-minus": "\U000F19D7", + "package-variant-plus": "\U000F19D8", + "package-variant-remove": "\U000F19D9", + "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", + "palette-swatch-variant": "\U000F195A", + "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": "\U000F1928", + "panorama-horizontal-outline": "\U000F03DE", + "panorama-outline": "\U000F198C", + "panorama-sphere": "\U000F198D", + "panorama-sphere-outline": "\U000F198E", + "panorama-variant": "\U000F198F", + "panorama-variant-outline": "\U000F1990", + "panorama-vertical": "\U000F1929", + "panorama-vertical-outline": "\U000F03DF", + "panorama-wide-angle": "\U000F195F", + "panorama-wide-angle-outline": "\U000F03E0", + "paper-cut-vertical": "\U000F03E1", + "paper-roll": "\U000F1157", + "paper-roll-outline": "\U000F1158", + "paperclip": "\U000F03E2", + "paperclip-check": "\U000F1AC6", + "paperclip-lock": "\U000F19DA", + "paperclip-minus": "\U000F1AC7", + "paperclip-off": "\U000F1AC8", + "paperclip-plus": "\U000F1AC9", + "paperclip-remove": "\U000F1ACA", + "parachute": "\U000F0CB4", + "parachute-outline": "\U000F0CB5", + "paragliding": "\U000F1745", + "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", + "paw-off-outline": "\U000F1676", + "paw-outline": "\U000F1675", + "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", + "pentagram": "\U000F1667", + "percent": "\U000F03F0", + "percent-box": "\U000F1A02", + "percent-box-outline": "\U000F1A03", + "percent-circle": "\U000F1A04", + "percent-circle-outline": "\U000F1A05", + "percent-outline": "\U000F1278", + "periodic-table": "\U000F08B6", + "perspective-less": "\U000F0D23", + "perspective-more": "\U000F0D24", + "ph": "\U000F17C5", + "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-clock": "\U000F19DB", + "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-outgoing": "\U000F1B3F", + "phone-incoming-outgoing-outline": "\U000F1B40", + "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-refresh": "\U000F1993", + "phone-refresh-outline": "\U000F1994", + "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-sync": "\U000F1995", + "phone-sync-outline": "\U000F1996", + "phone-voip": "\U000F03FE", + "pi": "\U000F03FF", + "pi-box": "\U000F0400", + "pi-hole": "\U000F0DF1", + "piano": "\U000F067D", + "piano-off": "\U000F0698", + "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", + "pig-variant-outline": "\U000F1678", + "piggy-bank": "\U000F1007", + "piggy-bank-outline": "\U000F1679", + "pill": "\U000F0402", + "pill-multiple": "\U000F1B4C", + "pill-off": "\U000F1A5C", + "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-valve": "\U000F184D", + "pipe-wrench": "\U000F1354", + "pirate": "\U000F0A08", + "pistol": "\U000F0703", + "piston": "\U000F088A", + "pitchfork": "\U000F1553", + "pizza": "\U000F0409", + "plane-car": "\U000F1AFF", + "plane-train": "\U000F1B00", + "play": "\U000F040A", + "play-box": "\U000F127A", + "play-box-lock": "\U000F1A16", + "play-box-lock-open": "\U000F1A17", + "play-box-lock-open-outline": "\U000F1A18", + "play-box-lock-outline": "\U000F1A19", + "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", + "pliers": "\U000F19A4", + "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-lock": "\U000F1A5D", + "plus-lock-open": "\U000F1A5E", + "plus-minus": "\U000F0992", + "plus-minus-box": "\U000F0993", + "plus-minus-variant": "\U000F14C9", + "plus-network": "\U000F041A", + "plus-network-outline": "\U000F0CBA", + "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", + "police-station": "\U000F1839", + "poll": "\U000F041F", + "polo": "\U000F14C3", + "polymer": "\U000F0421", + "pool": "\U000F0606", + "pool-thermometer": "\U000F1A5F", + "popcorn": "\U000F0422", + "post": "\U000F1008", + "post-lamp": "\U000F1A60", + "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-ch": "\U000F0FB3", + "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-heat": "\U000F18B8", + "printer-3d-nozzle-heat-outline": "\U000F18B9", + "printer-3d-nozzle-off": "\U000F1B19", + "printer-3d-nozzle-off-outline": "\U000F1B1A", + "printer-3d-nozzle-outline": "\U000F0E5C", + "printer-3d-off": "\U000F1B0E", + "printer-alert": "\U000F042C", + "printer-check": "\U000F1146", + "printer-eye": "\U000F1458", + "printer-off": "\U000F0E5D", + "printer-off-outline": "\U000F1785", + "printer-outline": "\U000F1786", + "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-pencil": "\U000F1787", + "progress-question": "\U000F1522", + "progress-star": "\U000F1788", + "progress-upload": "\U000F0998", + "progress-wrench": "\U000F0CBD", + "projector": "\U000F042E", + "projector-off": "\U000F1A23", + "projector-screen": "\U000F042F", + "projector-screen-off": "\U000F180D", + "projector-screen-off-outline": "\U000F180E", + "projector-screen-outline": "\U000F1724", + "projector-screen-variant": "\U000F180F", + "projector-screen-variant-off": "\U000F1810", + "projector-screen-variant-off-outline": "\U000F1811", + "projector-screen-variant-outline": "\U000F1812", + "propane-tank": "\U000F1357", + "propane-tank-outline": "\U000F1358", + "protocol": "\U000F0FD8", + "publish": "\U000F06A7", + "publish-off": "\U000F1945", + "pulse": "\U000F0430", + "pump": "\U000F1402", + "pump-off": "\U000F1B22", + "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", + "pyramid": "\U000F1952", + "pyramid-off": "\U000F1953", + "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", + "rabbit-variant": "\U000F1A61", + "rabbit-variant-outline": "\U000F1A62", + "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-circle": "\U000F185D", + "radioactive-circle-outline": "\U000F185E", + "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", + "raw": "\U000F1A0F", + "raw-off": "\U000F1A10", + "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", + "razor-double-edge": "\U000F1997", + "razor-single-edge": "\U000F1998", + "react": "\U000F0708", + "read": "\U000F0447", + "receipt": "\U000F0449", + "receipt-outline": "\U000F19DC", + "receipt-text-check": "\U000F1A63", + "receipt-text-check-outline": "\U000F1A64", + "receipt-text-minus": "\U000F1A65", + "receipt-text-minus-outline": "\U000F1A66", + "receipt-text-plus": "\U000F1A67", + "receipt-text-plus-outline": "\U000F1A68", + "receipt-text-remove": "\U000F1A69", + "receipt-text-remove-outline": "\U000F1A6A", + "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-auto": "\U000F18F2", + "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", + "repeat-variant": "\U000F0547", + "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-15": "\U000F1946", + "rewind-30": "\U000F0D96", + "rewind-45": "\U000F1B13", + "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-angry": "\U000F169D", + "robot-angry-outline": "\U000F169E", + "robot-confused": "\U000F169F", + "robot-confused-outline": "\U000F16A0", + "robot-dead": "\U000F16A1", + "robot-dead-outline": "\U000F16A2", + "robot-excited": "\U000F16A3", + "robot-excited-outline": "\U000F16A4", + "robot-happy": "\U000F1719", + "robot-happy-outline": "\U000F171A", + "robot-industrial": "\U000F0B46", + "robot-industrial-outline": "\U000F1A1A", + "robot-love": "\U000F16A5", + "robot-love-outline": "\U000F16A6", + "robot-mower": "\U000F11F7", + "robot-mower-outline": "\U000F11F3", + "robot-off": "\U000F16A7", + "robot-off-outline": "\U000F167B", + "robot-outline": "\U000F167A", + "robot-vacuum": "\U000F070D", + "robot-vacuum-variant": "\U000F0908", + "rocket": "\U000F0463", + "rocket-launch": "\U000F14DE", + "rocket-launch-outline": "\U000F14DF", + "rocket-outline": "\U000F13AF", + "rodent": "\U000F1327", + "roller-shade": "\U000F1A6B", + "roller-shade-closed": "\U000F1A6C", + "roller-skate": "\U000F0D2B", + "roller-skate-off": "\U000F0145", + "rollerblade": "\U000F0D2C", + "rollerblade-off": "\U000F002E", + "rollupjs": "\U000F0BC0", + "rolodex": "\U000F1AB9", + "rolodex-outline": "\U000F1ABA", + "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-360": "\U000F1999", + "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", + "sail-boat-sink": "\U000F1AEF", + "sale": "\U000F046F", + "sale-outline": "\U000F1A06", + "salesforce": "\U000F088E", + "sass": "\U000F07EC", + "satellite": "\U000F0470", + "satellite-uplink": "\U000F0909", + "satellite-variant": "\U000F0471", + "sausage": "\U000F08BA", + "sausage-off": "\U000F1789", + "saw-blade": "\U000F0E61", + "sawtooth-wave": "\U000F147A", + "saxophone": "\U000F0609", + "scale": "\U000F0472", + "scale-balance": "\U000F05D1", + "scale-bathroom": "\U000F0473", + "scale-off": "\U000F105A", + "scale-unbalanced": "\U000F19B8", + "scan-helper": "\U000F13D8", + "scanner": "\U000F06AB", + "scanner-off": "\U000F090A", + "scatter-plot": "\U000F0EC9", + "scatter-plot-outline": "\U000F0ECA", + "scent": "\U000F1958", + "scent-off": "\U000F1959", + "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-key": "\U000F1725", + "script-text-key-outline": "\U000F1726", + "script-text-outline": "\U000F0BC3", + "script-text-play": "\U000F1727", + "script-text-play-outline": "\U000F1728", + "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", + "seed-plus": "\U000F1A6D", + "seed-plus-outline": "\U000F1A6E", + "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-remove": "\U000F17C1", + "select-search": "\U000F1204", + "selection": "\U000F0489", + "selection-drag": "\U000F0A6D", + "selection-ellipse": "\U000F0D32", + "selection-ellipse-arrow-inside": "\U000F0F22", + "selection-ellipse-remove": "\U000F17C2", + "selection-marker": "\U000F1283", + "selection-multiple": "\U000F1285", + "selection-multiple-marker": "\U000F1284", + "selection-off": "\U000F0777", + "selection-remove": "\U000F17C3", + "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", + "shark": "\U000F18BA", + "shark-fin": "\U000F1673", + "shark-fin-outline": "\U000F1674", + "shark-off": "\U000F18BB", + "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-crown": "\U000F18BC", + "shield-crown-outline": "\U000F18BD", + "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-open": "\U000F199A", + "shield-lock-open-outline": "\U000F199B", + "shield-lock-outline": "\U000F0CCC", + "shield-moon": "\U000F1828", + "shield-moon-outline": "\U000F1829", + "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-sword": "\U000F18BE", + "shield-sword-outline": "\U000F18BF", + "shield-sync": "\U000F11A2", + "shield-sync-outline": "\U000F11A3", + "shimmer": "\U000F1545", + "ship-wheel": "\U000F0833", + "shipping-pallet": "\U000F184E", + "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", + "shopping-search-outline": "\U000F1A6F", + "shore": "\U000F14F9", + "shovel": "\U000F0710", + "shovel-off": "\U000F0711", + "shower": "\U000F09A0", + "shower-head": "\U000F09A1", + "shredder": "\U000F049C", + "shuffle": "\U000F049D", + "shuffle-disabled": "\U000F049E", + "shuffle-variant": "\U000F049F", + "shuriken": "\U000F137F", + "sickle": "\U000F18C0", + "sigma": "\U000F04A0", + "sigma-lower": "\U000F062B", + "sign-caution": "\U000F04A1", + "sign-direction": "\U000F0781", + "sign-direction-minus": "\U000F1000", + "sign-direction-plus": "\U000F0FDC", + "sign-direction-remove": "\U000F0FDD", + "sign-language": "\U000F1B4D", + "sign-language-outline": "\U000F1B4E", + "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", + "sitemap-outline": "\U000F199C", + "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", + "skate-off": "\U000F0699", + "skateboard": "\U000F14C2", + "skateboarding": "\U000F0501", + "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", + "sledding": "\U000F041B", + "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-off": "\U000F18F7", + "smart-card-off-outline": "\U000F18F8", + "smart-card-outline": "\U000F10BE", + "smart-card-reader": "\U000F10BF", + "smart-card-reader-outline": "\U000F10C0", + "smog": "\U000F0A71", + "smoke": "\U000F1799", + "smoke-detector": "\U000F0392", + "smoke-detector-alert": "\U000F192E", + "smoke-detector-alert-outline": "\U000F192F", + "smoke-detector-off": "\U000F1809", + "smoke-detector-off-outline": "\U000F180A", + "smoke-detector-outline": "\U000F1808", + "smoke-detector-variant": "\U000F180B", + "smoke-detector-variant-alert": "\U000F1930", + "smoke-detector-variant-off": "\U000F180C", + "smoking": "\U000F04B4", + "smoking-off": "\U000F04B5", + "smoking-pipe": "\U000F140D", + "smoking-pipe-off": "\U000F1428", + "snail": "\U000F1677", + "snake": "\U000F150E", + "snapchat": "\U000F04B6", + "snowboard": "\U000F1307", + "snowflake": "\U000F0717", + "snowflake-alert": "\U000F0F29", + "snowflake-check": "\U000F1A70", + "snowflake-melt": "\U000F12CB", + "snowflake-off": "\U000F14E3", + "snowflake-thermometer": "\U000F1A71", + "snowflake-variant": "\U000F0F2A", + "snowman": "\U000F04B7", + "snowmobile": "\U000F06DD", + "snowshoeing": "\U000F1A72", + "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", + "solar-power-variant": "\U000F1A73", + "solar-power-variant-outline": "\U000F1A74", + "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-off": "\U000F1ABB", + "sort-variant-remove": "\U000F1147", + "soundbar": "\U000F17DB", + "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", + "speaker": "\U000F04C3", + "speaker-bluetooth": "\U000F09A2", + "speaker-message": "\U000F1B11", + "speaker-multiple": "\U000F0D38", + "speaker-off": "\U000F04C4", + "speaker-wireless": "\U000F071F", + "spear": "\U000F1845", + "speedometer": "\U000F04C5", + "speedometer-medium": "\U000F0F85", + "speedometer-slow": "\U000F0F86", + "spellcheck": "\U000F04C6", + "sphere": "\U000F1954", + "sphere-off": "\U000F1955", + "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-fire": "\U000F199D", + "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-opacity": "\U000F1854", + "square-outline": "\U000F0763", + "square-root": "\U000F0784", + "square-root-box": "\U000F09A3", + "square-rounded": "\U000F14FB", + "square-rounded-badge": "\U000F1A07", + "square-rounded-badge-outline": "\U000F1A08", + "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-outline": "\U000F1B03", + "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-cog": "\U000F1668", + "star-cog-outline": "\U000F1669", + "star-crescent": "\U000F0979", + "star-david": "\U000F097A", + "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-settings": "\U000F166A", + "star-settings-outline": "\U000F166B", + "star-shooting": "\U000F1741", + "star-shooting-outline": "\U000F1742", + "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", + "sticker-text": "\U000F178E", + "sticker-text-outline": "\U000F178F", + "stocking": "\U000F04DA", + "stomach": "\U000F1093", + "stool": "\U000F195D", + "stool-outline": "\U000F195E", + "stop": "\U000F04DB", + "stop-circle": "\U000F0666", + "stop-circle-outline": "\U000F0667", + "storage-tank": "\U000F1A75", + "storage-tank-outline": "\U000F1A76", + "store": "\U000F04DC", + "store-24-hour": "\U000F04DD", + "store-alert": "\U000F18C1", + "store-alert-outline": "\U000F18C2", + "store-check": "\U000F18C3", + "store-check-outline": "\U000F18C4", + "store-clock": "\U000F18C5", + "store-clock-outline": "\U000F18C6", + "store-cog": "\U000F18C7", + "store-cog-outline": "\U000F18C8", + "store-edit": "\U000F18C9", + "store-edit-outline": "\U000F18CA", + "store-marker": "\U000F18CB", + "store-marker-outline": "\U000F18CC", + "store-minus": "\U000F165E", + "store-minus-outline": "\U000F18CD", + "store-off": "\U000F18CE", + "store-off-outline": "\U000F18CF", + "store-outline": "\U000F1361", + "store-plus": "\U000F165F", + "store-plus-outline": "\U000F18D0", + "store-remove": "\U000F1660", + "store-remove-outline": "\U000F18D1", + "store-search": "\U000F18D2", + "store-search-outline": "\U000F18D3", + "store-settings": "\U000F18D4", + "store-settings-outline": "\U000F18D5", + "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", + "sun-angle": "\U000F1B27", + "sun-angle-outline": "\U000F1B28", + "sun-clock": "\U000F1A77", + "sun-clock-outline": "\U000F1A78", + "sun-compass": "\U000F19A5", + "sun-snowflake": "\U000F1796", + "sun-snowflake-variant": "\U000F1A79", + "sun-thermometer": "\U000F18D6", + "sun-thermometer-outline": "\U000F18D7", + "sun-wireless": "\U000F17FE", + "sun-wireless-outline": "\U000F17FF", + "sunglasses": "\U000F04E0", + "surfing": "\U000F1746", + "surround-sound": "\U000F05C5", + "surround-sound-2-0": "\U000F07F0", + "surround-sound-2-1": "\U000F1729", + "surround-sound-3-1": "\U000F07F1", + "surround-sound-5-1": "\U000F07F2", + "surround-sound-5-1-2": "\U000F172A", + "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", + "synagogue": "\U000F1B04", + "synagogue-outline": "\U000F1B05", + "sync": "\U000F04E6", + "sync-alert": "\U000F04E7", + "sync-circle": "\U000F1378", + "sync-off": "\U000F04E8", + "tab": "\U000F04E9", + "tab-minus": "\U000F0B4B", + "tab-plus": "\U000F075C", + "tab-remove": "\U000F0B4C", + "tab-search": "\U000F199E", + "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-picnic": "\U000F1743", + "table-pivot": "\U000F183C", + "table-plus": "\U000F0A75", + "table-question": "\U000F1B21", + "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", + "taco": "\U000F0762", + "tag": "\U000F04F9", + "tag-arrow-down": "\U000F172B", + "tag-arrow-down-outline": "\U000F172C", + "tag-arrow-left": "\U000F172D", + "tag-arrow-left-outline": "\U000F172E", + "tag-arrow-right": "\U000F172F", + "tag-arrow-right-outline": "\U000F1730", + "tag-arrow-up": "\U000F1731", + "tag-arrow-up-outline": "\U000F1732", + "tag-check": "\U000F1A7A", + "tag-check-outline": "\U000F1A7B", + "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-search": "\U000F1907", + "tag-search-outline": "\U000F1908", + "tag-text": "\U000F1224", + "tag-text-outline": "\U000F04FD", + "tailwind": "\U000F13FF", + "tally-mark-1": "\U000F1ABC", + "tally-mark-2": "\U000F1ABD", + "tally-mark-3": "\U000F1ABE", + "tally-mark-4": "\U000F1ABF", + "tally-mark-5": "\U000F1AC0", + "tangram": "\U000F04F8", + "tank": "\U000F0D3A", + "tanker-truck": "\U000F0FE5", + "tape-drive": "\U000F16DF", + "tape-measure": "\U000F0B4D", + "target": "\U000F04FE", + "target-account": "\U000F0BD0", + "target-variant": "\U000F0A77", + "taxi": "\U000F04FF", + "tea": "\U000F0D9E", + "tea-outline": "\U000F0D9F", + "teamviewer": "\U000F0500", + "teddy-bear": "\U000F18FB", + "telescope": "\U000F0B4E", + "television": "\U000F0502", + "television-ambient-light": "\U000F1356", + "television-box": "\U000F0839", + "television-classic": "\U000F07F4", + "television-classic-off": "\U000F083A", + "television-guide": "\U000F0503", + "television-off": "\U000F083B", + "television-pause": "\U000F0F89", + "television-play": "\U000F0ECF", + "television-shimmer": "\U000F1110", + "television-speaker": "\U000F1B1B", + "television-speaker-off": "\U000F1B1C", + "television-stop": "\U000F0F8A", + "temperature-celsius": "\U000F0504", + "temperature-fahrenheit": "\U000F0505", + "temperature-kelvin": "\U000F0506", + "temple-buddhist": "\U000F1B06", + "temple-buddhist-outline": "\U000F1B07", + "temple-hindu": "\U000F1B08", + "temple-hindu-outline": "\U000F1B09", + "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-edit": "\U000F1A7C", + "text-box-edit-outline": "\U000F1A7D", + "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-long": "\U000F09AA", + "text-recognition": "\U000F113D", + "text-search": "\U000F13B8", + "text-search-variant": "\U000F1A7E", + "text-shadow": "\U000F0669", + "text-short": "\U000F09A9", + "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-auto": "\U000F1B0F", + "thermometer-bluetooth": "\U000F1895", + "thermometer-check": "\U000F1A7F", + "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", + "thermometer-probe": "\U000F1B2B", + "thermometer-probe-off": "\U000F1B2C", + "thermometer-water": "\U000F1A80", + "thermostat": "\U000F0393", + "thermostat-auto": "\U000F1B17", + "thermostat-box": "\U000F0891", + "thermostat-box-auto": "\U000F1B18", + "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", + "thumbs-up-down-outline": "\U000F1914", + "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", + "tilde-off": "\U000F18F3", + "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-alert": "\U000F1ACC", + "timer-alert-outline": "\U000F1ACD", + "timer-cancel": "\U000F1ACE", + "timer-cancel-outline": "\U000F1ACF", + "timer-check": "\U000F1AD0", + "timer-check-outline": "\U000F1AD1", + "timer-cog": "\U000F1925", + "timer-cog-outline": "\U000F1926", + "timer-edit": "\U000F1AD2", + "timer-edit-outline": "\U000F1AD3", + "timer-lock": "\U000F1AD4", + "timer-lock-open": "\U000F1AD5", + "timer-lock-open-outline": "\U000F1AD6", + "timer-lock-outline": "\U000F1AD7", + "timer-marker": "\U000F1AD8", + "timer-marker-outline": "\U000F1AD9", + "timer-minus": "\U000F1ADA", + "timer-minus-outline": "\U000F1ADB", + "timer-music": "\U000F1ADC", + "timer-music-outline": "\U000F1ADD", + "timer-off": "\U000F13AC", + "timer-off-outline": "\U000F051E", + "timer-outline": "\U000F051B", + "timer-pause": "\U000F1ADE", + "timer-pause-outline": "\U000F1ADF", + "timer-play": "\U000F1AE0", + "timer-play-outline": "\U000F1AE1", + "timer-plus": "\U000F1AE2", + "timer-plus-outline": "\U000F1AE3", + "timer-refresh": "\U000F1AE4", + "timer-refresh-outline": "\U000F1AE5", + "timer-remove": "\U000F1AE6", + "timer-remove-outline": "\U000F1AE7", + "timer-sand": "\U000F051F", + "timer-sand-complete": "\U000F199F", + "timer-sand-empty": "\U000F06AD", + "timer-sand-full": "\U000F078C", + "timer-sand-paused": "\U000F19A0", + "timer-settings": "\U000F1923", + "timer-settings-outline": "\U000F1924", + "timer-star": "\U000F1AE8", + "timer-star-outline": "\U000F1AE9", + "timer-stop": "\U000F1AEA", + "timer-stop-outline": "\U000F1AEB", + "timer-sync": "\U000F1AEC", + "timer-sync-outline": "\U000F1AED", + "timetable": "\U000F0520", + "tire": "\U000F1896", + "toaster": "\U000F1063", + "toaster-off": "\U000F11B7", + "toaster-oven": "\U000F0CD3", + "toggle-switch": "\U000F0521", + "toggle-switch-off": "\U000F0522", + "toggle-switch-off-outline": "\U000F0A19", + "toggle-switch-outline": "\U000F0A1A", + "toggle-switch-variant": "\U000F1A25", + "toggle-switch-variant-off": "\U000F1A26", + "toilet": "\U000F09AB", + "toolbox": "\U000F09AC", + "toolbox-outline": "\U000F09AD", + "tools": "\U000F1064", + "tooltip": "\U000F0523", + "tooltip-account": "\U000F000C", + "tooltip-cellphone": "\U000F183B", + "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", + "town-hall": "\U000F1875", + "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", + "track-light-off": "\U000F1B01", + "trackpad": "\U000F07F8", + "trackpad-lock": "\U000F0933", + "tractor": "\U000F0892", + "tractor-variant": "\U000F14C4", + "trademark": "\U000F0A78", + "traffic-cone": "\U000F137C", + "traffic-light": "\U000F052B", + "traffic-light-outline": "\U000F182A", + "train": "\U000F052C", + "train-car": "\U000F0BD8", + "train-car-autorack": "\U000F1B2D", + "train-car-box": "\U000F1B2E", + "train-car-box-full": "\U000F1B2F", + "train-car-box-open": "\U000F1B30", + "train-car-caboose": "\U000F1B31", + "train-car-centerbeam": "\U000F1B32", + "train-car-centerbeam-full": "\U000F1B33", + "train-car-container": "\U000F1B34", + "train-car-flatbed": "\U000F1B35", + "train-car-flatbed-car": "\U000F1B36", + "train-car-flatbed-tank": "\U000F1B37", + "train-car-gondola": "\U000F1B38", + "train-car-gondola-full": "\U000F1B39", + "train-car-hopper": "\U000F1B3A", + "train-car-hopper-covered": "\U000F1B3B", + "train-car-hopper-full": "\U000F1B3C", + "train-car-intermodal": "\U000F1B3D", + "train-car-passenger": "\U000F1733", + "train-car-passenger-door": "\U000F1734", + "train-car-passenger-door-open": "\U000F1735", + "train-car-passenger-variant": "\U000F1736", + "train-car-tank": "\U000F1B3E", + "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", + "transmission-tower-export": "\U000F192C", + "transmission-tower-import": "\U000F192D", + "transmission-tower-off": "\U000F19DD", + "trash-can": "\U000F0A79", + "trash-can-outline": "\U000F0A7A", + "tray": "\U000F1294", + "tray-alert": "\U000F1295", + "tray-arrow-down": "\U000F0120", + "tray-arrow-up": "\U000F011D", + "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-small-down": "\U000F1A09", + "triangle-small-up": "\U000F1A0A", + "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-alert": "\U000F19DE", + "truck-alert-outline": "\U000F19DF", + "truck-cargo-container": "\U000F18D8", + "truck-check": "\U000F0CD4", + "truck-check-outline": "\U000F129A", + "truck-delivery": "\U000F053E", + "truck-delivery-outline": "\U000F129B", + "truck-fast": "\U000F0788", + "truck-fast-outline": "\U000F129C", + "truck-flatbed": "\U000F1891", + "truck-minus": "\U000F19AE", + "truck-minus-outline": "\U000F19BD", + "truck-outline": "\U000F129D", + "truck-plus": "\U000F19AD", + "truck-plus-outline": "\U000F19BC", + "truck-remove": "\U000F19AF", + "truck-remove-outline": "\U000F19BE", + "truck-snowflake": "\U000F19A6", + "truck-trailer": "\U000F0727", + "trumpet": "\U000F1096", + "tshirt-crew": "\U000F0A7B", + "tshirt-crew-outline": "\U000F053F", + "tshirt-v": "\U000F0A7C", + "tshirt-v-outline": "\U000F0540", + "tsunami": "\U000F1A81", + "tumble-dryer": "\U000F0917", + "tumble-dryer-alert": "\U000F11BA", + "tumble-dryer-off": "\U000F11BB", + "tune": "\U000F062E", + "tune-variant": "\U000F1542", + "tune-vertical": "\U000F066A", + "tune-vertical-variant": "\U000F1543", + "tunnel": "\U000F183D", + "tunnel-outline": "\U000F183E", + "turbine": "\U000F1A82", + "turkey": "\U000F171B", + "turnstile": "\U000F0CD5", + "turnstile-outline": "\U000F0CD6", + "turtle": "\U000F0CD7", + "twitch": "\U000F0543", + "twitter": "\U000F0544", + "two-factor-authentication": "\U000F09AF", + "typewriter": "\U000F0F2D", + "ubisoft": "\U000F0BDA", + "ubuntu": "\U000F0548", + "ufo": "\U000F10C4", + "ufo-outline": "\U000F10C5", + "ultra-high-definition": "\U000F07F9", + "umbraco": "\U000F0549", + "umbrella": "\U000F054A", + "umbrella-beach": "\U000F188A", + "umbrella-beach-outline": "\U000F188B", + "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", + "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", + "vacuum": "\U000F19A1", + "vacuum-outline": "\U000F19A2", + "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-polygon-variant": "\U000F1856", + "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-square-close": "\U000F1857", + "vector-square-edit": "\U000F18D9", + "vector-square-minus": "\U000F18DA", + "vector-square-open": "\U000F1858", + "vector-square-plus": "\U000F18DB", + "vector-square-remove": "\U000F18DC", + "vector-triangle": "\U000F0563", + "vector-union": "\U000F0564", + "vhs": "\U000F0A1B", + "vibrate": "\U000F0566", + "vibrate-off": "\U000F0CD9", + "video": "\U000F0567", + "video-2d": "\U000F1A1C", + "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-marker": "\U000F19A9", + "video-marker-outline": "\U000F19AA", + "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-edit": "\U000F1947", + "view-dashboard-edit-outline": "\U000F1948", + "view-dashboard-outline": "\U000F0A1D", + "view-dashboard-variant": "\U000F0843", + "view-dashboard-variant-outline": "\U000F1489", + "view-day": "\U000F056F", + "view-day-outline": "\U000F148A", + "view-gallery": "\U000F1888", + "view-gallery-outline": "\U000F1889", + "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-off": "\U000F18E1", + "virus-off-outline": "\U000F18E2", + "virus-outline": "\U000F13B7", + "vlc": "\U000F057C", + "voicemail": "\U000F057D", + "volcano": "\U000F1A83", + "volcano-outline": "\U000F1A84", + "volleyball": "\U000F09B4", + "volume-equal": "\U000F1B10", + "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-fire": "\U000F1A11", + "wall-sconce": "\U000F091C", + "wall-sconce-flat": "\U000F091D", + "wall-sconce-flat-outline": "\U000F17C9", + "wall-sconce-flat-variant": "\U000F041C", + "wall-sconce-flat-variant-outline": "\U000F17CA", + "wall-sconce-outline": "\U000F17CB", + "wall-sconce-round": "\U000F0748", + "wall-sconce-round-outline": "\U000F17CC", + "wall-sconce-round-variant": "\U000F091E", + "wall-sconce-round-variant-outline": "\U000F17CD", + "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-circle": "\U000F1806", + "water-minus": "\U000F1506", + "water-minus-outline": "\U000F1507", + "water-off": "\U000F058D", + "water-off-outline": "\U000F1508", + "water-opacity": "\U000F1855", + "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-sync": "\U000F17C6", + "water-thermometer": "\U000F1A85", + "water-thermometer-outline": "\U000F1A86", + "water-well": "\U000F106B", + "water-well-outline": "\U000F106C", + "waterfall": "\U000F1849", + "watering-can": "\U000F1481", + "watering-can-outline": "\U000F1482", + "watermark": "\U000F0612", + "wave": "\U000F0F2E", + "waveform": "\U000F147D", + "waves": "\U000F078D", + "waves-arrow-left": "\U000F1859", + "waves-arrow-right": "\U000F185A", + "waves-arrow-up": "\U000F185B", + "waze": "\U000F0BDE", + "weather-cloudy": "\U000F0590", + "weather-cloudy-alert": "\U000F0F2F", + "weather-cloudy-arrow-right": "\U000F0E6E", + "weather-cloudy-clock": "\U000F18F6", + "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-cancel": "\U000F1790", + "web-check": "\U000F0789", + "web-clock": "\U000F124A", + "web-minus": "\U000F10A0", + "web-off": "\U000F0A8E", + "web-plus": "\U000F0033", + "web-refresh": "\U000F1791", + "web-remove": "\U000F0551", + "web-sync": "\U000F1792", + "webcam": "\U000F05A0", + "webcam-off": "\U000F1737", + "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": "\U000F1A87", + "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-alert": "\U000F16B5", + "wifi-arrow-down": "\U000F16B6", + "wifi-arrow-left": "\U000F16B7", + "wifi-arrow-left-right": "\U000F16B8", + "wifi-arrow-right": "\U000F16B9", + "wifi-arrow-up": "\U000F16BA", + "wifi-arrow-up-down": "\U000F16BB", + "wifi-cancel": "\U000F16BC", + "wifi-check": "\U000F16BD", + "wifi-cog": "\U000F16BE", + "wifi-lock": "\U000F16BF", + "wifi-lock-open": "\U000F16C0", + "wifi-marker": "\U000F16C1", + "wifi-minus": "\U000F16C2", + "wifi-off": "\U000F05AA", + "wifi-plus": "\U000F16C3", + "wifi-refresh": "\U000F16C4", + "wifi-remove": "\U000F16C5", + "wifi-settings": "\U000F16C6", + "wifi-star": "\U000F0E0B", + "wifi-strength-1": "\U000F091F", + "wifi-strength-1-alert": "\U000F0920", + "wifi-strength-1-lock": "\U000F0921", + "wifi-strength-1-lock-open": "\U000F16CB", + "wifi-strength-2": "\U000F0922", + "wifi-strength-2-alert": "\U000F0923", + "wifi-strength-2-lock": "\U000F0924", + "wifi-strength-2-lock-open": "\U000F16CC", + "wifi-strength-3": "\U000F0925", + "wifi-strength-3-alert": "\U000F0926", + "wifi-strength-3-lock": "\U000F0927", + "wifi-strength-3-lock-open": "\U000F16CD", + "wifi-strength-4": "\U000F0928", + "wifi-strength-4-alert": "\U000F0929", + "wifi-strength-4-lock": "\U000F092A", + "wifi-strength-4-lock-open": "\U000F16CE", + "wifi-strength-alert-outline": "\U000F092B", + "wifi-strength-lock-open-outline": "\U000F16CF", + "wifi-strength-lock-outline": "\U000F092C", + "wifi-strength-off": "\U000F092D", + "wifi-strength-off-outline": "\U000F092E", + "wifi-strength-outline": "\U000F092F", + "wifi-sync": "\U000F16C7", + "wikipedia": "\U000F05AC", + "wind-power": "\U000F1A88", + "wind-power-outline": "\U000F1A89", + "wind-turbine": "\U000F0DA5", + "wind-turbine-alert": "\U000F19AB", + "wind-turbine-check": "\U000F19AC", + "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-cog": "\U000F1A8A", + "window-shutter-open": "\U000F111E", + "window-shutter-settings": "\U000F1A8B", + "windsock": "\U000F15FA", + "wiper": "\U000F0AE9", + "wiper-wash": "\U000F0DA6", + "wiper-wash-alert": "\U000F18DF", + "wizard-hat": "\U000F1477", + "wordpress": "\U000F05B4", + "wrap": "\U000F05B6", + "wrap-disabled": "\U000F0BDF", + "wrench": "\U000F05B7", + "wrench-clock": "\U000F19A3", + "wrench-outline": "\U000F0BE0", + "xamarin": "\U000F0845", + "xml": "\U000F05C0", + "xmpp": "\U000F07FF", + "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/sbapp/kivymd/images/alpha_layer.png b/sbapp/kivymd/images/alpha_layer.png new file mode 100644 index 0000000..70cc2eb Binary files /dev/null and b/sbapp/kivymd/images/alpha_layer.png differ diff --git a/sbapp/kivymd/images/black.png b/sbapp/kivymd/images/black.png new file mode 100644 index 0000000..0380795 Binary files /dev/null and b/sbapp/kivymd/images/black.png differ diff --git a/sbapp/kivymd/images/blue.png b/sbapp/kivymd/images/blue.png new file mode 100644 index 0000000..b47c1e7 Binary files /dev/null and b/sbapp/kivymd/images/blue.png differ diff --git a/sbapp/kivymd/images/firebase-logo.png b/sbapp/kivymd/images/firebase-logo.png new file mode 100644 index 0000000..68674e6 Binary files /dev/null and b/sbapp/kivymd/images/firebase-logo.png differ diff --git a/sbapp/kivymd/images/folder.png b/sbapp/kivymd/images/folder.png new file mode 100644 index 0000000..58fed42 Binary files /dev/null and b/sbapp/kivymd/images/folder.png differ diff --git a/sbapp/kivymd/images/green.png b/sbapp/kivymd/images/green.png new file mode 100644 index 0000000..797f279 Binary files /dev/null and b/sbapp/kivymd/images/green.png differ diff --git a/sbapp/kivymd/images/quad_shadow-0.png b/sbapp/kivymd/images/quad_shadow-0.png new file mode 100644 index 0000000..f847665 Binary files /dev/null and b/sbapp/kivymd/images/quad_shadow-0.png differ diff --git a/sbapp/kivymd/images/quad_shadow-1.png b/sbapp/kivymd/images/quad_shadow-1.png new file mode 100644 index 0000000..fea057f Binary files /dev/null and b/sbapp/kivymd/images/quad_shadow-1.png differ diff --git a/sbapp/kivymd/images/quad_shadow-2.png b/sbapp/kivymd/images/quad_shadow-2.png new file mode 100644 index 0000000..1949d5e Binary files /dev/null and b/sbapp/kivymd/images/quad_shadow-2.png differ diff --git a/sbapp/kivymd/images/quad_shadow.atlas b/sbapp/kivymd/images/quad_shadow.atlas new file mode 100644 index 0000000..68e0aad --- /dev/null +++ b/sbapp/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/sbapp/kivymd/images/rec_shadow-0.png b/sbapp/kivymd/images/rec_shadow-0.png new file mode 100644 index 0000000..f02b919 Binary files /dev/null and b/sbapp/kivymd/images/rec_shadow-0.png differ diff --git a/sbapp/kivymd/images/rec_shadow-1.png b/sbapp/kivymd/images/rec_shadow-1.png new file mode 100644 index 0000000..f752fd2 Binary files /dev/null and b/sbapp/kivymd/images/rec_shadow-1.png differ diff --git a/sbapp/kivymd/images/rec_shadow.atlas b/sbapp/kivymd/images/rec_shadow.atlas new file mode 100644 index 0000000..71b0e9d --- /dev/null +++ b/sbapp/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/sbapp/kivymd/images/rec_st_shadow-0.png b/sbapp/kivymd/images/rec_st_shadow-0.png new file mode 100644 index 0000000..887327d Binary files /dev/null and b/sbapp/kivymd/images/rec_st_shadow-0.png differ diff --git a/sbapp/kivymd/images/rec_st_shadow-1.png b/sbapp/kivymd/images/rec_st_shadow-1.png new file mode 100644 index 0000000..759ee65 Binary files /dev/null and b/sbapp/kivymd/images/rec_st_shadow-1.png differ diff --git a/sbapp/kivymd/images/rec_st_shadow-2.png b/sbapp/kivymd/images/rec_st_shadow-2.png new file mode 100644 index 0000000..e9fdacc Binary files /dev/null and b/sbapp/kivymd/images/rec_st_shadow-2.png differ diff --git a/sbapp/kivymd/images/rec_st_shadow.atlas b/sbapp/kivymd/images/rec_st_shadow.atlas new file mode 100644 index 0000000..d4c24ab --- /dev/null +++ b/sbapp/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/sbapp/kivymd/images/red.png b/sbapp/kivymd/images/red.png new file mode 100644 index 0000000..eac94b1 Binary files /dev/null and b/sbapp/kivymd/images/red.png differ diff --git a/sbapp/kivymd/images/restdb-logo.png b/sbapp/kivymd/images/restdb-logo.png new file mode 100644 index 0000000..01adbcc Binary files /dev/null and b/sbapp/kivymd/images/restdb-logo.png differ diff --git a/sbapp/kivymd/images/round_shadow-0.png b/sbapp/kivymd/images/round_shadow-0.png new file mode 100644 index 0000000..26d9840 Binary files /dev/null and b/sbapp/kivymd/images/round_shadow-0.png differ diff --git a/sbapp/kivymd/images/round_shadow-1.png b/sbapp/kivymd/images/round_shadow-1.png new file mode 100644 index 0000000..d0f4c0f Binary files /dev/null and b/sbapp/kivymd/images/round_shadow-1.png differ diff --git a/sbapp/kivymd/images/round_shadow-2.png b/sbapp/kivymd/images/round_shadow-2.png new file mode 100644 index 0000000..d5feef2 Binary files /dev/null and b/sbapp/kivymd/images/round_shadow-2.png differ diff --git a/sbapp/kivymd/images/round_shadow.atlas b/sbapp/kivymd/images/round_shadow.atlas new file mode 100644 index 0000000..f25016d --- /dev/null +++ b/sbapp/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/sbapp/kivymd/images/transparent.png b/sbapp/kivymd/images/transparent.png new file mode 100644 index 0000000..effe1c2 Binary files /dev/null and b/sbapp/kivymd/images/transparent.png differ diff --git a/sbapp/kivymd/images/yellow.png b/sbapp/kivymd/images/yellow.png new file mode 100644 index 0000000..6062231 Binary files /dev/null and b/sbapp/kivymd/images/yellow.png differ diff --git a/sbapp/kivymd/material_resources.py b/sbapp/kivymd/material_resources.py new file mode 100755 index 0000000..0a1b811 --- /dev/null +++ b/sbapp/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/sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py b/sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py new file mode 100644 index 0000000..801f5bf --- /dev/null +++ b/sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py @@ -0,0 +1,87 @@ +""" +PyInstaller freezing test +========================= + +PyInstaller must package KivyMD apps correctly. +""" + +import subprocess + +from PyInstaller import __main__ as pyi_main + + +def test_datas(tmp_path) -> None: + """Test fonts and images.""" + + app_name = "userapp" + workpath = tmp_path / "build" + distpath = tmp_path / "dist" + app = tmp_path / (app_name + ".py") + app.write_text( + """ +import os + +from kivy.core.text import LabelBase + +import kivymd + +fonts = os.listdir(kivymd.fonts_path) +print(fonts) +assert "Roboto-Regular.ttf" in fonts +assert "materialdesignicons-webfont.ttf" in fonts +print(LabelBase._fonts.keys()) +assert "Roboto" in LabelBase._fonts.keys() # NOQA +assert "Icons" in LabelBase._fonts.keys() # NOQA + +images = os.listdir(kivymd.images_path) +print(images) +assert "folder.png" in images +assert "rec_shadow.atlas" in images +""" + ) + pyi_main.run( + [ + "--workpath", + str(workpath), + "--distpath", + str(distpath), + "--specpath", + str(tmp_path), + str(app), + ] + ) + subprocess.run([str(distpath / app_name / app_name)], check=True) + + +def test_widgets(tmp_path) -> None: + """Test that all widgets are accesible.""" + + app_name = "userapp" + workpath = tmp_path / "build" + distpath = tmp_path / "dist" + app = tmp_path / (app_name + ".py") + app.write_text( + """ +import os + +import kivymd # NOQA +__import__("kivymd.uix.label") +__import__("kivymd.uix.button") +__import__("kivymd.uix.list") +__import__("kivymd.uix.navigationdrawer") + +print(os.listdir(os.path.dirname(kivymd.uix.__path__[0]))) +""" + ) + pyi_main.run( + [ + "--workpath", + str(workpath), + "--distpath", + str(distpath), + "--specpath", + str(tmp_path), + str(app), + ] + ) + subprocess.run([str(distpath / app_name / app_name)], check=True) diff --git a/sbapp/kivymd/tests/test_app.py b/sbapp/kivymd/tests/test_app.py new file mode 100644 index 0000000..076e9dd --- /dev/null +++ b/sbapp/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/sbapp/kivymd/tests/test_create_project.py b/sbapp/kivymd/tests/test_create_project.py new file mode 100644 index 0000000..3122f1e --- /dev/null +++ b/sbapp/kivymd/tests/test_create_project.py @@ -0,0 +1,16 @@ +def test_create_project(): + import os + import sys + + os.system( + f"{sys.executable} -m kivymd.tools.patterns.create_project " + f"MVC " + f"{os.path.expanduser('~')} " + f"TestProject " + f"{sys.executable} " + f"master " + f"--name_screen TestProjectScreen " + f"--name_database restdb " + f"--use_hotreload yes" + ) + assert os.path.exists(os.path.join(os.path.expanduser("~"), "TestProject")) diff --git a/sbapp/kivymd/tests/test_font_definitions.py b/sbapp/kivymd/tests/test_font_definitions.py new file mode 100644 index 0000000..adf8e75 --- /dev/null +++ b/sbapp/kivymd/tests/test_font_definitions.py @@ -0,0 +1,16 @@ +def test_fonts_registration(): + # This should register fonts: + from kivy.core.text import LabelBase + + import kivymd # NOQA + + fonts = [ + "Roboto", + "RobotoThin", + "RobotoLight", + "RobotoMedium", + "RobotoBlack", + "Icons", + ] + for font in fonts: + assert font in LabelBase._fonts.keys() diff --git a/sbapp/kivymd/tests/test_icon_definitions.py b/sbapp/kivymd/tests/test_icon_definitions.py new file mode 100644 index 0000000..d1f6bab --- /dev/null +++ b/sbapp/kivymd/tests/test_icon_definitions.py @@ -0,0 +1,10 @@ +def test_icons_have_size(): + from kivy.core.text import Label + + from kivymd.icon_definitions import md_icons + + 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/sbapp/kivymd/theming.py b/sbapp/kivymd/theming.py new file mode 100755 index 0000000..2a7a0c6 --- /dev/null +++ b/sbapp/kivymd/theming.py @@ -0,0 +1,1230 @@ +""" +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. + +Changing the theme colors +------------------------- + +The standard theme_cls is designed to provide the standard themes and colors as +defined by Material Design. + +We do not recommend that you change this. + +However, if you do need to change the standard colors, for instance to meet branding +guidelines, you can do this by overloading the `color_definitions.py` object. + +Create a custom color defintion object. This should have the same format as +the `colors `_ +object in `color_definitions.py` and contain definitions for at least the +primary color, the accent color and the Light and Dark backgrounds. + +.. note:: Your custom colors *must* use the names of the `existing colors as + defined in the palette`_ + e.g. You can have `Blue` but you cannot have `NavyBlue`. + +Add the custom theme to the MDApp as shown in the following snippet. + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import ObjectProperty + + from kivymd.app import MDApp + from kivymd.uix.floatlayout import MDFloatLayout + from kivymd.uix.tab import MDTabsBase + from kivymd.icon_definitions import md_icons + + colors = { + "Teal": { + "50": "e4f8f9", + "100": "bdedf0", + "200": "97e2e8", + "300": "79d5de", + "400": "6dcbd6", + "500": "6ac2cf", + "600": "63b2bc", + "700": "5b9ca3", + "800": "54888c", + "900": "486363", + "A100": "bdedf0", + "A200": "97e2e8", + "A400": "6dcbd6", + "A700": "5b9ca3", + }, + "Blue": { + "50": "e3f3f8", + "100": "b9e1ee", + "200": "91cee3", + "300": "72bad6", + "400": "62acce", + "500": "589fc6", + "600": "5191b8", + "700": "487fa5", + "800": "426f91", + "900": "35506d", + "A100": "b9e1ee", + "A200": "91cee3", + "A400": "62acce", + "A700": "487fa5", + }, + "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", + }, + "Light": { + "StatusBar": "E0E0E0", + "AppBar": "F5F5F5", + "Background": "FAFAFA", + "CardsDialogs": "FFFFFF", + "FlatButtonDown": "cccccc", + }, + "Dark": { + "StatusBar": "000000", + "AppBar": "212121", + "Background": "303030", + "CardsDialogs": "424242", + "FlatButtonDown": "999999", + } + } + + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "Example Tabs" + + MDTabs: + id: tabs + + + + + MDIconButton: + id: icon + icon: root.icon + user_font_size: "48sp" + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class Tab(MDFloatLayout, MDTabsBase): + '''Class implementing content for a tab.''' + + icon = ObjectProperty() + + + class Example(MDApp): + icons = list(md_icons.keys())[15:30] + + def build(self): + self.theme_cls.colors = colors + self.theme_cls.primary_palette = "Blue" + self.theme_cls.accent_palette = "Teal" + return Builder.load_string(KV) + + def on_start(self): + for name_tab in self.icons: + tab = Tab(text="This is " + name_tab, icon=name_tab) + self.root.ids.tabs.add_widget(tab) + + + Example().run() + +This will change the theme colors to your custom defintion. In all other +respects, the theming stays as documented. + +.. warning:: Please note that the key ``'Red'`` is a required key for the + dictionary ``colors``. +""" + + +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, + ColorProperty, + DictProperty, + 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.font_definitions import theme_font_styles +from kivymd.material_resources import DEVICE_IOS, DEVICE_TYPE + + +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 kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + from kivymd.uix.button import MDRectangleFlatButton + + + class MainApp(MDApp): + def build(self): + self.theme_cls.primary_palette = "Green" # "Purple", "Red" + screen = MDScreen() + 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 kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + 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 = MDScreen() + 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) -> list: + return get_color_from_hex( + self.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) -> list: + return get_color_from_hex( + self.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 = ''' + MDScreen: + + 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) -> list: + return get_color_from_hex( + self.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:`accent_palette` 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) -> list: + return get_color_from_hex( + self.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) -> list: + return get_color_from_hex( + self.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) -> list: + return get_color_from_hex( + self.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. + """ + + material_style = OptionProperty("M2", options=["M2", "M3"]) + """ + Material design style. + Available options are: 'M2', 'M3'. + + .. versionadded:: 1.0.0 + + .. seealso:: + + `Material Design 2 `_ and + `Material Design 3 `_ + + + :attr:`material_style` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'M2'`. + """ + + theme_style = OptionProperty("Light", options=["Light", "Dark"]) + """ + App theme style. + + .. code-block:: python + + from kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + from kivymd.uix.button import MDRectangleFlatButton + + + class MainApp(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" # "Light" + screen = MDScreen() + 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: bool) -> str: + if opposite: + return "Light" if self.theme_style == "Dark" else "Dark" + else: + return self.theme_style + + def _get_bg_darkest(self, opposite: bool = False) -> list: + theme_style = self._get_theme_style(opposite) + if theme_style == "Light": + return get_color_from_hex(self.colors["Light"]["StatusBar"]) + elif theme_style == "Dark": + return get_color_from_hex(self.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 = ''' + MDBoxLayout: + + MDBoxLayout: + md_bg_color: app.theme_cls.bg_light + + MDBoxLayout: + md_bg_color: app.theme_cls.bg_normal + + MDBoxLayout: + md_bg_color: app.theme_cls.bg_dark + + MDBoxLayout: + md_bg_color: 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) -> list: + 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: bool = False) -> list: + theme_style = self._get_theme_style(opposite) + if theme_style == "Light": + return get_color_from_hex(self.colors["Light"]["AppBar"]) + elif theme_style == "Dark": + return get_color_from_hex(self.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) -> list: + 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: bool = False) -> list: + theme_style = self._get_theme_style(opposite) + if theme_style == "Light": + return get_color_from_hex(self.colors["Light"]["Background"]) + elif theme_style == "Dark": + return get_color_from_hex(self.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) -> list: + 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: bool = False) -> list: + theme_style = self._get_theme_style(opposite) + if theme_style == "Light": + return get_color_from_hex(self.colors["Light"]["CardsDialogs"]) + elif theme_style == "Dark": + return get_color_from_hex(self.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) -> list: + 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: bool = False) -> list: + 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) -> list: + 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_disabled_primary_color(self, opposite: bool = False) -> list: + theme_style = self._get_theme_style(opposite) + lum = sum(self.primary_color[0:3]) / 3.0 + if theme_style == "Light": + a = 0.38 + elif theme_style == "Dark": + a = 0.50 + return [lum, lum, lum, a] + + disabled_primary_color = AliasProperty( + _get_disabled_primary_color, bind=["theme_style"] + ) + """ + The greyscale disabled version of the current application theme color + in ``rgba`` format. + + .. versionadded:: 1.0.0 + + :attr:`disabled_primary_color` + is an :class:`~kivy.properties.AliasProperty` that returns the value + in ``rgba`` format for :attr:`disabled_primary_color`, + property is readonly. + """ + + def _get_op_disabled_primary_color(self) -> list: + return self._get_disabled_primary_color(True) + + opposite_disabled_primary_color = AliasProperty( + _get_op_disabled_primary_color, bind=["theme_style"] + ) + """ + The opposite value of color in the :attr:`disabled_primary_color`. + + .. versionadded:: 1.0.0 + + :attr:`opposite_disabled_primary_color` is an + :class:`~kivy.properties.AliasProperty` that returns the value + in ``rgba`` format for :attr:`opposite_disabled_primary_color`, + property is readonly. + """ + + def _get_text_color(self, opposite: bool = False) -> list: + 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) -> list: + 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: bool = False) -> list: + 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) -> list: + 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: bool = False) -> list: + 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) -> list: + 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: bool = False) -> list: + 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) -> list: + 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) -> list: + return get_color_from_hex(self.colors["Red"]["A700"]) + + error_color = AliasProperty(_get_error_color, bind=["theme_style"]) + """ + 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) -> list: + return self._ripple_color + + def _set_ripple_color(self, value) -> None: + self._ripple_color = value + + _ripple_color = ColorProperty(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) -> None: + 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) -> float: + 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) -> float: + 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, interval: int, theme_style: str) -> None: + if ( + hasattr(App.get_running_app(), "theme_cls") + and App.get_running_app().theme_cls == self + ): + self.set_clearcolor_by_theme_style(theme_style) + + set_clearcolor = BooleanProperty(True) + + def set_clearcolor_by_theme_style(self, theme_style): + if not self.set_clearcolor: + return + Window.clearcolor = get_color_from_hex( + self.colors[theme_style]["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 = ''' + MDScreen: + + 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 set_colors( + self, + primary_palette: str, + primary_hue: str, + primary_light_hue: str, + primary_dark_hue: str, + accent_palette: str, + accent_hue: str, + accent_light_hue: str, + accent_dark_hue: str, + ) -> None: + """ + Courtesy method to allow all of the theme color attributes to be set in one call. + + :attr:`set_colors` allows all of the following to be set in one method call: + + * primary palette color, + * primary hue, + * primary light hue, + * primary dark hue, + * accent palette color, + * accent hue, + * accent ligth hue, and + * accent dark hue. + + Note that all values *must* be provided. If you only want to set one or two values + use the appropriate method call for that. + + .. code-block:: python + + from kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + from kivymd.uix.button import MDRectangleFlatButton + + + class MainApp(MDApp): + def build(self): + self.theme_cls.set_colors( + "Blue", "600", "50", "800", "Teal", "600", "100", "800" + ) + screen = MDScreen() + screen.add_widget( + MDRectangleFlatButton( + text="Hello, World", + pos_hint={"center_x": 0.5, "center_y": 0.5}, + ) + ) + return screen + + + MainApp().run() + + """ + self.primary_palette = primary_palette + self.primary_hue = primary_hue + self.primary_light_hue = primary_light_hue + self.primary_dark_hue = primary_dark_hue + self.accent_palette = accent_palette + self.accent_hue = accent_hue + self.accent_light_hue = accent_light_hue + self.accent_dark_hue = accent_dark_hue + + 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) + self.bind(font_styles=self.sync_theme_styles) + self.colors = colors + Clock.schedule_once(self.sync_theme_styles) + + def sync_theme_styles(self, *args) -> None: + # Syncs the values from self.font_styles to theme_font_styles + # this will ensure continuity when someone registers a new font_style. + for num, style in enumerate(theme_font_styles): + if style not in self.font_styles: + theme_font_styles.pop(num) + for style in self.font_styles.keys(): + theme_font_styles.append(style) + + +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`. + """ + + widget_style = OptionProperty( + "android", options=["android", "ios", "desktop"] + ) + """ + Allows to set one of the three style properties for the widget: + `'android'`, `'ios'`, `'desktop'`. + + For example, for the class :class:`~kivymd.uix.selectioncontrol.MDSwitch` + has two styles - `'android'` and `'ios'`: + + .. code-block:: kv + + MDSwitch: + widget_style: "ios" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-switch-ios.gif + :align: center + + .. code-block:: kv + + MDSwitch: + widget_style: "android" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-switch-android.gif + :align: center + + :attr:`widget_style` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'android'`. + """ + + opposite_colors = BooleanProperty(False) + """ + For some widgets, for example, for a widget + :class:`~kivymd.uix.toolbar.MDTopAppBar` changes the color of the label to + the color opposite to the main theme. + + .. code-block:: kv + + MDTopAppBar: + title: "MDTopAppBar" + opposite_colors: True + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-true.png + :align: center + + .. code-block:: kv + + MDTopAppBar: + title: "MDTopAppBar" + opposite_colors: True + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-false.png + :align: center + """ + + 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`" + ) + 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/sbapp/kivymd/theming_dynamic_text.py b/sbapp/kivymd/theming_dynamic_text.py new file mode 100755 index 0000000..64f9921 --- /dev/null +++ b/sbapp/kivymd/theming_dynamic_text.py @@ -0,0 +1,90 @@ +""" +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/sbapp/kivymd/toast/LICENSE b/sbapp/kivymd/toast/LICENSE new file mode 100755 index 0000000..b18e56d --- /dev/null +++ b/sbapp/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/sbapp/kivymd/toast/README.md b/sbapp/kivymd/toast/README.md new file mode 100755 index 0000000..4d8abab --- /dev/null +++ b/sbapp/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/sbapp/kivymd/toast/__init__.py b/sbapp/kivymd/toast/__init__.py new file mode 100755 index 0000000..d5270c6 --- /dev/null +++ b/sbapp/kivymd/toast/__init__.py @@ -0,0 +1,11 @@ +__all__ = ("toast",) + +from kivy.utils import platform + +if platform == "android": + try: + from .androidtoast import toast + except ModuleNotFoundError: + from .kivytoast import toast +else: + from .kivytoast import toast diff --git a/sbapp/kivymd/toast/androidtoast/__init__.py b/sbapp/kivymd/toast/androidtoast/__init__.py new file mode 100755 index 0000000..300dd2d --- /dev/null +++ b/sbapp/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/sbapp/kivymd/toast/androidtoast/androidtoast.py b/sbapp/kivymd/toast/androidtoast/androidtoast.py new file mode 100644 index 0000000..18af63e --- /dev/null +++ b/sbapp/kivymd/toast/androidtoast/androidtoast.py @@ -0,0 +1,81 @@ +""" +AndroidToast +============ + +.. rubric:: Native implementation of toast for Android devices. + +.. code-block:: python + + # 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 kivy.lang import Builder + from kivy.uix.screenmanager import ScreenManager + + from kivymd.toast import toast + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDFlatButton: + text: "My Toast" + pos_hint:{"center_x": .5, "center_y": .5} + on_press: app.show_toast() + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def show_toast(self): + toast("Hello World", True, 80, 200, 0) + + + Test().run() +""" + +__all__ = ("toast",) + +from kivy import platform + +if platform != "android": + raise TypeError( + f"{platform.capitalize()} platform does not support Android Toast" + ) + +from android.runnable import run_on_ui_thread +from jnius import autoclass + +activity = autoclass("org.kivy.android.PythonActivity").mActivity +Toast = autoclass("android.widget.Toast") +String = autoclass("java.lang.String") + + +@run_on_ui_thread +def toast(text, length_long=False, gravity=0, y=0, x=0): + """ + Displays a toast. + + :param length_long: the amount of time (in seconds) that the toast is + visible on the screen; + :param text: text to be displayed in the toast; + :param short_duration: duration of the toast, if `True` the toast + will last 2.3s but if it is `False` the toast will last 3.9s; + :param gravity: refers to the toast position, if it is 80 the toast will + be shown below, if it is 40 the toast will be displayed above; + :param y: refers to the vertical position of the toast; + :param x: refers to the horizontal position of the toast; + + Important: if only the text value is specified and the value of + the `gravity`, `y`, `x` parameters is not specified, their values will + be 0 which means that the toast will be shown in the center. + """ + + duration = Toast.LENGTH_SHORT if length_long else Toast.LENGTH_LONG + t = Toast.makeText(activity, String(text), duration) + t.setGravity(gravity, x, y) + t.show() diff --git a/sbapp/kivymd/toast/kivytoast/__init__.py b/sbapp/kivymd/toast/kivytoast/__init__.py new file mode 100755 index 0000000..0d9c204 --- /dev/null +++ b/sbapp/kivymd/toast/kivytoast/__init__.py @@ -0,0 +1,3 @@ +__all__ = ("toast",) + +from .kivytoast import toast diff --git a/sbapp/kivymd/toast/kivytoast/kivytoast.py b/sbapp/kivymd/toast/kivytoast/kivytoast.py new file mode 100755 index 0000000..87645bc --- /dev/null +++ b/sbapp/kivymd/toast/kivytoast/kivytoast.py @@ -0,0 +1,154 @@ +""" +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 = ''' + MDScreen: + + MDTopAppBar: + title: 'Test Toast' + pos_hint: {'top': 1} + left_action_items: [['menu', lambda x: x]] + + MDRaisedButton: + text: 'TEST KIVY TOAST' + pos_hint: {'center_x': .5, 'center_y': .5} + on_release: app.show_toast() + ''' + + + 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 typing import List + +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 +from kivy.uix.label import Label + +from kivymd.uix.dialog import BaseDialog + +Builder.load_string( + """ +: + size_hint: (None, None) + pos_hint: {"center_x": 0.5, "center_y": 0.1} + opacity: 0 + auto_dismiss: True + overlay_color: [0, 0, 0, 0] + canvas: + Color: + rgba: root._md_bg_color + RoundedRectangle: + pos: self.pos + size: self.size + radius: root.radius +""" +) + + +class Toast(BaseDialog): + 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`. + """ + + _md_bg_color = ListProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + 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_label: Label, texture_size: List[int] + ) -> None: + """ + Resizes the text if the text texture is larger than the screen size. + Sets the size of the toast according to the texture size of the toast + text. + """ + + texture_width, texture_height = texture_size + if texture_width > Window.width: + instance_label.text_size = (Window.width - dp(10), None) + instance_label.texture_update() + texture_width, texture_height = instance_label.texture_size + self.size = (texture_width + 25, texture_height + 25) + + def toast(self, text_toast: str) -> None: + """Displays a toast.""" + + self.label_toast.text = text_toast + self.open() + + def on_open(self) -> None: + """Default open event handler.""" + + self.fade_in() + Clock.schedule_once(self.fade_out, self.duration) + + def fade_in(self) -> None: + """Animation of opening toast on the screen.""" + + anim = Animation(opacity=1, duration=0.4) + anim.start(self.label_toast) + anim.start(self) + + def fade_out(self, *args) -> None: + """Animation of hiding toast on the screen.""" + + anim = Animation(opacity=0, duration=0.4) + anim.bind(on_complete=lambda *x: self.dismiss()) + anim.start(self.label_toast) + anim.start(self) + + def on_touch_down(self, touch): + if not self.collide_point(*touch.pos): + if self.auto_dismiss: + self.fade_out() + return False + super().on_touch_down(touch) + return True + + +def toast( + text: str = "", background: list = None, duration: float = 2.5 +) -> None: + """ + Displays a toast. + + :param text: text to be displayed in the toast; + :param duration: the amount of time (in seconds) that the toast is visible on the screen + :param background: toast background color in ``rgba`` format; + """ + + if background is None: + background = [0.2, 0.2, 0.2, 1] + Toast(duration=duration, _md_bg_color=background).toast(text) diff --git a/sbapp/kivymd/tools/__init__.py b/sbapp/kivymd/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/argument_parser.py b/sbapp/kivymd/tools/argument_parser.py new file mode 100644 index 0000000..122ec77 --- /dev/null +++ b/sbapp/kivymd/tools/argument_parser.py @@ -0,0 +1,92 @@ +# Copyright (c) 2019-2021 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/sbapp/kivymd/tools/hotreload/__init__.py b/sbapp/kivymd/tools/hotreload/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/hotreload/app.py b/sbapp/kivymd/tools/hotreload/app.py new file mode 100644 index 0000000..e036875 --- /dev/null +++ b/sbapp/kivymd/tools/hotreload/app.py @@ -0,0 +1,549 @@ +""" +HotReload +========= + +.. versionadded:: 1.0.0 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hot-reload.png + :align: center + +.. rubric:: + Hot reload tool - is a fork of the project https://github.com/tito/kaki + +.. note:: + Since the project is not developing, we decided to include it in the + KivvMD library and hope that the further development of the hot reload + tool in the KivyMD project will develop faster. + +.. rubric:: + This library enhance Kivy frameworks with opiniated features such as: + +- Auto reloading kv or py (watchdog required, limited to some uses cases); +- Idle detection support; +- Foreground lock (Windows OS only); + +Usage +----- + +.. note:: + See `create project with hot reload `_ + for more information. + +TODO +---- + +- Add automatic reloading of Python classes; +- Add save application state on reloading; + +FIXME +----- + +- On Windows, hot reloading of Python files may not work; +""" + +import os +import sys +import traceback +from fnmatch import fnmatch +from os.path import join, realpath + +original_argv = sys.argv + +from kivy.base import ExceptionHandler, ExceptionManager # NOQA E402 +from kivy.clock import Clock, mainthread # NOQA E402 +from kivy.factory import Factory # NOQA E402 +from kivy.lang import Builder # NOQA E402 +from kivy.logger import Logger # NOQA E402 +from kivy.properties import ( # NOQA E402 + BooleanProperty, + DictProperty, + ListProperty, + NumericProperty, +) + +from kivymd.app import MDApp as BaseApp # NOQA E402 + +try: + from monotonic import monotonic +except ImportError: + monotonic = None +try: + from importlib import reload + + PY3 = True +except ImportError: + PY3 = False + +import watchdog # NOQA + + +class ExceptionClass(ExceptionHandler): + def handle_exception(self, inst): + if isinstance(inst, (KeyboardInterrupt, SystemExit)): + return ExceptionManager.RAISE + app = MDApp.get_running_app() + if not app.DEBUG and not app.RAISE_ERROR: + return ExceptionManager.RAISE + app.set_error(inst, tb=traceback.format_exc()) + return ExceptionManager.PASS + + +ExceptionManager.add_handler(ExceptionClass()) + + +class MDApp(BaseApp): + """HotReload Application class.""" + + DEBUG = BooleanProperty("DEBUG" in os.environ) + """ + Control either we activate debugging in the app or not. + Defaults depend if 'DEBUG' exists in os.environ. + + :attr:`DEBUG` is a :class:`~kivy.properties.BooleanProperty`. + """ + + FOREGROUND_LOCK = BooleanProperty(False) + """ + If `True` it will require the foreground lock on windows. + + :attr:`FOREGROUND_LOCK` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + KV_FILES = ListProperty() + """ + List of KV files under management for auto reloader. + + :attr:`KV_FILES` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + KV_DIRS = ListProperty() + """ + List of managed KV directories for autoloader. + + :attr:`KV_DIRS` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + AUTORELOADER_PATHS = ListProperty([(".", {"recursive": True})]) + """ + List of path to watch for auto reloading. + + :attr:`AUTORELOADER_PATHS` is a :class:`~kivy.properties.ListProperty` + and defaults to `([(".", {"recursive": True})]`. + """ + + AUTORELOADER_IGNORE_PATTERNS = ListProperty(["*.pyc", "*__pycache__*"]) + """ + List of extensions to ignore. + + :attr:`AUTORELOADER_IGNORE_PATTERNS` is a :class:`~kivy.properties.ListProperty` + and defaults to `['*.pyc', '*__pycache__*']`. + """ + + CLASSES = DictProperty() + """ + Factory classes managed by hotreload. + + :attr:`CLASSES` is a :class:`~kivy.properties.DictProperty` + and defaults to `{}`. + """ + + IDLE_DETECTION = BooleanProperty(False) + """ + Idle detection (if True, event on_idle/on_wakeup will be fired). + Rearming idle can also be done with `rearm_idle()`. + + :attr:`IDLE_DETECTION` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + IDLE_TIMEOUT = NumericProperty(60) + """ + Default idle timeout. + + :attr:`IDLE_TIMEOUT` is a :class:`~kivy.properties.NumericProperty` + and defaults to `60`. + """ + + RAISE_ERROR = BooleanProperty(True) + """ + Raise error. + When the `DEBUG` is activated, it will raise any error instead + of showing it on the screen. If you still want to show the error + when not in `DEBUG`, put this to `False`. + + :attr:`RAISE_ERROR` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + __events__ = ["on_idle", "on_wakeup"] + + def build(self): + if self.DEBUG: + Logger.info("{}: Debug mode activated".format(self.appname)) + self.enable_autoreload() + self.patch_builder() + self.bind_key(32, self.rebuild) + if self.FOREGROUND_LOCK: + self.prepare_foreground_lock() + + self.state = None + self.approot = None + self.root = self.get_root() + self.rebuild(first=True) + + if self.IDLE_DETECTION: + self.install_idle(timeout=self.IDLE_TIMEOUT) + + return super().build() + + def get_root(self): + """ + Return a root widget, that will contains your application. + It should not be your application widget itself, as it may + be destroyed and recreated from scratch when reloading. + + By default, it returns a RelativeLayout, but it could be + a Viewport. + """ + + return Factory.RelativeLayout() + + def get_root_path(self): + """Return the root file path.""" + + return realpath(os.getcwd()) + + def build_app(self, first=False): + """ + Must return your application widget. + + If `first` is set, it means that will be your first time ever + that the application is built. Act according to it. + """ + + raise NotImplementedError() + + def unload_app_dependencies(self): + """ + Called when all the application dependencies must be unloaded. + Usually happen before a reload + """ + + for path_to_kv_file in self.KV_FILES: + path_to_kv_file = realpath(path_to_kv_file) + Builder.unload_file(path_to_kv_file) + + for name, module in self.CLASSES.items(): + Factory.unregister(name) + + for path in self.KV_DIRS: + for path_to_dir, dirs, files in os.walk(path): + for name_file in files: + if os.path.splitext(name_file)[1] == ".kv": + path_to_kv_file = os.path.join(path_to_dir, name_file) + Builder.unload_file(path_to_kv_file) + + def load_app_dependencies(self): + """ + Load all the application dependencies. + This is called before rebuild. + """ + + for path_to_kv_file in self.KV_FILES: + path_to_kv_file = realpath(path_to_kv_file) + Builder.load_file(path_to_kv_file) + + for name, module in self.CLASSES.items(): + Factory.register(name, module=module) + + for path in self.KV_DIRS: + for path_to_dir, dirs, files in os.walk(path): + for name_file in files: + if os.path.splitext(name_file)[1] == ".kv": + path_to_kv_file = os.path.join(path_to_dir, name_file) + Builder.load_file(path_to_kv_file) + + def rebuild(self, *args, **kwargs): + print("{}: Rebuild the application".format(self.appname)) + first = kwargs.get("first", False) + try: + if not first: + self.unload_app_dependencies() + + # In case the loading fail in the middle of building a widget + # there will be existing rules context that will break later + # instanciation. + # Just clean it. + Builder.rulectx = {} + + self.load_app_dependencies() + self.set_widget(None) + self.approot = self.build_app() + self.set_widget(self.approot) + self.apply_state(self.state) + except Exception as exc: + import traceback + + Logger.exception("{}: Error when building app".format(self.appname)) + self.set_error(repr(exc), traceback.format_exc()) + if not self.DEBUG and self.RAISE_ERROR: + raise + + @mainthread + def set_error(self, exc, tb=None): + print(tb) + from kivy.core.window import Window + from kivy.utils import get_color_from_hex + + Window.clearcolor = get_color_from_hex("#e50000") + scroll = Factory.ScrollView(scroll_y=0) + lbl = Factory.Label( + text_size=(Window.width - 100, None), + size_hint_y=None, + text="{}\n\n{}".format(exc, tb or ""), + ) + lbl.bind(texture_size=lbl.setter("size")) + scroll.add_widget(lbl) + self.set_widget(scroll) + + def bind_key(self, key, callback): + """Bind a key (keycode) to a callback (cannot be unbind).""" + + from kivy.core.window import Window + + def _on_keyboard(window, keycode, *args): + if key == keycode: + return callback() + + Window.bind(on_keyboard=_on_keyboard) + + @property + def appname(self): + """Return the name of the application class.""" + + return self.__class__.__name__ + + def enable_autoreload(self): + """ + Enable autoreload manually. It is activated automatically + if "DEBUG" exists in environ. It requires the `watchdog` module. + """ + + try: + from watchdog.events import FileSystemEventHandler + from watchdog.observers import Observer + except ImportError: + Logger.warn( + "{}: Autoreloader is missing watchdog".format(self.appname) + ) + return + Logger.info("{}: Autoreloader activated".format(self.appname)) + rootpath = self.get_root_path() + self.w_handler = handler = FileSystemEventHandler() + handler.dispatch = self._reload_from_watchdog + self._observer = observer = Observer() + for path in self.AUTORELOADER_PATHS: + options = {"recursive": True} + if isinstance(path, (tuple, list)): + path, options = path + observer.schedule(handler, join(rootpath, path), **options) + observer.start() + + def prepare_foreground_lock(self): + """ + Try forcing app to front permanently to avoid windows + pop ups and notifications etc.app. + + Requires fake full screen and borderless. + + .. note:: + This function is called automatically if `FOREGROUND_LOCK` is set + """ + + try: + import ctypes + + LSFW_LOCK = 1 + ctypes.windll.user32.LockSetForegroundWindow(LSFW_LOCK) + Logger.info("App: Foreground lock activated") + except Exception: + Logger.warn("App: No foreground lock available") + + def set_widget(self, wid): + """ + Clear the root container, and set the new approot widget to `wid`. + """ + + self.root.clear_widgets() + self.approot = wid + if wid is None: + return + self.root.add_widget(self.approot) + try: + wid.do_layout() + except Exception: + pass + + # State management. + def apply_state(self, state): + """Whatever the current state is, reapply the current state.""" + + # Idle management leave. + def install_idle(self, timeout=60): + """ + Install the idle detector. Default timeout is 60s. + Once installed, it will check every second if the idle timer + expired. The timer can be rearm using :func:`rearm_idle`. + """ + + if monotonic is None: + Logger.exception( + "{}: Cannot use idle detector, monotonic is missing".format( + self.appname + ) + ) + self.idle_timer = None + self.idle_timeout = timeout + Logger.info( + "{}: Install idle detector, {} seconds".format( + self.appname, timeout + ) + ) + Clock.schedule_interval(self._check_idle, 1) + self.root.bind( + on_touch_down=self.rearm_idle, on_touch_up=self.rearm_idle + ) + + def rearm_idle(self, *args): + """Rearm the idle timer.""" + + if not hasattr(self, "idle_timer"): + return + if self.idle_timer is None: + self.dispatch("on_wakeup") + self.idle_timer = monotonic() + + # Internals. + def patch_builder(self): + Builder.orig_load_string = Builder.load_string + Builder.load_string = self._builder_load_string + + def on_idle(self, *args): + """Event fired when the application enter the idle mode.""" + + def on_wakeup(self, *args): + """Event fired when the application leaves idle mode.""" + + @mainthread + def _reload_from_watchdog(self, event): + from watchdog.events import FileModifiedEvent + + if not isinstance(event, FileModifiedEvent): + return + + for pat in self.AUTORELOADER_IGNORE_PATTERNS: + if fnmatch(event.src_path, pat): + return + + if event.src_path.endswith(".py"): + # source changed, reload it + try: + Builder.unload_file(event.src_path) + self._reload_py(event.src_path) + except Exception as e: + import traceback + + self.set_error(repr(e), traceback.format_exc()) + return + + Clock.unschedule(self.rebuild) + Clock.schedule_once(self.rebuild, 0.1) + + def _builder_load_string(self, string, **kwargs): + if "filename" not in kwargs: + from inspect import getframeinfo, stack + + caller = getframeinfo(stack()[1][0]) + kwargs["filename"] = caller.filename + return Builder.orig_load_string(string, **kwargs) + + def _check_idle(self, *args): + if not hasattr(self, "idle_timer"): + return + if self.idle_timer is None: + return + if monotonic() - self.idle_timer > self.idle_timeout: + self.idle_timer = None + self.dispatch("on_idle") + + def _reload_py(self, filename): + # We don't have dependency graph yet, so if the module actually exists + # reload it. + + filename = realpath(filename) + # Check if it's our own application file. + try: + mod = sys.modules[self.__class__.__module__] + mod_filename = realpath(mod.__file__) + except Exception: + mod_filename = None + + # Detect if it's the application class // main. + if mod_filename == filename: + return self._restart_app(mod) + + module = self._filename_to_module(filename) + if module in sys.modules: + Logger.debug("{}: Module exist, reload it".format(self.appname)) + Factory.unregister_from_filename(filename) + self._unregister_factory_from_module(module) + reload(sys.modules[module]) + + def _unregister_factory_from_module(self, module): + # Check module directly. + to_remove = [ + x for x in Factory.classes if Factory.classes[x]["module"] == module + ] + # Check class name. + for x in Factory.classes: + cls = Factory.classes[x]["cls"] + if not cls: + continue + if getattr(cls, "__module__", None) == module: + to_remove.append(x) + + for name in set(to_remove): + del Factory.classes[name] + + def _filename_to_module(self, filename): + orig_filename = filename + rootpath = self.get_root_path() + if filename.startswith(rootpath): + filename = filename[len(rootpath) :] + if filename.startswith("/"): + filename = filename[1:] + module = filename[:-3].replace("/", ".") + Logger.debug( + "{}: Translated {} to {}".format( + self.appname, orig_filename, module + ) + ) + return module + + def _restart_app(self, mod): + _has_execv = sys.platform != "win32" + cmd = [sys.executable] + original_argv + if not _has_execv: + import subprocess + + subprocess.Popen(cmd) + sys.exit(0) + else: + try: + os.execv(sys.executable, cmd) + except OSError: + os.spawnv(os.P_NOWAIT, sys.executable, cmd) + os._exit(0) diff --git a/sbapp/kivymd/tools/packaging/__init__.py b/sbapp/kivymd/tools/packaging/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/packaging/pyinstaller/__init__.py b/sbapp/kivymd/tools/packaging/pyinstaller/__init__.py new file mode 100644 index 0000000..47db41b --- /dev/null +++ b/sbapp/kivymd/tools/packaging/pyinstaller/__init__.py @@ -0,0 +1,72 @@ +""" +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", "get_hook_dirs", "get_pyinstaller_tests") + +import os +from pathlib import Path + +import kivymd + +hooks_path = str(Path(__file__).absolute().parent) +"""Path to hook directory to use with PyInstaller. +See :mod:`kivymd.tools.packaging.pyinstaller` for more information.""" + + +def get_hook_dirs(): + return [hooks_path] + + +def get_pyinstaller_tests(): + return [os.path.join(kivymd.path, "tests", "pyinstaller")] + + +if __name__ == "__main__": + print(hooks_path) + print(get_hook_dirs()) + print(get_pyinstaller_tests()) diff --git a/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py b/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py new file mode 100644 index 0000000..d2cb9a5 --- /dev/null +++ b/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py @@ -0,0 +1,42 @@ +""" +PyInstaller hook for KivyMD +=========================== + +Adds fonts, images and KV files to package. + +All modules from uix directory are added by Kivy hook. +""" + +import os +from pathlib import Path + +import kivymd + +datas = [ + # Add `.ttf` files from the `kivymd/fonts` directory. + ( + kivymd.fonts_path, + str(Path("kivymd").joinpath(Path(kivymd.fonts_path).name)), + ), + # Add files from the `kivymd/images` directory. + ( + kivymd.images_path, + str(Path("kivymd").joinpath(Path(kivymd.images_path).name)), + ), +] + +# Add `.kv. files from the `kivymd/uix` directory. +for path_to_kv_file in Path(kivymd.uix_path).glob("**/*.kv"): + datas.append( + ( + str(Path(path_to_kv_file).parent.joinpath("*.kv")), + str( + Path("kivymd").joinpath( + "uix", + str(Path(path_to_kv_file).parent).split( + str(Path("kivymd").joinpath("uix")) + os.sep + )[1], + ) + ), + ) + ) diff --git a/sbapp/kivymd/tools/patterns/MVC/Controller/__init__.py b/sbapp/kivymd/tools/patterns/MVC/Controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/patterns/MVC/Controller/first_screen.py_tmp b/sbapp/kivymd/tools/patterns/MVC/Controller/first_screen.py_tmp new file mode 100644 index 0000000..1bcf0e8 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/Controller/first_screen.py_tmp @@ -0,0 +1,3 @@ +%s + def get_view(self) -> %s: + return self.view diff --git a/sbapp/kivymd/tools/patterns/MVC/Makefile.tmp b/sbapp/kivymd/tools/patterns/MVC/Makefile.tmp new file mode 100644 index 0000000..4fd0715 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/Makefile.tmp @@ -0,0 +1,26 @@ +# FILE TO FIND AND CREATE LOCALIZATION FILES FOR YOUR APPLICATION. \ +\ +In this file, you can specify in which files of your project to search for \ +localization strings. \ +These files should be listed in the below command: \ +\ +\ +xgettext -Lpython --output=messages.pot --from-code=utf-8 \ + path/to/file-1 \ + path/to/file-2 \ + ... + +.PHONY: po mo + +po: + xgettext -Lpython --output=messages.pot --from-code=utf-8 \ + View/%s/%s.kv \ + View/%s/%s.py + msgmerge --update --no-fuzzy-matching --backup=off data/locales/po/en.po messages.pot + msgmerge --update --no-fuzzy-matching --backup=off data/locales/po/ru.po messages.pot + +mo: + mkdir -p data/locales/en/LC_MESSAGES + mkdir -p data/locales/ru/LC_MESSAGES + msgfmt -c -o data/locales/en/LC_MESSAGES/%s.mo data/locales/po/en.po + msgfmt -c -o data/locales/ru/LC_MESSAGES/%s.mo data/locales/po/ru.po \ No newline at end of file diff --git a/sbapp/kivymd/tools/patterns/MVC/Model/__init__.py b/sbapp/kivymd/tools/patterns/MVC/Model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/patterns/MVC/Model/base_model.py_tmp b/sbapp/kivymd/tools/patterns/MVC/Model/base_model.py_tmp new file mode 100644 index 0000000..554a4f4 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/Model/base_model.py_tmp @@ -0,0 +1,33 @@ +# The model implements the observer pattern. This means that the class must +# support adding, removing, and alerting observers. In this case, the model is +# completely independent of controllers and views. It is important that all +# registered observers implement a specific method that will be called by the +# model when they are notified (in this case, it is the `model_is_changed` +# method). For this, observers must be descendants of an abstract class, +# inheriting which, the `model_is_changed` method must be overridden. + + +class BaseScreenModel: + """Implements a base class for model modules.""" + + _observers = [] + + def add_observer(self, observer) -> None: + self._observers.append(observer) + + def remove_observer(self, observer) -> None: + self._observers.remove(observer) + + def notify_observers(self, name_screen: str) -> None: + """ + Method that will be called by the observer when the model data changes. + + :param name_screen: + name of the view for which the method should be called + :meth:`model_is_changed`. + """ + + for observer in self._observers: + if observer.name == name_screen: + observer.model_is_changed() + break diff --git a/sbapp/kivymd/tools/patterns/MVC/Model/database_firebase.py b/sbapp/kivymd/tools/patterns/MVC/Model/database_firebase.py new file mode 100644 index 0000000..7331f3a --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/Model/database_firebase.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +import socket + +import requests +from firebase import firebase + + +def get_connect(func, host="8.8.8.8", port=53, timeout=3): + """Checks for an active Internet connection.""" + + def wrapped(*args): + try: + socket.setdefaulttimeout(timeout) + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect( + (host, port) + ) + return func(*args) + except Exception: + return False + + return wrapped + + +class DataBase: + """ + Your methods for working with the database should be implemented in this + class. + """ + + name = "Firebase" + + def __init__(self): + self.DATABASE_URL = "https://fir-db73a-default-rtdb.firebaseio.com/" + # Address for users collections. + self.USER_DATA = "Userdata" + # RealTime Database attribute. + self.real_time_firebase = firebase.FirebaseApplication( + self.DATABASE_URL, None + ) + + @get_connect + def get_data_from_collection(self, name_collection: str) -> dict | bool: + """Returns data of the selected collection from the database.""" + + try: + data = self.real_time_firebase.get( + self.DATABASE_URL, name_collection + ) + except requests.exceptions.ConnectionError: + return False + + return data diff --git a/sbapp/kivymd/tools/patterns/MVC/Model/database_restdb.py b/sbapp/kivymd/tools/patterns/MVC/Model/database_restdb.py new file mode 100644 index 0000000..561b76b --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/Model/database_restdb.py @@ -0,0 +1,134 @@ +""" +Restdb.io API Wrapper +--------------------- + +This package is an API Wrapper for the website `restdb.io `_, +which allows for online databases. +""" + +from __future__ import annotations + +import json +import os +import socket + +import requests + + +def get_connect(func, host="8.8.8.8", port=53, timeout=3): + """Checks for an active Internet connection.""" + + def wrapped(*args): + try: + socket.setdefaulttimeout(timeout) + socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect( + (host, port) + ) + return func(*args) + except Exception: + return False + + return wrapped + + +class DataBase: + name = "RestDB" + + def __init__(self): + database_url = "https://restdbio-5498.restdb.io" + api_key = "7ce258d66f919d3a891d1166558765f0b4dbd" + + self.HEADERS = {"x-apikey": api_key, "Content-Type": "application/json"} + # Address for file collections. + self.USER_MEDIA = f"{database_url}/media" + # Address for users collections. + self.USER_DATA = f"{database_url}/rest/userdata" + + @get_connect + def upload_file(self, path_to_file: str) -> dict | bool: + """ + Uploads a file to the database. + You can upload a file to the database only from a paid account. + """ + + HEADERS = self.HEADERS.copy() + del HEADERS["Content-Type"] + payload = {} + name_file = os.path.split(path_to_file)[1] + files = [("file", (name_file, open(path_to_file, "rb"), name_file))] + response = requests.post( + url=self.USER_MEDIA, + headers=HEADERS, + data=payload, + files=files, + ) + + if response.status_code == 201: + # { + # "msg":"OK", + # "uploadid": "ed1bca42334f68d873161641144e57b7", + # "ids": ["62614fb90f9df71600284aa7"], + # } + json = response.json() + if "msg" in json and json["msg"] == "OK": + return json + else: + return False + + @get_connect + def get_data_from_collection(self, collection_address: str) -> bool | list: + """Returns data of the selected collection from the database.""" + + response = requests.get(url=collection_address, headers=self.HEADERS) + if response.status_code != 200: + return False + else: + return response.json() + + @get_connect + def delete_doc_from_collection(self, collection_address: str) -> bool: + """ + Delete data of the selected collection from the database. + + :param collection_address: "database_url/id_collection". + """ + + response = requests.delete(collection_address, headers=self.HEADERS) + if response.status_code == 200: + return True + else: + return False + + @get_connect + def add_doc_to_collection( + self, data: dict, collection_address: str + ) -> bool: + """Add collection to the database.""" + + response = requests.post( + url=collection_address, + data=json.dumps(data), + headers=self.HEADERS, + ) + if response.status_code == 201: + if "_id" in response.json(): + return response.json() + else: + return False + + @get_connect + def edit_data( + self, collection: dict, collection_address: str, collection_id: str + ) -> bool: + """Modifies data in a collection of data in a database.""" + + response = requests.put( + url=f"{collection_address}/{collection_id}", + headers=self.HEADERS, + json=collection, + ) + if response.status_code == 200: + if "_id" in response.json(): + return True + else: + return False diff --git a/sbapp/kivymd/tools/patterns/MVC/Model/first_screen.py_tmp b/sbapp/kivymd/tools/patterns/MVC/Model/first_screen.py_tmp new file mode 100644 index 0000000..3aa8911 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/Model/first_screen.py_tmp @@ -0,0 +1 @@ +%s diff --git a/sbapp/kivymd/tools/patterns/MVC/Utility/__init__.py b/sbapp/kivymd/tools/patterns/MVC/Utility/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/patterns/MVC/Utility/observer.py_tmp b/sbapp/kivymd/tools/patterns/MVC/Utility/observer.py_tmp new file mode 100644 index 0000000..bbf1a9f --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/Utility/observer.py_tmp @@ -0,0 +1,16 @@ +# Of course, "very flexible Python" allows you to do without an abstract +# superclass at all or use the clever exception `NotImplementedError`. In my +# opinion, this can negatively affect the architecture of the application. +# I would like to point out that using Kivy, one could use the on-signaling +# model. In this case, when the state changes, the model will send a signal +# that can be received by all attached observers. This approach seems less +# universal - you may want to use a different library in the future. + + +class Observer: + """Abstract superclass for all observers.""" + + def model_is_changed(self): + """ + The method that will be called on the observer when the model changes. + """ diff --git a/sbapp/kivymd/tools/patterns/MVC/View/FirstScreen/__init__.py b/sbapp/kivymd/tools/patterns/MVC/View/FirstScreen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/patterns/MVC/View/FirstScreen/first_screen.kv b/sbapp/kivymd/tools/patterns/MVC/View/FirstScreen/first_screen.kv new file mode 100644 index 0000000..9d8c3b0 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/View/FirstScreen/first_screen.kv @@ -0,0 +1,72 @@ +#:import images_path kivymd.images_path +#:import colors kivymd.color_definitions.colors +#:import get_color_from_hex kivy.utils.get_color_from_hex + + +<%s> + + FitImage: + source: + ( \ + f"{images_path}restdb-logo.png" \ + if root.model.database.name == "RestDB" else \ + f"{images_path}firebase-logo.png" \ + ) \ + if hasattr(root.model, "database") else \ + f"{images_path}transparent.png" + + MDBoxLayout: + orientation: "vertical" + + MDToolbar: + id: toolbar + title: "%s" + right_action_items: [["web", lambda x: %s]] + md_bg_color: + ( \ + get_color_from_hex(colors["Yellow"]["700"]) \ + if root.model.database.name == "Firebase" else \ + get_color_from_hex(colors["Blue"]["300"]) \ + ) \ + if hasattr(root.model, "database") else \ + app.theme_cls.primary_color + + MDFloatLayout: + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + size_hint_x: None + width: root.width - dp(72) + radius: 12 + padding: "12dp" + md_bg_color: 1, 1, 1, .5 + pos_hint: {"center_x": .5, "center_y": .5} + + MDLabel: + id: prev_label + text: %s + font_style: "H6" + adaptive_height: True + halign: "center" + color: 1, 1, 1, 1 + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + padding: "50dp" + spacing: "20dp" + + MDTextField: + hint_text: %s + on_text: root.controller.set_user_data("login", self.text) + + MDTextField: + hint_text: %s + on_text: root.controller.set_user_data("password", self.text) + + MDFillRoundFlatButton: + text: %s + on_release: root.controller.on_tap_button_login() + pos_hint: {"center_x": .5, "center_y": .1} + md_bg_color: toolbar.md_bg_color diff --git a/sbapp/kivymd/tools/patterns/MVC/View/FirstScreen/first_screen.py_tmp b/sbapp/kivymd/tools/patterns/MVC/View/FirstScreen/first_screen.py_tmp new file mode 100644 index 0000000..f063c31 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/View/FirstScreen/first_screen.py_tmp @@ -0,0 +1,15 @@ +%s +from View.base_screen import BaseScreenView + + +class %s(BaseScreenView): + """Implements the login start screen in the user application.""" +%s + def model_is_changed(self) -> None: + """ + Called whenever any change has occurred in the data model. + The view in this method tracks these changes and updates the UI + according to these changes. + """ + + %s \ No newline at end of file diff --git a/sbapp/kivymd/tools/patterns/MVC/View/base_screen.py_tmp b/sbapp/kivymd/tools/patterns/MVC/View/base_screen.py_tmp new file mode 100644 index 0000000..62a4242 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/View/base_screen.py_tmp @@ -0,0 +1,47 @@ +from kivy.properties import ObjectProperty + +from kivymd.app import MDApp +from kivymd.theming import ThemableBehavior +from kivymd.uix.screen import MDScreen + +from Utility.observer import Observer + + +class BaseScreenView(ThemableBehavior, MDScreen, Observer): + """ + A base class that implements a visual representation of the model data + :class:`~Model.%s.%s`. + The view class must be inherited from this class. + """ + + controller = ObjectProperty() + """ + Controller object - :class:`~Controller.%s.%s`. + + :attr:`controller` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + model = ObjectProperty() + """ + Model object - :class:`~Model.%s.%s`. + + :attr:`model` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + manager_screens = ObjectProperty() + """ + Screen manager object - :class:`~kivy.uix.screenmanager.ScreenManager`. + + :attr:`manager_screens` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + def __init__(self, **kw): + super().__init__(**kw) + # Often you need to get access to the application object from the view + # class. You can do this using this attribute. + self.app = MDApp.get_running_app() + # Adding a view class as observer. + self.model.add_observer(self) diff --git a/sbapp/kivymd/tools/patterns/MVC/View/screens.py_tmp b/sbapp/kivymd/tools/patterns/MVC/View/screens.py_tmp new file mode 100644 index 0000000..f1ab8c3 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/View/screens.py_tmp @@ -0,0 +1,13 @@ +# The screens dictionary contains the objects of the models and controllers +# of the screens of the application. + +from Model.%s import %s + +from Controller.%s import %s + +screens = { + %s: { + "model": %s, + "controller": %s, + }, +} diff --git a/sbapp/kivymd/tools/patterns/MVC/__init__.py b/sbapp/kivymd/tools/patterns/MVC/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/patterns/MVC/data/locales/po/en.po b/sbapp/kivymd/tools/patterns/MVC/data/locales/po/en.po new file mode 100644 index 0000000..0515fab --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/data/locales/po/en.po @@ -0,0 +1,12 @@ +msgid "" +msgstr "" +"Project-Id-Version: 0.0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-10-27 18:54+0300\n" +"PO-Revision-Date: 2019-09-22 23:12+0300\n" +"Last-Translator: KivyMD library https://github.com/kivymd/KivyMD\n" +"Language-Team: KivyMD library https://github.com/kivymd/KivyMD\n" +"Language: Russian\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" diff --git a/sbapp/kivymd/tools/patterns/MVC/data/locales/po/ru.po b/sbapp/kivymd/tools/patterns/MVC/data/locales/po/ru.po new file mode 100644 index 0000000..295b482 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/data/locales/po/ru.po @@ -0,0 +1,12 @@ +msgid "" +msgstr "" +"Project-Id-Version: 0.0.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-10-27 18:54+0300\n" +"PO-Revision-Date: 2019-09-22 23:12+0300\n" +"Last-Translator: KivyMD library https://github.com/kivymd/KivyMD\n" +"Language-Team: KivyMD library https://github.com/kivymd/KivyMD\n" +"Language: English\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" diff --git a/sbapp/kivymd/tools/patterns/MVC/libs/__init__.py b/sbapp/kivymd/tools/patterns/MVC/libs/__init__.py new file mode 100644 index 0000000..06c3e4b --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/libs/__init__.py @@ -0,0 +1 @@ +# This package is for additional application modules. diff --git a/sbapp/kivymd/tools/patterns/MVC/libs/translation.py b/sbapp/kivymd/tools/patterns/MVC/libs/translation.py new file mode 100644 index 0000000..ecd1aa9 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/libs/translation.py @@ -0,0 +1,49 @@ +import gettext + +from kivy.lang import Observable + + +class Translation(Observable): + """Original source - https://github.com/tito/kivy-gettext-example.""" + + observers = [] + + def __init__(self, defaultlang, domian, resource_dir): + super().__init__() + self.ugettext = None + self.lang = defaultlang + self.domian = domian + self.resource_dir = resource_dir + self.switch_lang(self.lang) + + def _(self, text): + return self.ugettext(text) + + def fbind(self, name, func, args, **kwargs): + if name == "_": + self.observers.append((func, args, kwargs)) + else: + return super().fbind(name, func, *args, **kwargs) + + def funbind(self, name, func, args, **kwargs): + if name == "_": + key = (func, args, kwargs) + if key in self.observers: + self.observers.remove(key) + else: + return super().funbind(name, func, *args, **kwargs) + + def switch_lang(self, lang): + locales = gettext.translation( + self.domian, self.resource_dir, languages=[lang] + ) + try: + self.ugettext = locales.ugettext + except AttributeError: + self.ugettext = locales.gettext + + for func, largs, kwargs in self.observers: + try: + func(largs, None, None) + except ReferenceError: + pass diff --git a/sbapp/kivymd/tools/patterns/MVC/main.py_tmp b/sbapp/kivymd/tools/patterns/MVC/main.py_tmp new file mode 100644 index 0000000..d697286 --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/main.py_tmp @@ -0,0 +1,54 @@ +""" +The entry point to the application. + +The application uses the MVC template. Adhering to the principles of clean +architecture means ensuring that your application is easy to test, maintain, +and modernize. + +You can read more about this template at the links below: + +https://github.com/HeaTTheatR/LoginAppMVC +https://en.wikipedia.org/wiki/Model–view–controller +""" + +from typing import NoReturn + +from kivy.uix.screenmanager import ScreenManager%s + +from kivymd.app import MDApp + +from View.screens import screens%s +%s + +class %s(MDApp):%s + def __init__(self, **kwargs): + super().__init__(**kwargs)%s + self.load_all_kv_files(self.directory) + # This is the screen manager that will contain all the screens of your + # application. + self.manager_screens = ScreenManager() + %s + def build(self) -> ScreenManager: + self.generate_application_screens() + return self.manager_screens + + def generate_application_screens(self) -> NoReturn: + """ + Creating and adding screens to the screen manager. + You should not change this cycle unnecessarily. He is self-sufficient. + + If you need to add any screen, open the `View.screens.py` module and + see how new screens are added according to the given application + architecture. + """ + + for i, name_screen in enumerate(screens.keys()): + model = screens[name_screen]["model"](%s) + controller = screens[name_screen]["controller"](model) + view = controller.get_view() + view.manager_screens = self.manager_screens + view.name = name_screen + self.manager_screens.add_widget(view) +%s%s + +%s().run() diff --git a/sbapp/kivymd/tools/patterns/MVC/messages.pot b/sbapp/kivymd/tools/patterns/MVC/messages.pot new file mode 100644 index 0000000..2ce25cc --- /dev/null +++ b/sbapp/kivymd/tools/patterns/MVC/messages.pot @@ -0,0 +1,18 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-10-27 18:54+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" diff --git a/sbapp/kivymd/tools/patterns/__init__.py b/sbapp/kivymd/tools/patterns/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/patterns/create_project.py b/sbapp/kivymd/tools/patterns/create_project.py new file mode 100644 index 0000000..de59c1a --- /dev/null +++ b/sbapp/kivymd/tools/patterns/create_project.py @@ -0,0 +1,1065 @@ +""" +Script creates a project with the MVC pattern +============================================= + +.. versionadded:: 1.0.0 + +.. seealso:: + + `MVC pattern `_ + +.. rubric:: Use a clean architecture for your applications. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/preview-mvc.png + :align: center + +Use a clean architecture for your applications. KivyMD allows you to quickly +create a project template with the MVC pattern. So far, this is the only +pattern that this utility offers. You can also include database support in +your project. At the moment, support for the Firebase database +(the basic implementation of the real time database) and RestDB +(the full implementation) is available. + +Project creation +---------------- + +Template command:: + + python -m kivymd.tools.patterns.create_project \\ + name_pattern \\ + path_to_project \\ + name_project \\ + python_version \\ + kivy_version + +Example command:: + + python -m kivymd.tools.patterns.create_project \\ + MVC \\ + /Users/macbookair/Projects \\ + MyMVCProject \\ + python3.10 \\ + 2.1.0 + +This command will by default create a project with an MVC pattern. +Also, the project will create a virtual environment with Python 3.10, +Kivy version 2.1.0 and KivyMD master version. + +.. note:: + Please note that the Python version you specified must be installed on your + computer. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mvc-base.png + :align: center + +Creating a project using a database +----------------------------------- + +.. note:: + Note that in the following command, you can use one of two database names: + 'firebase' or 'restdb'. + +Template command:: + + python -m kivymd.tools.patterns.create_project \\ + name_pattern \\ + path_to_project \\ + name_project \\ + python_version \\ + kivy_version \\ + --name_database + +Example command:: + + python -m kivymd.tools.patterns.create_project \\ + MVC \\ + /Users/macbookair/Projects \\ + MyMVCProject \\ + python3.10 \\ + 2.1.0 \\ + --name_database restdb + +This command will create a project with an MVC template by default. +The project will also create a virtual environment with Python 3.10, +Kivy version 2.1.0, KivyMD master version and a wrapper for working with +the database restdb.io. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mvc-database.png + :align: center + +.. code-block:: python + + class DataBase: + def __init__(self): + database_url = "https://restdbio-5498.restdb.io" + api_key = "7ce258d66f919d3a891d1166558765f0b4dbd" + +.. note:: + Please note that `database.py` the shell in the `DataBase` class uses the + `database_url` and `api_key` parameters on the test database (works only in read mode), + so you should use your data for the database. + +Preview of the basic MVC template using the RestDB database +----------------------------------------------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mvc-restbd-preview.png + :align: center + +Create project with hot reload +------------------------------ + +Template command:: + + python -m kivymd.tools.patterns.create_project \\ + name_pattern \\ + path_to_project \\ + name_project \\ + python_version \\ + kivy_version \\ + --use_hotreload + +Example command:: + + python -m kivymd.tools.patterns.create_project \\ + MVC \\ + /Users/macbookair/Projects \\ + MyMVCProject \\ + python3.10 \\ + 2.1.0 \\ + --use_hotreload yes + +After creating the project, open the file `main.py`, there is a lot of useful +information. Also, the necessary information is in other modules of the project +in the form of comments. So do not forget to look at the source files of the +created project. + +Others command line arguments +====================== + +Required Arguments +------------------ + +- pattern + - the name of the pattern with which the project will be created + +- directory + - directory in which the project will be created + +- name + - project name + +- python_version + - the version of Python (specify as `python3.9` or `python3.8`) with + - which the virtual environment will be created + +- kivy_version + - version of Kivy (specify as `2.1.0` or `master`) that will be used in the project + +Optional arguments +------------------ + +- name_screen + - the name of the class which be used when creating the project pattern + +- name_database + - provides a basic template for working with the 'firebase' library + - or a complete implementation for working with a database 'restdb.io' + +- use_hotreload + - creates a hot reload entry point to the application + +-use_localization + - creates application localization files + +.. warning:: On Windows, hot reloading of Python files may not work. + But, for example, there is no such problem in macOS. If you fix this, + please report it to the KivyMD community. +""" + +import os +import re +import shutil +from typing import Union + +from kivy import Logger, platform + +from kivymd import path as kivymd_path +from kivymd.tools.argument_parser import ArgumentParserWithHelp + +_database_model = '''import multitasking + +from Model.base_model import BaseScreenModel + +multitasking.set_max_threads(10) + + +class {name_screen}Model(BaseScreenModel): + """ + Implements the logic of the + :class:`~View.{name_screen}.{module_name}.{name_screen}View` class. + """ + + def __init__(self, database): + self.database = database + # Dict: + # 'login': 'KivyMD' + # 'password': '1111' + self.user_data = dict() + self._data_validation_status = None + + @property + def data_validation_status(self): + return self._data_validation_status + + @data_validation_status.setter + def data_validation_status(self, value): + self._data_validation_status = value + # We notify the View - + # :class:`~View.{name_screen}.{module_name}.{name_screen}View` about the + # changes that have occurred in the data model. + self.notify_observers({notify_name_screen}) + + @multitasking.task + def check_data(self): + """ + Get data from the database and compares this data with the data entered + by the user. This method is completely asynchronous. + It does not return any value. + """ + + data = self.database.get_data_from_collection(self.database.USER_DATA) + data_validation_status = False + + if data: + if self.database.name == "RestDB": + data = data[0] + else: + data = list(data.values())[0] + if ( + data["login"] == self.user_data["login"] + and data["password"] == self.user_data["password"] + ): + data_validation_status = True + self.data_validation_status = data_validation_status + + def set_user_data(self, key: str, value: str) -> None: + """Sets a dictionary of data that the user enters.""" + + self.user_data[key] = value + + def reset_data_validation_status(self) -> None: + self.data_validation_status = None +''' + +_without_database_model = '''from Model.base_model import BaseScreenModel + + +class {name_screen}Model(BaseScreenModel): + """ + Implements the logic of the + :class:`~View.{module_name}.{name_screen}.{name_screen}View` class. + """''' + +_database_controller = ''' +{import_module} + + +class {name_screen}Controller: + """ + The `{name_screen}Controller` class represents a controller implementation. + Coordinates work of the view with the model. + The controller implements the strategy pattern. The controller connects to + the view to control its actions. + """ + + def __init__(self, model): + self.model = model # Model.{module_name}.{name_screen}Model object + self.view = {name_view}( + controller=self, model=self.model + ) + + def set_user_data(self, key: str, value: str) -> None: + """Called every time the user enters text into the text fields.""" + + self.model.set_user_data(key, value) + + def on_tap_button_login(self) -> None: + """Called when the `LOGIN` button is pressed.""" + + self.view.show_dialog_wait() + self.model.check_data() + + def reset_data_validation_status(self, *args) -> None: + self.model.reset_data_validation_status() +''' + +_without_database_controller = ''' +{import_module} + + +class {name_screen}Controller: + """ + The `{name_screen}Controller` class represents a controller implementation. + Coordinates work of the view with the model. + The controller implements the strategy pattern. The controller connects to + the view to control its actions. + """ + + def __init__(self, model): + self.model = model # Model.{module_name}.{name_screen}Model + self.view = {name_view}(controller=self, model=self.model) + + def on_tap_button_login(self) -> None: + """Called when the `LOGIN` button is pressed.""" + + def set_user_data(self, key, value) -> None: + """Called every time the user enters text into the text fields.""" +''' + +_database_view_import = """from typing import Union + +from kivy.clock import Clock + +from kivymd.uix.dialog import MDDialog +from kivymd.uix.snackbar import Snackbar +""" + +_without_database_view_import = """ +""" + +_database_view_methods = ''' def __init__(self, **kwargs): + super().__init__(**kwargs) + self.dialog = MDDialog() + self.dialog.bind(on_dismiss=self.controller.reset_data_validation_status) + + def show_dialog_wait(self) -> None: + """Displays a wait dialog while the model is processing data.""" + + self.dialog.auto_dismiss = False + self.dialog.text = "Data validation..." + self.dialog.open() + + def show_toast(self, interval: Union[int, float]) -> None: + Snackbar( + text="You have passed the verification successfully!", + snackbar_x="10dp", + snackbar_y="10dp", + size_hint_x=.8, + bg_color=self.theme_cls.primary_color, + ).open() +''' + +_database_view_model_is_changed_method = """if self.model.data_validation_status: + self.dialog.dismiss() + Clock.schedule_once(self.show_toast, 1) + if self.model.data_validation_status is False: + self.dialog.text = "Wrong data!" + self.dialog.auto_dismiss = True +""" + +_firebase_requirements = """kivy==2.1.0 +kivymd==1.0.0 +multitasking +firebase +firebase-admin +python_jwt +gcloud +sseclient +pycryptodome==3.4.3 +requests_toolbelt +""" + +_without_firebase_requirements = """kivy==2.1.0 +kivymd==1.0.0 +""" + +_hot_reload_main = ''' +""" +Script for managing hot reloading of the project. +For more details see the documentation page - + +https://kivymd.readthedocs.io/en/latest/api/kivymd/tools/patterns/create_project/ + +To run the application in hot boot mode, execute the command in the console: +DEBUG=1 python main.py +""" + +import importlib +import os + +from kivy import Config +from kivy.uix.screenmanager import ScreenManager + +from PIL import ImageGrab + +# TODO: You may know an easier way to get the size of a computer display. +resolution = ImageGrab.grab().size + +# Change the values of the application window size as you need. +Config.set("graphics", "height", resolution[1]) +Config.set("graphics", "width", "400") + +from kivy.core.window import Window%s + +# Place the application window on the right side of the computer screen. +Window.top = 0 +Window.left = resolution[0] - Window.width + +from kivymd.tools.hotreload.app import MDApp +%s%s + +class %s(MDApp): + KV_FILES = { + os.path.join( + os.getcwd(), + "View", + "%s", + "%s.kv", + ), + }%s + + def build_app(self) -> ScreenManager: + """ + In this method, you don't need to change anything other than the + application theme. + """ + + import View.screens + + self.theme_cls.primary_palette = "Orange" + self.manager_screens = ScreenManager()%s%s + Window.bind(on_key_down=self.on_keyboard_down) + importlib.reload(View.screens) + screens = View.screens.screens + + for i, name_screen in enumerate(screens.keys()): + model = screens[name_screen]["model"](%s) + controller = screens[name_screen]["controller"](model) + view = controller.get_view() + view.manager_screens = self.manager_screens + view.name = name_screen + self.manager_screens.add_widget(view) + + return self.manager_screens + + def on_keyboard_down(self, window, keyboard, keycode, text, modifiers) -> None: + """ + The method handles keyboard events. + + By default, a forced restart of an application is tied to the + `CTRL+R` key on Windows OS and `COMMAND+R` on Mac OS. + """ + + if "meta" in modifiers or "ctrl" in modifiers and text == "r": + self.rebuild()%s%s + + +%s().run() + +# After you finish the project, remove the above code and uncomment the below +# code to test the application normally without hot reloading. +''' + +available_patterns = ["MVC"] +available_databases = ["firebase", "restdb"] + + +def main(): + parser = create_argument_parser() + args = parser.parse_args() + + pattern_name = args.pattern + project_directory = args.directory + project_name = "".join(args.name.split(" ")) + kivy_version = args.kivy_version + python_version = args.python_version + if "3" not in python_version: + parser.error("Python must be at least version 3") + name_screen = "".join(args.name_screen.split(" ")) + path_to_project = os.path.join(project_directory, project_name) + name_database = args.name_database + if name_database != "no" and name_database not in available_databases: + parser.error( + f"The database name must be one of the {available_databases} list" + ) + use_hotreload = args.use_hotreload + use_localization = args.use_localization + + # Check arguments. + if name_screen[-6:] != "Screen": + parser.error( + "Name of the screen must contain the word 'Screen' at the end. " + "\nFor example - '... --name_screen MyFirstScreen'" + ) + module_name = chek_camel_case_name_project(name_screen) + if not module_name: + parser.error( + "The name of the screen should be written in camel case style. " + "\nFor example - 'MyFirstScreen'" + ) + module_name = "_".join([name.lower() for name in module_name]) + if not os.path.exists( + os.path.join(kivymd_path, "tools", "patterns", pattern_name) + ): + parser.error( + f"There is no {pattern_name} pattern.\n" + f"Only {available_patterns} template is available." + ) + + # Call the functions of creating a project. + if not os.path.exists(path_to_project): + shutil.copytree( + os.path.join(kivymd_path, "tools", "patterns", pattern_name), + path_to_project, + ) + create_main( + name_database, use_localization, path_to_project, project_name + ) + create_model(name_database, name_screen, module_name, path_to_project) + create_controller( + name_database, + use_hotreload, + name_screen, + module_name, + path_to_project, + ) + create_view( + name_database, + use_localization, + name_screen, + module_name, + path_to_project, + ) + create_requirements(name_database, path_to_project) + os.makedirs(os.path.join(path_to_project, "assets", "images")) + os.mkdir(os.path.join(path_to_project, "assets", "fonts")) + rename_ext_py_tmp_to_py(path_to_project) + move_init(path_to_project, name_screen) + + if name_database != "no": + check_databases(name_database, path_to_project) + + if use_hotreload == "yes": + create_main_with_hotreload( + path_to_project, + project_name, + name_screen, + module_name, + name_database, + use_localization, + ) + with open( + os.path.join(path_to_project, "requirements.txt"), + "a", + encoding="utf-8", + ) as requirements: + requirements.write("watchdog") + + if use_localization == "yes": + Logger.info("KivyMD: Create localization files...") + create_makefile( + path_to_project, project_name, module_name, name_screen + ) + localization_po_file(path_to_project) + create_mofile(path_to_project) + else: + os.remove(os.path.join(path_to_project, "messages.pot")) + os.remove(os.path.join(path_to_project, "libs", "translation.py")) + shutil.rmtree(os.path.join(path_to_project, "data")) + Logger.info(f"KivyMD: Project '{path_to_project}' created") + Logger.info( + f"KivyMD: Create a virtual environment for '{path_to_project}' project..." + ) + create_virtual_environment(python_version, path_to_project) + Logger.info( + f"KivyMD: Install requirements for '{path_to_project}' project..." + ) + install_requirements(path_to_project, kivy_version, name_database) + else: + parser.error(f"The {path_to_project} project already exists") + + +def create_main_with_hotreload( + path_to_project: str, + project_name: str, + name_screen: str, + module_name: str, + name_database: str, + use_localization: str, +) -> None: + with open( + os.path.join(path_to_project, "main.py"), encoding="utf-8" + ) as main_file: + main_code = "" + for string in main_file.readlines(): + main_code += f"# {string}" + with open( + os.path.join(path_to_project, "main.py"), "w", encoding="utf-8" + ) as main_file: + main_file.write(f"{_hot_reload_main}\n{main_code}") + + replace_in_file( + os.path.join(path_to_project, "main.py"), + ( + "\nfrom kivy.properties import StringProperty" + if use_localization == "yes" + else "", + "\nfrom Model.database import DataBase" + if name_database != "no" + else "", + "\nfrom libs.translation import Translation\n" + if use_localization == "yes" + else "", + project_name, + name_screen, + module_name, + '\n lang = StringProperty("en")\n' + if use_localization == "yes" + else "", + "\n self.base = DataBase()\n" + if name_database != "no" + else "", + "\n self.translation = Translation(\n" + ' self.lang, "%s", f"{self.directory}/data/locales"' + "\n )" % project_name + if use_localization == "yes" + else "", + "self.database" if name_database != "no" else "", + "\n\n def on_lang(self, instance_app, lang_value: str) -> None:\n" + " self.translation.switch_lang(lang_value)\n" + if use_localization == "yes" + else "", + "\n def switch_lang(self) -> None:\n" + ' """Switch lang."""\n\n' + ' self.lang = "ru" if self.lang == "en" else "en"' + if use_localization == "yes" + else "", + project_name, + ), + ) + + +def create_main( + name_database: str, + use_localization: str, + path_to_project: str, + project_name: str, +) -> None: + replace_in_file( + os.path.join(path_to_project, "main.py_tmp"), + ( + "\nfrom kivy.properties import StringProperty" + if use_localization == "yes" + else "", + "\nfrom libs.translation import Translation" + if use_localization == "yes" + else "", + "from Model.database import DataBase\n" + if name_database != "no" + else "", + project_name, + '\n lang = StringProperty("en")\n' + if use_localization == "yes" + else "", + "\n self.translation = Translation(\n" + ' self.lang, "%s", f"{self.directory}/data/locales"' + "\n )" % project_name + if use_localization == "yes" + else "", + "self.database = DataBase()\n" if name_database != "no" else "", + "self.database" if name_database != "no" else "", + "\n def on_lang(self, instance_app, lang_value: str) -> None:\n" + " self.translation.switch_lang(lang_value)\n" + if use_localization == "yes" + else "", + "\n def switch_lang(self) -> None:\n" + ' """Switch lang."""\n\n' + ' self.lang = "ru" if self.lang == "en" else "en"\n' + if use_localization == "yes" + else "", + project_name, + ), + ) + + +def create_model( + name_database: str, name_screen: str, module_name: str, path_to_project: str +) -> None: + if name_database != "no": + database_model = _database_model.format( + name_screen=name_screen, + module_name=module_name, + notify_name_screen=f'"{" ".join(module_name.split("_"))}"', + ) + replace_in_file( + os.path.join(path_to_project, "Model", "first_screen.py_tmp"), + (database_model), + ) + else: + without_database_model = _without_database_model.format( + module_name=module_name, name_screen=name_screen + ) + replace_in_file( + os.path.join(path_to_project, "Model", "first_screen.py_tmp"), + (without_database_model), + ) + os.remove( + os.path.join(path_to_project, "Model", "database_firebase.py") + ) + os.remove(os.path.join(path_to_project, "Model", "database_restdb.py")) + os.rename( + os.path.join(path_to_project, "Model", "first_screen.py_tmp"), + os.path.join(path_to_project, "Model", f"{module_name}.py_tmp"), + ) + + +def create_controller( + name_database: str, + use_hotreload: str, + name_screen: str, + module_name: str, + path_to_project: str, +) -> None: + if name_database != "no": + database_controller = _database_controller + else: + database_controller = _without_database_controller + name_view = ( + f"View.{name_screen}.{module_name}.{name_screen}View" + if use_hotreload == "yes" + else f"{name_screen}View" + ) + database_controller = database_controller.format( + name_screen=name_screen, + module_name=module_name, + import_module="" + f"import importlib\n\n" + f"import View.{name_screen}.{module_name}\n\n" + f"# We have to manually reload the view module in order to apply the\n" + f"# changes made to the code on a subsequent hot reload.\n" + f"# If you no longer need a hot reload, you can delete this instruction.\n" + f"importlib.reload(View.{name_screen}.{module_name})" + if use_hotreload == "yes" + else f"\nfrom View.{name_screen}.{module_name} import {name_screen}View", + name_view=name_view, + ) + replace_in_file( + os.path.join(path_to_project, "Controller", "first_screen.py_tmp"), + (database_controller, name_view), + ) + os.rename( + os.path.join(path_to_project, "Controller", "first_screen.py_tmp"), + os.path.join(path_to_project, "Controller", f"{module_name}.py_tmp"), + ) + + +def create_view( + name_database: str, + use_localization: str, + name_screen: str, + module_name: str, + path_to_project: str, +) -> None: + replace_in_file( + os.path.join(path_to_project, "View", "screens.py_tmp"), + ( + module_name, + f"{name_screen}Model", + module_name, + f"{name_screen}Controller", + f'"{" ".join(module_name.split("_"))}"', + f"{name_screen}Model", + f"{name_screen}Controller", + ), + ) + replace_in_file( + os.path.join(path_to_project, "View", "FirstScreen", "first_screen.kv"), + ( + f"{name_screen}View", + name_screen, + "app.switch_lang()" if use_localization == "yes" else "x", + 'app.translation._("To log in, enter your personal data:")' + if use_localization == "yes" + else '"To log in, enter your personal data:"', + 'app.translation._("Login")' + if use_localization == "yes" + else '"Login"', + 'app.translation._("Password")' + if use_localization == "yes" + else '"Password"', + 'app.translation._("LOGIN")' + if use_localization == "yes" + else '"LOGIN"', + ), + ) + replace_in_file( + os.path.join( + path_to_project, "View", "FirstScreen", "first_screen.py_tmp" + ), + ( + _database_view_import + if name_database != "no" + else _without_database_view_import, + f"{name_screen}View", + _database_view_methods if name_database != "no" else "", + _database_view_model_is_changed_method + if name_database != "no" + else "", + ), + ) + replace_in_file( + os.path.join(path_to_project, "View", "base_screen.py_tmp"), + ( + module_name, + f"{name_screen}Model", + module_name, + f"{name_screen}Controller", + module_name, + f"{name_screen}Model", + ), + ) + os.rename( + os.path.join(path_to_project, "View", "base_screen.py_tmp"), + os.path.join(path_to_project, "View", "base_screen.py"), + ) + os.rename( + os.path.join(path_to_project, "View", "FirstScreen", "first_screen.kv"), + os.path.join( + path_to_project, "View", "FirstScreen", f"{module_name}.kv" + ), + ) + os.rename( + os.path.join( + path_to_project, "View", "FirstScreen", "first_screen.py_tmp" + ), + os.path.join( + path_to_project, "View", "FirstScreen", f"{module_name}.py_tmp" + ), + ) + os.rename( + os.path.join(path_to_project, "View", "FirstScreen"), + os.path.join(path_to_project, "View", name_screen), + ) + + +def create_requirements(name_database: str, path_to_project: str) -> None: + with open( + os.path.join(path_to_project, "requirements.txt"), "w", encoding="utf-8" + ) as requirements: + requirements.write( + _firebase_requirements + if name_database != "no" + else _without_firebase_requirements + ) + + +def create_makefile( + path_to_project: str, project_name: str, module_name: str, name_screen: str +) -> None: + path_to_makefile_tmp = os.path.join(path_to_project, "Makefile.tmp") + replace_in_file( + path_to_makefile_tmp, + ( + name_screen, + module_name, + name_screen, + module_name, + project_name, + project_name, + ), + ) + os.rename(path_to_makefile_tmp, os.path.splitext(path_to_makefile_tmp)[0]) + os.chdir(path_to_project) + os.system("make po") + + +def create_mofile(path_to_project: str) -> None: + os.chdir(path_to_project) + os.system("make mo") + + +def create_virtual_environment( + python_version: str, path_to_project: str +) -> None: + os.system(f"{python_version} -m pip install virtualenv") + os.system( + f"virtualenv -p {python_version} {os.path.join(path_to_project, 'venv')}" + ) + + +def localization_po_file(path_to_project: str) -> None: + path_to_file_po = os.path.join( + path_to_project, "data", "locales", "po", "ru.po" + ) + with open(path_to_file_po, "rt", encoding="utf-8") as file_po: + file_po_content = ( + file_po.read() + .replace( + 'msgid "To log in, enter your personal data:"\nmsgstr ""', + 'msgid "To log in, enter your personal data:"\nmsgstr "Для входа введите свои личные данные"', + ) + .replace( + 'msgid "Login"\nmsgstr ""', 'msgid "Login"\nmsgstr "Логин"' + ) + .replace( + 'msgid "Password"\nmsgstr ""', + 'msgid "Password"\nmsgstr "Пароль"', + ) + .replace( + 'msgid "LOGIN"\nmsgstr ""', 'msgid "LOGIN"\nmsgstr "ЛОГИН"' + ) + ) + with open(path_to_file_po, "wt", encoding="utf-8") as file_po: + file_po.write(file_po_content) + + +def install_requirements( + path_to_project: str, kivy_version: str, name_database: str +) -> None: + python = os.path.join(path_to_project, "venv", "bin", "python3") + if kivy_version == "master": + if platform == "macosx": + os.system( + f"{python} -m pip install 'kivy[base] @ https://github.com/kivy/kivy/archive/master.zip'" + ) + else: + os.system( + f"{python} -m pip install https://github.com/kivy/kivy/archive/master.zip" + ) + elif kivy_version == "stable": + os.system(f"{python} -m pip install kivy") + else: + os.system(f"{python} -m pip install kivy=={kivy_version}") + os.system( + f"{python} -m pip install https://github.com/kivymd/KivyMD/archive/master.zip" + ) + os.system(f"{python} -m pip install watchdog") + if name_database == "firebase": + os.system( + f"{python} -m pip install " + f"multitasking " + f"firebase " + f"firebase-admin " + f"python_jwt " + f"gcloud " + f"sseclient " + f"pycryptodome==3.4.3 " + f"requests_toolbelt " + f"watchdog " + ) + os.system( + f"{os.path.join(path_to_project, 'venv', 'bin', 'python3')} -m pip list" + ) + + +def rename_ext_py_tmp_to_py(path_to_project: str) -> None: + for path_to_dir, dirs, files in os.walk(path_to_project): + for name_file in files: + if os.path.splitext(name_file)[1] == ".py_tmp": + os.rename( + os.path.join(path_to_dir, name_file), + os.path.join( + path_to_dir, f"{os.path.splitext(name_file)[0]}.py" + ), + ) + + +def move_init(path_to_project: str, name_screen: str) -> None: + path_to_init_file = __file__.replace("create_project", "__init__") + for name_dir in ("Controller", "Model", "Utility", "View"): + shutil.copy( + path_to_init_file, + os.path.join(path_to_project, name_dir, "__init__.py"), + ) + shutil.copy( + path_to_init_file, + os.path.join(path_to_project, "View", name_screen, "__init__.py"), + ) + path_to_components = os.path.join( + path_to_project, "View", name_screen, "components" + ) + os.mkdir(path_to_components) + shutil.copy( + path_to_init_file, os.path.join(path_to_components, "__init__.py") + ) + + +def check_databases(name_database: str, path_to_project: str) -> None: + databases = {"firebase": "restdb", "restdb": "firebase"} + os.remove( + os.path.join( + path_to_project, "Model", f"database_{databases[name_database]}.py" + ) + ) + os.rename( + os.path.join(path_to_project, "Model", f"database_{name_database}.py"), + os.path.join(path_to_project, "Model", "database.py"), + ) + + +def chek_camel_case_name_project(name_project) -> Union[bool, list]: + result = re.findall("[A-Z][a-z]*", name_project) + if len(result) == 1: + return False + return result + + +def replace_in_file(path_to_file: str, args) -> None: + with open(path_to_file, "rt", encoding="utf-8") as file_content: + new_file_content = file_content.read() % (args) + with open(path_to_file, "wt", encoding="utf-8") as original_file: + original_file.write(new_file_content) + + +def create_argument_parser() -> ArgumentParserWithHelp: + parser = ArgumentParserWithHelp( + prog="create_project.py", + allow_abbrev=False, + ) + parser.add_argument( + "pattern", + help="the name of the pattern with which the project will be created.", + ) + parser.add_argument( + "directory", + help="directory in which the project will be created.", + ) + parser.add_argument( + "name", + help="project name.", + ) + parser.add_argument( + "python_version", + help="the version of Python (specify as `python3.9` or `python3.8`) " + "with which the virtual environment will be created.", + ) + parser.add_argument( + "kivy_version", + help="version of Kivy (specify as `2.1.0` or `master`) that will be " + "used in the project.", + ) + parser.add_argument( + "--name_screen", + default="MainScreen", + help="the name of the class wich be used when creating the project pattern.", + ) + parser.add_argument( + "--name_database", + default="no", + help="name of the database provider ('firebase' or 'restdb').", + ) + parser.add_argument( + "--use_hotreload", + default="no", + help="creates a hot reload entry point to the application.", + ) + parser.add_argument( + "--use_localization", + default="no", + help="creates application localization files.", + ) + return parser + + +if __name__ == "__main__": + main() diff --git a/sbapp/kivymd/tools/release/__init__.py b/sbapp/kivymd/tools/release/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/sbapp/kivymd/tools/release/git_commands.py b/sbapp/kivymd/tools/release/git_commands.py new file mode 100644 index 0000000..e194765 --- /dev/null +++ b/sbapp/kivymd/tools/release/git_commands.py @@ -0,0 +1,94 @@ +# Copyright (c) 2019-2021 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/sbapp/kivymd/tools/release/make_release.py b/sbapp/kivymd/tools/release/make_release.py new file mode 100644 index 0000000..fbb616a --- /dev/null +++ b/sbapp/kivymd/tools/release/make_release.py @@ -0,0 +1,350 @@ +# Copyright (c) 2019-2021 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.md` +* Format files +* Rename file "unreleased.rst" to version, add to index.rst +* Commit "Version ..." +* Create tag +* Add `unreleased.rst` to Changelog, add to `index.rst` +* Commit +* Git push +""" + +import os +import re +import subprocess +import sys + +from kivymd.tools.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.md`.""" + + 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}(?=, )" + 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"(?<=Changelog\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/sbapp/kivymd/tools/release/update_icons.py b/sbapp/kivymd/tools/release/update_icons.py new file mode 100644 index 0000000..2e24148 --- /dev/null +++ b/sbapp/kivymd/tools/release/update_icons.py @@ -0,0 +1,174 @@ +# Copyright (c) 2019-2021 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 + +os.environ["KIVY_NO_ARGS"] = "1" + +from kivymd.tools.release.git_commands import git_commit # NOQA E402 + +# 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/sbapp/kivymd/uix/__init__.py b/sbapp/kivymd/uix/__init__.py new file mode 100755 index 0000000..2d48cc1 --- /dev/null +++ b/sbapp/kivymd/uix/__init__.py @@ -0,0 +1,86 @@ +__all__ = ("MDAdaptiveWidget",) + +from kivy.properties import BooleanProperty +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.label import Label +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, md_widget, value: bool) -> None: + self.size_hint_y = None + if issubclass(self.__class__, Label): + self.bind( + texture_size=lambda *x: self.setter("height")( + self, self.texture_size[1] + ) + ) + else: + if not isinstance(self, (FloatLayout, Screen)): + self.bind(minimum_height=self.setter("height")) + + def on_adaptive_width(self, md_widget, value: bool) -> None: + self.size_hint_x = None + if issubclass(self.__class__, Label): + self.bind( + texture_size=lambda *x: self.setter("width")( + self, self.texture_size[0] + ) + ) + else: + if not isinstance(self, (FloatLayout, Screen)): + self.bind(minimum_width=self.setter("width")) + + def on_adaptive_size(self, md_widget, value: bool) -> None: + self.size_hint = (None, None) + if issubclass(self.__class__, Label): + self.text_size = (None, None) + self.bind( + texture_size=lambda *x: self.setter("size")( + self, self.texture_size + ) + ) + else: + if not isinstance(self, (FloatLayout, Screen)): + self.bind(minimum_size=self.setter("size")) diff --git a/sbapp/kivymd/uix/anchorlayout.py b/sbapp/kivymd/uix/anchorlayout.py new file mode 100644 index 0000000..c50fa0c --- /dev/null +++ b/sbapp/kivymd/uix/anchorlayout.py @@ -0,0 +1,92 @@ +""" +Components/AnchorLayout +======================= + +.. versionadded:: 1.0.0 + +:class:`~kivy.uix.anchorlayout.AnchorLayout` class equivalent. Simplifies working +with some widget properties. For example: + +AnchorLayout +------------ + +.. code-block:: + + AnchorLayout: + canvas: + Color: + rgba: app.theme_cls.primary_color + Rectangle: + pos: self.pos + size: self.size + +AnchorLayout +------------ + +.. code-block:: + + MDBoxLayout: + 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 +""" + +__all__ = ("MDAnchorLayout",) + +from kivy.uix.anchorlayout import AnchorLayout + +from kivymd.uix import MDAdaptiveWidget + + +class MDAnchorLayout(AnchorLayout, MDAdaptiveWidget): + pass diff --git a/sbapp/kivymd/uix/backdrop/__init__.py b/sbapp/kivymd/uix/backdrop/__init__.py new file mode 100644 index 0000000..7a5e6cf --- /dev/null +++ b/sbapp/kivymd/uix/backdrop/__init__.py @@ -0,0 +1 @@ +from .backdrop import MDBackdrop # NOQA F401 diff --git a/sbapp/kivymd/uix/backdrop/backdrop.kv b/sbapp/kivymd/uix/backdrop/backdrop.kv new file mode 100644 index 0000000..372c754 --- /dev/null +++ b/sbapp/kivymd/uix/backdrop/backdrop.kv @@ -0,0 +1,50 @@ + + md_bg_color: + root.theme_cls.primary_color \ + if not root.back_layer_color \ + else root.back_layer_color + + MDBackdropToolbar: + id: toolbar + type_height: "small" + anchor_title: root.anchor_title + title: root.title + elevation: 0 + left_action_items: root.left_action_items + right_action_items: root.right_action_items + pos_hint: {"top": 1} + md_bg_color: + root.theme_cls.primary_color \ + if not root.back_layer_color \ + else root.back_layer_color + + _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 + md_bg_color: + root.theme_cls.bg_normal \ + if not root.front_layer_color \ + else root.front_layer_color + radius: + [root.radius_left, root.radius_left, + root.radius_right, root.radius_right] + + OneLineListItem: + id: header_button + text: root.header_text + divider: None + _no_ripple_effect: True + on_press: root.open() + + MDBoxLayout: + id: front_layer + padding: 0, 0, 0, "10dp" diff --git a/sbapp/kivymd/uix/backdrop/backdrop.py b/sbapp/kivymd/uix/backdrop/backdrop.py new file mode 100644 index 0000000..77a3028 --- /dev/null +++ b/sbapp/kivymd/uix/backdrop/backdrop.py @@ -0,0 +1,441 @@ +""" +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 kivymd.uix.screen import MDScreen + 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(MDScreen): + 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", +) + +import os +from typing import Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.properties import ( + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.boxlayout import BoxLayout + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import FakeRectangularElevationBehavior +from kivymd.uix.card import MDCard +from kivymd.uix.floatlayout import MDFloatLayout +from kivymd.uix.toolbar.toolbar import ActionTopAppBarButton, MDTopAppBar + +with open( + os.path.join(uix_path, "backdrop", "backdrop.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDBackdrop(ThemableBehavior, MDFloatLayout): + """ + :Events: + :attr:`on_open` + When the front layer drops. + :attr:`on_close` + When the front layer rises. + """ + + anchor_title = OptionProperty("left", options=["left", "center", "right"]) + """ + Position toolbar title. Only used with `material_style = 'M3'` + Available options are: `'left'`, `'center'`, `'right'`. + + .. versionadded:: 1.0.0 + + :attr:`anchor_title` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'left'`. + """ + + 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.MDTopAppBar` + in back layer. For more information, see the + :class:`kivymd.uix.toolbar.MDTopAppBar` 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.MDTopAppBar.title` parameter. + + :attr:`title` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + back_layer_color = ColorProperty(None) + """ + Background color of back layer. + + :attr:`back_layer_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + front_layer_color = ColorProperty(None) + """ + Background color of front layer. + + :attr:`front_layer_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + 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'`. + """ + + opening_time = NumericProperty(0.2) + """ + The time taken for the panel to slide to the :attr:`state` `'open'`. + + .. versionadded:: 1.0.0 + + :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + opening_transition = StringProperty("out_quad") + """ + The name of the animation transition type to use when animating to + the :attr:`state` `'open'`. + + .. versionadded:: 1.0.0 + + :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_quad'`. + """ + + closing_time = NumericProperty(0.2) + """ + The time taken for the panel to slide to the :attr:`state` `'close'`. + + .. versionadded:: 1.0.0 + + :attr:`closing_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + closing_transition = StringProperty("out_quad") + """ + The name of the animation transition type to use when animating to + the :attr:`state` 'close'. + + .. versionadded:: 1.0.0 + + :attr:`closing_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_quad'`. + """ + + _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) -> None: + """When the front layer drops.""" + + def on_close(self) -> None: + """When the front layer rises.""" + + def on_left_action_items(self, instance_backdrop, menu: list) -> None: + if menu: + self.left_action_items = [menu[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_backdrop, value: bool) -> None: + if not value: + self.ids._front_layer.remove_widget(self.ids.header_button) + + def open(self, open_up_to: int = 0) -> None: + """ + 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.animate_opacity_icon() + 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=self.opening_time, t=self.opening_transition).start( + self.ids._front_layer + ) + self._front_layer_open = True + self.dispatch("on_open") + + def close(self) -> None: + """Opens the front layer.""" + + Animation(y=0, d=self.closing_time, t=self.closing_transition).start( + self.ids._front_layer + ) + self._front_layer_open = False + self.dispatch("on_close") + + def animate_opacity_icon( + self, + instance_icon_menu: Union[ActionTopAppBarButton, None] = None, + opacity_value: int = 0, + call_set_new_icon: bool = True, + ) -> None: + """Starts the opacity animation of the icon.""" + + if not instance_icon_menu: + instance_icon_menu = self.ids.toolbar.ids.left_actions.children[0] + anim = Animation( + opacity=opacity_value, + d=self.opening_time, + t=self.opening_transition, + ) + if call_set_new_icon: + anim.bind(on_complete=self.set_new_icon) + anim.start(instance_icon_menu) + + def set_new_icon( + self, + instance_animation: Animation, + instance_icon_menu: ActionTopAppBarButton, + ) -> None: + """ + Sets the icon of the button depending on the state of the backdrop. + """ + + instance_icon_menu.icon = ( + self.close_icon + if instance_icon_menu.icon == self._open_icon + else self._open_icon + ) + self.animate_opacity_icon(instance_icon_menu, 1, False) + + 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(MDTopAppBar): + """Implements a toolbar for back content.""" + + +class MDBackdropFrontLayer(BoxLayout): + """Container for front content.""" + + +class MDBackdropBackLayer(BoxLayout): + """Container for back content.""" + + +class _BackLayer(BoxLayout): + pass + + +class _FrontLayer(MDCard, FakeRectangularElevationBehavior): + pass diff --git a/sbapp/kivymd/uix/banner/__init__.py b/sbapp/kivymd/uix/banner/__init__.py new file mode 100644 index 0000000..3ed33d0 --- /dev/null +++ b/sbapp/kivymd/uix/banner/__init__.py @@ -0,0 +1 @@ +from .banner import MDBanner # NOQA F401 diff --git a/sbapp/kivymd/uix/banner/banner.kv b/sbapp/kivymd/uix/banner/banner.kv new file mode 100644 index 0000000..3d874ce --- /dev/null +++ b/sbapp/kivymd/uix/banner/banner.kv @@ -0,0 +1,85 @@ +#:import Window kivy.core.window.Window + + + + 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 + + MDBoxLayout: + id: container_message + adaptive_height: True + + MDBoxLayout: + adaptive_size: True + pos_hint: {"right": 1} + padding: 0, 0, "8dp", "8dp" + spacing: "8dp" + + MDBoxLayout: + id: left_action_box + adaptive_size: True + + MDBoxLayout: + id: right_action_box + adaptive_size: True diff --git a/sbapp/kivymd/uix/banner/banner.py b/sbapp/kivymd/uix/banner/banner.py new file mode 100644 index 0000000..7feb5c9 --- /dev/null +++ b/sbapp/kivymd/uix/banner/banner.py @@ -0,0 +1,433 @@ +""" +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 + + MDTopAppBar: + id: toolbar + title: "Example Banners" + elevation: 10 + pos_hint: {'top': 1} + + MDBoxLayout: + 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",) + +import os +from typing import Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BoundedNumericProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.widget import Widget + +from kivymd import uix_path +from kivymd.uix.behaviors import FakeRectangularElevationBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.button import MDFlatButton +from kivymd.uix.card import MDCard +from kivymd.uix.list import ( + OneLineAvatarListItem, + OneLineListItem, + ThreeLineAvatarListItem, + ThreeLineListItem, + TwoLineAvatarListItem, + TwoLineListItem, +) + +with open( + os.path.join(uix_path, "banner", "banner.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDBanner(MDCard, FakeRectangularElevationBehavior): + 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'`. + """ + + opening_timeout = BoundedNumericProperty(0.7, min=0.7) + """ + Time interval after which the banner will be shown. + + .. versionadded:: 1.0.0 + + :attr:`opening_timeout` is an :class:`~kivy.properties.BoundedNumericProperty` + and defaults to `0.7`. + """ + + opening_time = NumericProperty(0.15) + """ + The time taken for the banner to slide to the :attr:`state` `'open'`. + + .. versionadded:: 1.0.0 + + :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.15`. + """ + + closing_time = NumericProperty(0.15) + """ + The time taken for the banner to slide to the :attr:`state` `'close'`. + + .. versionadded:: 1.0.0 + + :attr:`closing_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.15`. + """ + + _type_message = None + _progress = False + + def add_actions_buttons( + self, instance_box: MDBoxLayout, data: list + ) -> None: + """ + Adds buttons to the banner. + + :param data: ['NAME BUTTON', ]; + """ + + 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 + instance_box.add_widget(action_button) + + def show(self) -> None: + """Displays a banner on the screen.""" + + def show(interval: Union[int, float]): + self.set_type_banner() + self.add_actions_buttons(self.ids.left_action_box, self.left_action) + self.add_actions_buttons( + self.ids.right_action_box, self.right_action + ) + self._add_banner_to_container() + Clock.schedule_once(self.animation_display_banner, 0.1) + + if not self._progress: + self._progress = True + if self.ids.container_message.children: + self.hide() + Clock.schedule_once(show, self.opening_timeout) + + def hide(self) -> None: + """Hides the banner from the screen.""" + + def hide(interval: Union[int, float]): + anim = Animation(banner_y=0, d=self.closing_time) + anim.bind(on_complete=self._remove_banner) + anim.start(self) + Animation( + y=self.over_widget.y + self.height, d=self.closing_time + ).start(self.over_widget) + + if not self._progress: + self._progress = True + Clock.schedule_once(hide, 0.5) + + def set_type_banner(self) -> None: + 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 animation_display_banner(self, interval: Union[int, float]) -> None: + Animation( + banner_y=self.height + self.vertical_pad, + d=self.opening_time, + t=self.opening_transition, + ).start(self) + anim = Animation( + y=self.over_widget.y - self.height, + d=self.opening_time, + t=self.opening_transition, + ) + anim.bind(on_complete=self._reset_progress) + anim.start(self.over_widget) + + 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() + self._reset_progress() + + def _reset_progress(self, *args): + self._progress = False + + def _add_banner_to_container(self) -> None: + self.ids.container_message.add_widget( + self._type_message(text_message=self.text, icon=self.icon) + ) + + +class BaseBanner(Widget): + """Implements the base banner class.""" + + text_message = ListProperty(["", "", ""]) + """ + List of banner strings. First, second and, respectively, third lines. + + :attr:`text_message` is an :class:`~kivy.properties.ListProperty` + and defaults to `['', '', '']`. + """ + + icon = StringProperty() + """ + Icon banner. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + 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/sbapp/kivymd/uix/behaviors/__init__.py b/sbapp/kivymd/uix/behaviors/__init__.py new file mode 100755 index 0000000..639acff --- /dev/null +++ b/sbapp/kivymd/uix/behaviors/__init__.py @@ -0,0 +1,26 @@ +""" +Behaviors +========= + +Modules and classes implementing various behaviors for buttons etc. +""" + +# flake8: NOQA +from .hover_behavior import HoverBehavior # isort:skip +from .backgroundcolor_behavior import ( + BackgroundColorBehavior, + SpecificBackgroundColorBehavior, +) +from .elevation import ( + CircularElevationBehavior, + CommonElevationBehavior, + FakeCircularElevationBehavior, + FakeRectangularElevationBehavior, + ObservableShadow, + RectangularElevationBehavior, + RoundedRectangularElevationBehavior, +) +from .focus_behavior import FocusBehavior +from .magic_behavior import MagicBehavior +from .ripple_behavior import CircularRippleBehavior, RectangularRippleBehavior +from .touch_behavior import TouchBehavior diff --git a/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py b/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py new file mode 100755 index 0000000..d96fd60 --- /dev/null +++ b/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py @@ -0,0 +1,238 @@ +""" +Behaviors/Background Color +========================== + +.. note:: The following classes are intended for in-house use of the library. +""" + +__all__ = ("BackgroundColorBehavior", "SpecificBackgroundColorBehavior") + +from typing import List + +from kivy.lang import Builder +from kivy.properties import ( + ColorProperty, + ListProperty, + NumericProperty, + OptionProperty, + ReferenceListProperty, + StringProperty, + VariableListProperty, +) +from kivy.utils import get_color_from_hex + +from kivymd.color_definitions import hue, palette, text_colors +from kivymd.theming import ThemeManager + +from .elevation import CommonElevationBehavior + +Builder.load_string( + """ +#:import RelativeLayout kivy.uix.relativelayout.RelativeLayout + + + + canvas: + PushMatrix + Rotate: + angle: self.angle + origin: self._background_origin + Color: + rgba: self.md_bg_color + RoundedRectangle: + group: "Background_instruction" + size: self.size + pos: self.pos if not isinstance(self, RelativeLayout) else (0, 0) + # FIXME: Sometimes the radius has the value [], which get a + # `GraphicException: + # Invalid radius value, must be list of tuples/numerics` error` + radius: root.radius if root.radius else [0, 0, 0, 0] + source: root.background + Color: + rgba: self.line_color if self.line_color else (0, 0, 0, 0) + Line: + width: root.line_width + rounded_rectangle: + [ \ + self.x, + self.y, \ + self.width, \ + self.height, \ + *self.radius, \ + 100, \ + ] + PopMatrix +""", + filename="BackgroundColorBehavior.kv", +) + + +class BackgroundColorBehavior(CommonElevationBehavior): + background = StringProperty() + """ + Background image path. + + :attr:`background` is a :class:`~kivy.properties.StringProperty` + and defaults to `None`. + """ + + radius = VariableListProperty([0], length=4) + """ + 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.VariableListProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + md_bg_color = ColorProperty([1, 1, 1, 0]) + """ + 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.ColorProperty` + and defaults to `[1, 1, 1, 0]`. + """ + + line_color = ColorProperty([0, 0, 0, 0]) + """ + If a custom value is specified for the `line_color parameter`, the border + of the specified color will be used to border the widget: + + .. code-block:: kv + + MDBoxLayout: + size_hint: .5, .2 + md_bg_color: 0, 1, 1, .5 + line_color: 0, 0, 1, 1 + radius: [24, ] + + .. versionadded:: 0.104.2 + + :attr:`line_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + line_width = NumericProperty(1) + """ + Border of the specified width will be used to border the widget. + + .. versionadded:: 1.0.0 + + :attr:`line_width` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + angle = NumericProperty(0) + background_origin = ListProperty(None) + + _background_x = NumericProperty(0) + _background_y = NumericProperty(0) + _background_origin = ReferenceListProperty( + _background_x, + _background_y, + ) + + def __init__(self, **kwarg): + super().__init__(**kwarg) + self.bind(pos=self.update_background_origin) + + def update_background_origin( + self, instance_md_widget, pos: List[float] + ) -> None: + if self.background_origin: + self._background_origin = self.background_origin + else: + self._background_origin = self.center + + +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 = ColorProperty([0, 0, 0, 0.87]) + """ + :attr:`specific_text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0.87]`. + """ + + specific_secondary_text_color = ColorProperty([0, 0, 0, 0.87]) + """ + :attr:`specific_secondary_text_color`is an :class:`~kivy.properties.ColorProperty` + 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_theme_manager: ThemeManager, theme_style: str + ) -> None: + 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/sbapp/kivymd/uix/behaviors/elevation.py b/sbapp/kivymd/uix/behaviors/elevation.py new file mode 100755 index 0000000..584c82b --- /dev/null +++ b/sbapp/kivymd/uix/behaviors/elevation.py @@ -0,0 +1,1475 @@ +""" +Behaviors/Elevation +=================== + +.. seealso:: + + `Material Design spec, Elevation `_ + +.. rubric:: Elevation is the relative distance between two surfaces along the z-axis. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/elevation-previous.png + :align: center + +There are 5 classes in KivyMD that can simulate shadow: + + #. :class:`~FakeRectangularElevationBehavior` + #. :class:`~FakeCircularElevationBehavior` + + #. :class:`~RectangularElevationBehavior` + #. :class:`~CircularElevationBehavior` + #. :class:`~RoundedRectangularElevationBehavior` + +By default, KivyMD widgets use the elevation behavior implemented in classes +:class:`~FakeRectangularElevationBehavior` and :class:`~FakeCircularElevationBehavior` +for cast shadows. These classes use the old method of rendering shadows and it +doesn't look very aesthetically pleasing. Shadows are harsh, no softness: + +The :class:`~RectangularElevationBehavior`, :class:`~CircularElevationBehavior`, +:class:`~RoundedRectangularElevationBehavior` classes use the new shadow +rendering algorithm, based on textures creation using the `Pillow` library. +It looks very aesthetically pleasing and beautiful. + +.. warning:: Remember that :class:`~RectangularElevationBehavior`, + :class:`~CircularElevationBehavior`, :class:`~RoundedRectangularElevationBehavior` + classes require a lot of resources from the device on which your application will run, + so you should not use these classes on mobile devices. + +.. code-block:: python + + from kivy.lang import Builder + from kivy.uix.widget import Widget + + from kivymd.app import MDApp + from kivymd.uix.card import MDCard + from kivymd.uix.behaviors import RectangularElevationBehavior + from kivymd.uix.boxlayout import MDBoxLayout + + KV = ''' + + adaptive_size: True + orientation: "vertical" + spacing: "36dp" + + + + size_hint: None, None + size: 100, 100 + md_bg_color: 0, 0, 1, 1 + elevation: 36 + pos_hint: {'center_x': .5} + + + MDFloatLayout: + + MDBoxLayout: + adaptive_size: True + pos_hint: {'center_x': .5, 'center_y': .5} + spacing: "56dp" + + Box: + + MDLabel: + text: "Deprecated shadow rendering" + adaptive_size: True + + DeprecatedShadowWidget: + + MDLabel: + text: "Doesn't require a lot of resources" + adaptive_size: True + + Box: + + MDLabel: + text: "New shadow rendering" + adaptive_size: True + + NewShadowWidget: + + MDLabel: + text: "It takes a lot of resources" + adaptive_size: True + ''' + + + class BaseShadowWidget(Widget): + pass + + + class DeprecatedShadowWidget(MDCard, BaseShadowWidget): + '''Deprecated shadow rendering. Doesn't require a lot of resources.''' + + + class NewShadowWidget(RectangularElevationBehavior, BaseShadowWidget, MDBoxLayout): + '''New shadow rendering. It takes a lot of resources.''' + + + class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/elevation-differential.png + :align: center + + +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, + FakeRectangularElevationBehavior, + ) + + KV = ''' + : + size_hint: None, None + size: "250dp", "50dp" + + + MDScreen: + + # With elevation effect + RectangularElevationButton: + pos_hint: {"center_x": .5, "center_y": .6} + elevation: 18 + + # Without elevation effect + RectangularElevationButton: + pos_hint: {"center_x": .5, "center_y": .4} + ''' + + + class RectangularElevationButton( + RectangularRippleBehavior, + FakeRectangularElevationBehavior, + 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 circular button: + +.. code-block:: python + + from kivy.lang import Builder + from kivy.uix.behaviors import ButtonBehavior + + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.app import MDApp + from kivymd.uix.behaviors import ( + CircularRippleBehavior, + FakeCircularElevationBehavior, + ) + + KV = ''' + : + size_hint: None, None + size: "100dp", "100dp" + radius: self.size[0] / 2 + md_bg_color: 0, 0, 1, 1 + + MDIcon: + icon: "hand-heart" + halign: "center" + valign: "center" + size: root.size + pos: root.pos + font_size: root.size[0] * .6 + theme_text_color: "Custom" + text_color: [1] * 4 + + + MDScreen: + + CircularElevationButton: + pos_hint: {"center_x": .5, "center_y": .6} + elevation: 24 + ''' + + + class CircularElevationButton( + FakeCircularElevationBehavior, + CircularRippleBehavior, + ButtonBehavior, + MDBoxLayout, + ): + pass + + + + 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-fake-elevation.png + :align: center + +Animating the elevation +----------------------- + +.. code-block:: python + + from kivy.animation import Animation + from kivy.lang import Builder + from kivy.properties import ObjectProperty + from kivy.uix.behaviors import ButtonBehavior + + from kivymd.app import MDApp + from kivymd.theming import ThemableBehavior + from kivymd.uix.behaviors import FakeRectangularElevationBehavior, RectangularRippleBehavior + from kivymd.uix.boxlayout import MDBoxLayout + + KV = ''' + MDFloatLayout: + + ElevatedWidget: + pos_hint: {'center_x': .5, 'center_y': .5} + size_hint: None, None + size: 100, 100 + md_bg_color: 0, 0, 1, 1 + ''' + + + class ElevatedWidget( + ThemableBehavior, + FakeRectangularElevationBehavior, + RectangularRippleBehavior, + ButtonBehavior, + MDBoxLayout, + ): + shadow_animation = ObjectProperty() + + def on_press(self, *args): + if self.shadow_animation: + Animation.cancel_all(self, "_elevation") + self.shadow_animation = Animation(_elevation=self.elevation + 10, d=0.4) + self.shadow_animation.start(self) + + def on_release(self, *args): + if self.shadow_animation: + Animation.cancel_all(self, "_elevation") + self.shadow_animation = Animation(_elevation=self.elevation, d=0.1) + self.shadow_animation.start(self) + + + 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-animation-effect.gif + :align: center + +Lighting position +----------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.card import MDCard + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.behaviors import RectangularElevationBehavior + + KV = ''' + MDScreen: + + ShadowCard: + pos_hint: {'center_x': .5, 'center_y': .5} + size_hint: None, None + size: 100, 100 + shadow_pos: -10 + slider.value, -10 + slider.value + elevation: 24 + md_bg_color: 1, 1, 1, 1 + + MDSlider: + id: slider + max: 20 + size_hint_x: .6 + pos_hint: {'center_x': .5, 'center_y': .3} + ''' + + + class ShadowCard(RectangularElevationBehavior, MDBoxLayout): + pass + + + class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-pos.gif + :align: center +""" + +__all__ = ( + "CommonElevationBehavior", + "RectangularElevationBehavior", + "CircularElevationBehavior", + "RoundedRectangularElevationBehavior", + "ObservableShadow", + "FakeRectangularElevationBehavior", + "FakeCircularElevationBehavior", +) + +from io import BytesIO +from weakref import WeakMethod, ref + +from kivy import Logger +from kivy.clock import Clock +from kivy.core.image import Image as CoreImage +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + BoundedNumericProperty, + ListProperty, + NumericProperty, + ObjectProperty, + ReferenceListProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.widget import Widget +from PIL import Image, ImageDraw, ImageFilter + +from kivymd.app import MDApp + +Builder.load_string( + """ +#:import InstructionGroup kivy.graphics.instructions.InstructionGroup + + + + canvas.before: + # SOFT SHADOW + PushMatrix + Rotate: + angle: self.angle + origin: self._shadow_origin + Color: + group: "soft_shadow" + rgba: root.soft_shadow_cl + Rectangle: + group: "soft_shadow" + texture: self._soft_shadow_texture + size: self.soft_shadow_size + pos: self.soft_shadow_pos + PopMatrix + + # HARD SHADOW + PushMatrix + Rotate: + angle: self.angle + origin: self.center + Color: + group: "hard_shadow" + rgba: root.hard_shadow_cl + Rectangle: + group: "hard_shadow" + texture: self.hard_shadow_texture + size: self.hard_shadow_size + pos: self.hard_shadow_pos + PopMatrix + Color: + group: "shadow" + a: 1 +""", + filename="CommonElevationBehavior.kv", +) + + +class CommonElevationBehavior(Widget): + """Common base class for rectangular and circular elevation behavior.""" + + elevation = BoundedNumericProperty(0, min=0, errorvalue=0) + """ + Elevation of the widget. + + .. note:: + Although, this value does not represent the current elevation of the + widget. :attr:`~CommonElevationBehavior._elevation` can be used to + animate the current elevation and come back using the + :attr:`~CommonElevationBehavior.elevation` property directly. + + For example: + + .. code-block:: python + + from kivy.lang import Builder + from kivy.uix.behaviors import ButtonBehavior + + from kivymd.app import MDApp + from kivymd.uix.behaviors import CircularElevationBehavior, CircularRippleBehavior + from kivymd.uix.boxlayout import MDBoxLayout + + KV = ''' + #:import Animation kivy.animation.Animation + + + + size_hint: [None, None] + elevation: 6 + animation_: None + md_bg_color: [1] * 4 + on_size: + self.radius = [self.height / 2] * 4 + on_press: + if self.animation_: \ + self.animation_.cancel(self); \ + self.animation_ = Animation(_elevation=self.elevation + 6, d=0.08); \ + self.animation_.start(self) + on_release: + if self.animation_: \ + self.animation_.cancel(self); \ + self.animation_ = Animation(_elevation = self.elevation, d=0.08); \ + self.animation_.start(self) + + MDFloatLayout: + + WidgetWithShadow: + size: [root.size[1] / 2] * 2 + pos_hint: {"center": [0.5, 0.5]} + ''' + + + class WidgetWithShadow( + CircularElevationBehavior, + CircularRippleBehavior, + ButtonBehavior, + MDBoxLayout, + ): + def __init__(self, **kwargs): + # always set the elevation before the super().__init__ call + # self.elevation = 6 + super().__init__(**kwargs) + + def on_size(self, *args): + self.radius = [self.size[0] / 2] + + + class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + + Example().run() + + :attr:`elevation` is an :class:`~kivy.properties.BoundedNumericProperty` + and defaults to `0`. + """ + + # Shadow rendering properties. + # Shadow rotation memory - SHARED ACROSS OTHER CLASSES. + angle = NumericProperty(0) + """ + Angle of rotation in degrees of the current shadow. + This value is shared across different widgets. + + .. note:: + This value will affect both, hard and soft shadows. + Each shadow has his own origin point that's computed every time the + elevation changes. + + .. warning:: + Do not add `PushMatrix` inside the canvas before and add `PopMatrix` + in the next layer, this will cause visual errors, because the stack + used will clip the push and pop matrix already inside the canvas.before + canvas layer. + + Incorrect: + + .. code-block:: kv + + + canvas.before: + PushMatrix + [...] + canvas: + PopMatrix + + Correct: + + .. code-block:: kv + + + canvas.before: + PushMatrix + [...] + PopMatrix + + + + :attr:`angle` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + radius = VariableListProperty([0]) + """ + Radius of the corners of the shadow. + This values represents each corner of the shadow, starting from `top-left` + corner and going clockwise. + + .. code-block:: python + + radius = [ + "top-left", + "top-right", + "bottom-right", + "bottom-left", + ] + + This value can be expanded thus allowing this settings to be valid: + + .. code-block:: python + + widget.radius=[0] # Translates to [0, 0, 0, 0] + widget.radius=[10, 3] # Translates to [10, 3, 10, 3] + widget.radius=[7.0, 8.7, 1.5, 3.0] # Translates to [7, 8, 1, 3] + + .. note:: + This value will affect both, hard and soft shadows. + This value only affects :class:`~RoundedRectangularElevationBehavior` + for now, but can be stored and used by custom shadow draw functions. + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + # Position of the shadow. + _shadow_origin_x = NumericProperty(0) + """ + Shadow origin `x` position for the rotation origin. + + Managed by `_shadow_origin`. + + :attr:`_shadow_origin_x` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + + .. note:: + This property is automatically processed. by _shadow_origin. + """ + + _shadow_origin_y = NumericProperty(0) + """ + Shadow origin y position for the rotation origin. + + Managed by :attr:`_shadow_origin`. + + :attr:`_shadow_origin_y` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + + .. note:: + This property is automatically processed. + """ + + _shadow_origin = ReferenceListProperty(_shadow_origin_x, _shadow_origin_y) + """ + Soft shadow rotation origin point. + + :attr:`_shadow_origin` is an :class:`~kivy.properties.ReferenceListProperty` + and defaults to `[0, 0]`. + + .. note:: + This property is automatically processed and relative to the canvas center. + """ + + _shadow_pos = ListProperty([0, 0]) # custom offset + """ + Soft shadow origin point. + + :attr:`_shadow_pos` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0]`. + + .. note:: + This property is automatically processed and relative to the widget's + canvas center. + """ + + shadow_pos = ListProperty([0, 0]) # bottom left corner + """ + Custom shadow origin point. If this property is set, :attr:`_shadow_pos` + will be ommited. + + This property allows users to fake light source. + + :attr:`shadow_pos` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0]`. + + .. note:: + this value overwrite the :attr:`_shadow_pos` processing. + """ + + # Shadow Group shared memory + __shadow_groups = {"global": []} + + shadow_group = StringProperty("global") + """ + Widget's shadow group. + By default every widget with a shadow is saved inside the memory + :attr:`__shadow_groups` as a weakref. This means that you can have multiple + light sources, one for every shadow group. + + To fake a light source use :attr:`force_shadow_pos`. + + :attr:`shadow_group` is an :class:`~kivy.properties.StringProperty` + and defaults to `"global"`. + """ + + _elevation = BoundedNumericProperty(0, min=0, errorvalue=0) + """ + Current elevation of the widget. + + .. warning:: + This property is the current elevation of the widget, do not + use this property directly, instead, use :class:`~CommonElevationBehavior` + elevation. + + :attr:`_elevation` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + # soft shadow + _soft_shadow_texture = ObjectProperty() + """ + Texture of the soft shadow texture for the canvas. + + :attr:`_soft_shadow_texture` is an :class:`~kivy.core.image.Image` + and defaults to `None`. + + .. note:: + This property is automatically processed. + """ + + soft_shadow_size = ListProperty([0, 0]) + """ + Size of the soft shadow texture over the canvas. + + :attr:`soft_shadow_size` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0]`. + + .. note:: + This property is automatically processed. + """ + + soft_shadow_pos = ListProperty([0, 0]) + """ + Position of the hard shadow texture over the canvas. + + :attr:`soft_shadow_pos` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0]`. + + .. note:: + This property is automatically processed. + """ + + soft_shadow_cl = ListProperty([0, 0, 0, 0.50]) + """ + Color of the soft shadow. + + :attr:`soft_shadow_cl` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0, 0, 0.15]`. + """ + + # hard shadow + hard_shadow_texture = ObjectProperty() + """ + Texture of the hard shadow texture for the canvas. + + :attr:`hard_shadow_texture` is an :class:`~kivy.core.image.Image` + and defaults to `None`. + + .. note:: + This property is automatically processed when elevation is changed. + """ + + hard_shadow_size = ListProperty([0, 0]) + """ + Size of the hard shadow texture over the canvas. + + :attr:`hard_shadow_size` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0]`. + + .. note:: + This property is automatically processed when elevation is changed. + """ + + hard_shadow_pos = ListProperty([0, 0]) + """ + Position of the hard shadow texture over the canvas. + + :attr:`hard_shadow_pos` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0]`. + + .. note:: + This property is automatically processed when elevation is changed. + """ + + hard_shadow_cl = ListProperty([0, 0, 0, 0.15]) + """ + Color of the hard shadow. + + .. note:: + :attr:`hard_shadow_cl` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0, 0, 0.15]`. + """ + + # Shared property for some calculations. + # This values are used to improve the gaussain blur and avoid that + # the blur goes outside the texture. + hard_shadow_offset = BoundedNumericProperty( + 2, min=0, errorhandler=lambda x: 0 if x < 0 else x + ) + """ + This value sets a special offset to the shadow canvas, this offset allows a + correct draw of the canvas size. allowing the effect to correctly blur the + image in the given space. + + :attr:`hard_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty` + and defaults to `2`. + """ + + soft_shadow_offset = BoundedNumericProperty( + 4, min=0, errorhandler=lambda x: 0 if x < 0 else x + ) + """ + This value sets a special offset to the shadow canvas, this offset allows a + correct draw of the canvas size. allowing the effect to correctly blur the + image in the given space. + + :attr:`soft_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty` + and defaults to `4`. + """ + + draw_shadow = ObjectProperty(None) + """ + This property controls the draw call of the context. + + This property is automatically set to :attr:`__draw_shadow__` inside the + `super().__init__ call.` unless the property is different of None. + + To set a different drawing instruction function, set this property before the + `super(),__init__` call inside the `__init__` definition of the new class. + + You can use the source for this classes as example of how to draw over + with the context: + + Real time shadows: + #. :class:`~RectangularElevationBehavior` + #. :class:`~CircularElevationBehavior` + #. :class:`~RoundedRectangularElevationBehavior` + #. :class:`~ObservableShadow` + + + Fake shadows (d`ont use this property): + #. :class:`~FakeRectangularElevationBehavior` + #. :class:`~FakeCircularElevationBehavior` + + :attr:`draw_shadow` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + + .. note:: If this property is left to `None` the + :class:`~CommonElevationBehavior` will set to a function that will + raise a `NotImplementedError` inside `super().__init__`. + + Follow the next example to set a new draw instruction for the class + inside `__init__`: + + .. code-block:: python + + class RoundedRectangularElevationBehavior(CommonElevationBehavior): + ''' + Shadow class for the RoundedRectangular shadow behavior. + Controls the size and position of the shadow. + ''' + + def __init__(self, **kwargs): + self._draw_shadow = WeakMethod(self.__draw_shadow__) + super().__init__(**kwargs) + + def __draw_shadow__(self, origin, end, context=None): + context.draw(...) + + Context is a `Pillow` `ImageDraw` class. For more information check the + [Pillow official documentation](https://github.com/python-pillow/Pillow/). + """ + + # All classes that uses a fake shadow shall set this value as `True` + # for performance. + _fake_elevation = BooleanProperty(False) + + def __init__(self, **kwargs): + if self.draw_shadow is None: + self.draw_shadow = WeakMethod(self.__draw_shadow__) + self.prev_shadow_group = None + im = BytesIO() + Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png") + im.seek(0) + # Setting a empty image as texture, improves performance. + self._soft_shadow_texture = self.hard_shadow_texture = CoreImage( + im, ext="png" + ).texture + Clock.schedule_once(self.shadow_preset, -1) + self.on_shadow_group(self, self.shadow_group) + + self.bind( + pos=self._update_shadow, + size=self._update_shadow, + radius=self._update_shadow, + ) + super().__init__(**kwargs) + + def on_shadow_group(self, instance, value): + """ + This function controls the shadow group of the widget. + Do not use Directly to change the group. instead, use the shadow_group + :attr:`property`. + """ + + groups = CommonElevationBehavior.__shadow_groups + if self.prev_shadow_group: + group = groups[self.prev_shadow_group] + for widget in group[:]: + if widget() is self: + group.remove(widget) + group = self.prev_shadow_group = self.shadow_group + if group not in groups: + groups[group] = [] + r = ref(self, CommonElevationBehavior._clear_shadow_groups) + groups[group].append(r) + + @staticmethod + def _clear_shadow_groups(wk): + # auto flush the element when the weak reference have been deleted + groups = CommonElevationBehavior.__shadow_groups + for group in list(groups.values()): + if not group: + break + if wk in group: + group.remove(wk) + break + + def force_shadow_pos(self, shadow_pos): + """ + This property forces the shadow position in every widget inside the + widget. The argument :attr:`shadow_pos` is expected as a + or . + """ + + if self.shadow_group is None: + return + group = CommonElevationBehavior.__shadow_groups[self.shadow_group] + for wk in group[:]: + widget = wk() + if widget is None: + group.remove(wk) + widget.shadow_pos = shadow_pos + del group + + def update_group_property(self, property_name, value): + """ + This functions allows to change properties of every widget inside the + shadow group. + """ + + if self.shadow_group is None: + return + group = CommonElevationBehavior.__shadow_groups[self.shadow_group] + for wk in group[:]: + widget = wk() + if widget is None: + group.remove(wk) + setattr(widget, property_name, value) + del group + + def shadow_preset(self, *args): + """ + This function is meant to set the default configuration of the + elevation. + + After a new instance is created, the elevation property will be launched + and thus this function will update the elevation if the KV lang have not + done it already. + + Works similar to an `__after_init__` call inside a widget. + """ + + from kivymd.uix.card import MDCard + + if self.elevation is None and not issubclass(self.__class__, MDCard): + self.elevation = 10 + if self._fake_elevation is False: + self._update_shadow(self, self.elevation) + self.bind( + pos=self._update_shadow, + size=self._update_shadow, + _elevation=self._update_shadow, + ) + + def on_elevation(self, instance, value): + """ + Elevation event that sets the current elevation value to `_elevation`. + """ + + if value is not None: + self._elevation = value + + def _set_soft_shadow_a(self, value): + value = 0 if value < 0 else (1 if value > 1 else value) + self.soft_shadow_cl[-1] = value + return True + + def _set_hard_shadow_a(self, value): + value = 0 if value < 0 else (1 if value > 1 else value) + self.hard_shadow_cl[-1] = value + return True + + def _get_soft_shadow_a(self): + return self.soft_shadow_cl[-1] + + def _get_hard_shadow_a(self): + return self.hard_shadow_cl[-1] + + _soft_shadow_a = AliasProperty( + _get_soft_shadow_a, _set_soft_shadow_a, bind=["soft_shadow_cl"] + ) + _hard_shadow_a = AliasProperty( + _get_hard_shadow_a, _set_hard_shadow_a, bind=["hard_shadow_cl"] + ) + + def on_disabled(self, instance, value): + """ + This function hides the shadow when the widget is disabled. + It sets the shadow to `0`. + """ + + if self.disabled is True: + self._elevation = 0 + else: + self._elevation = 0 if self.elevation is None else self.elevation + self._update_shadow(self, self._elevation) + try: + super().on_disabled(instance, value) + except Exception: + pass + + def _update_elevation(self, instance, value): + self._elevation = value + self._update_shadow(instance, value) + + def _update_shadow_pos(self, instance, value): + if self._elevation > 0: + self.hard_shadow_pos = [ + self.x - dp(self.hard_shadow_offset), # + self.shadow_pos[0], + self.y - dp(self.hard_shadow_offset), # + self.shadow_pos[1], + ] + if self.shadow_pos == [0, 0]: + self.soft_shadow_pos = [ + self.x + + self._shadow_pos[0] + - self._elevation + - dp(self.soft_shadow_offset), + self.y + + self._shadow_pos[1] + - self._elevation + - dp(self.soft_shadow_offset), + ] + else: + self.soft_shadow_pos = [ + self.x + + self.shadow_pos[0] + - self._elevation + - dp(self.soft_shadow_offset), + self.y + + self.shadow_pos[1] + - self._elevation + - dp(self.soft_shadow_offset), + ] + self._shadow_origin = [ + self.soft_shadow_pos[0] + self.soft_shadow_size[0] / 2, + self.soft_shadow_pos[1] + self.soft_shadow_size[1] / 2, + ] + + def on__shadow_pos(self, ins, val): + """ + Updates the shadow with the computed value. + + Call this function every time you need to force a shadow update. + """ + + self._update_shadow_pos(ins, val) + + def on_shadow_pos(self, ins, val): + """ + Updates the shadow with the fixed value. + + Call this function every time you need to force a shadow update. + """ + + self._update_shadow_pos(ins, val) + + def _update_shadow(self, instance, value): + self._update_shadow_pos(instance, value) + if self._elevation > 0 and self._fake_elevation is False: + # dynamic elevation position for the shadow + if self.shadow_pos == [0, 0]: + self._shadow_pos = [0, -self._elevation * 0.4] + + # HARD Shadow + offset = int(dp(self.hard_shadow_offset)) + size = [ + int(self.size[0] + (offset * 2)), + int(self.size[1] + (offset * 2)), + ] + im = BytesIO() + # context + img = Image.new("RGBA", tuple(size), color=(0, 0, 0, 0)) + # draw context + shadow = ImageDraw.Draw(img) + self.draw_shadow()( + [offset, offset], + [ + int(size[0] - 1 - offset), + int(size[1] - 1 - offset), + ], + context=shadow + # context=ref(shadow) + ) + img = img.filter( + ImageFilter.GaussianBlur( + radius=int(dp(1 + self.hard_shadow_offset / 3)) + ) + ) + img.save(im, format="png") + im.seek(0) + self.hard_shadow_size = size + self.hard_shadow_texture = CoreImage(im, ext="png").texture + + # soft shadow + if self.soft_shadow_cl[-1] > 0: + offset = dp(self.soft_shadow_offset) + size = [ + int(self.size[0] + dp(self._elevation * 2) + (offset * 2)), + int(self.size[1] + dp(self._elevation * 2) + (offset * 2)), + # ((self._elevation)*2) + x + (offset*2)) for x in self.size + ] + im = BytesIO() + img = Image.new("RGBA", tuple(size), color=((0,) * 4)) + shadow = ImageDraw.Draw(img) + _offset = int(dp(self._elevation + offset)) + self.draw_shadow()( + [ + _offset, + _offset, + ], + [int(size[0] - _offset - 1), int(size[1] - _offset - 1)], + context=shadow + # context=ref(shadow) + ) + img = img.filter( + ImageFilter.GaussianBlur(radius=self._elevation // 2) + ) + shadow = ImageDraw.Draw(img) + img.save(im, format="png") + im.seek(0) + self.soft_shadow_size = size + self._soft_shadow_texture = CoreImage(im, ext="png").texture + else: + im = BytesIO() + Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png") + im.seek(0) + self._soft_shadow_texture = self.hard_shadow_texture = CoreImage( + im, ext="png" + ).texture + return + + def _get_center(self): + center = [self.pos[0] + self.width / 2, self.pos[1] + self.height / 2] + return center + + def __draw_shadow__(self, origin, end, context=None): + Logger.warning( + f"KivyMD: " + f"If you see this error, this means that either youre using " + f"`CommonElevationBehavio`r directly or your 'shader' dont have a " + f"`_draw_shadow` instruction, remember to overwrite this function" + f"to draw over the image context. Тhe figure you would like. " + f"Or your class {self.__class__.__name__} is not inherited from " + f"any of the classes {__all__}" + ) + + +class RectangularElevationBehavior(CommonElevationBehavior): + """ + Base class for a rectangular elevation behavior. + """ + + def __init__(self, **kwargs): + self.draw_shadow = WeakMethod(self.__draw_shadow__) + super().__init__(**kwargs) + + def __draw_shadow__(self, origin, end, context=None): + context.rectangle(origin + end, fill=tuple([255] * 4)) + + +class CircularElevationBehavior(CommonElevationBehavior): + """ + Base class for a circular elevation behavior. + """ + + def __init__(self, **kwargs): + self.draw_shadow = WeakMethod(self.__draw_shadow__) + super().__init__(**kwargs) + + def __draw_shadow__(self, origin, end, context=None): + context.ellipse(origin + end, fill=tuple([255] * 4)) + + +class RoundedRectangularElevationBehavior(CommonElevationBehavior): + """ + Base class for rounded rectangular elevation behavior. + """ + + def __init__(self, **kwargs): + self.bind( + radius=self._update_shadow, + ) + self.draw_shadow = WeakMethod(self.__draw_shadow__) + super().__init__(**kwargs) + + def __draw_shadow__(self, origin, end, context=None): + if self.radius == [0, 0, 0, 0]: + context.rectangle(origin + end, fill=tuple([255] * 4)) + else: + radius = [x * 2 for x in self.radius] + context.pieslice( + [ + origin[0], + origin[1], + origin[0] + radius[0], + origin[1] + radius[0], + ], + 180, + 270, + fill=(255, 255, 255, 255), + ) + context.pieslice( + [ + end[0] - radius[1], + origin[1], + end[0], + origin[1] + radius[1], + ], + 270, + 360, + fill=(255, 255, 255, 255), + ) + context.pieslice( + [ + end[0] - radius[2], + end[1] - radius[2], + end[0], + end[1], + ], + 0, + 90, + fill=(255, 255, 255, 255), + ) + context.pieslice( + [ + origin[0], + end[1] - radius[3], + origin[0] + radius[3], + end[1], + ], + 90, + 180, + fill=(255, 255, 255, 255), + ) + if all((x == self.radius[0] for x in self.radius)): + radius = int(self.radius[0]) + context.rectangle( + [ + origin[0] + radius, + origin[1], + end[0] - radius, + end[1], + ], + fill=(255,) * 4, + ) + context.rectangle( + [ + origin[0], + origin[1] + radius, + end[0], + end[1] - radius, + ], + fill=(255,) * 4, + ) + else: + radius = [ + max((self.radius[0], self.radius[1])), + max((self.radius[1], self.radius[2])), + max((self.radius[2], self.radius[3])), + max((self.radius[3], self.radius[0])), + ] + context.rectangle( + [ + origin[0] + self.radius[0], + origin[1], + end[0] - self.radius[1], + end[1] - radius[2], + ], + fill=(255,) * 4, + ) + context.rectangle( + [ + origin[0] + radius[3], + origin[1] + self.radius[1], + end[0], + end[1] - self.radius[2], + ], + fill=(255,) * 4, + ) + context.rectangle( + [ + origin[0] + self.radius[3], + origin[1] + radius[0], + end[0] - self.radius[2], + end[1], + ], + fill=(255,) * 4, + ) + context.rectangle( + [ + origin[0], + origin[1] + self.radius[0], + end[0] - radius[2], + end[1] - self.radius[3], + ], + fill=(255,) * 4, + ) + + +class ObservableShadow(CommonElevationBehavior): + """ + ObservableShadow is real time shadow render that it's intended to only + render a partial shadow of widgets based upon on the window observable + area, this is meant to improve the performance of bigger widgets. + + .. warning:: + This is an empty class, the name has been reserved for future use. + if you include this clas in your object, you wil get a + `NotImplementedError`. + """ + + def __init__(self, **kwargs): + # self._shadow = MDApp.get_running_app().theme_cls.round_shadow + # self._fake_elevation=True + raise NotImplementedError( + "ObservableShadow:\n\t" "This class is in current development" + ) + super().__init__(**kwargs) + + +class FakeRectangularElevationBehavior(CommonElevationBehavior): + """ + `FakeRectangularElevationBehavio`r is a shadow mockup for widgets. Improves + performance using cached images inside `kivymd.images` dir + + This class cast a fake Rectangular shadow behaind the widget. + + You can either use this behavior to overwrite the elevation of a prefab + widget, or use it directly inside a new widget class definition. + + Use this class as follows for new widgets: + + .. code-block:: python + + class NewWidget( + ThemableBehavior, + FakeCircularElevationBehavior, + SpecificBackgroundColorBehavior, + # here you add the other front end classes for the widget front_end, + ): + [...] + + With this method each class can draw it's content in the canvas in the + correct order, avoiding some visual errors. + + `FakeCircularElevationBehavior` will load prefabricated textures to + optimize loading times. + + .. note:: About rounded corners: + be careful, since this behavior is a mockup and will not draw any + rounded corners. + """ + + def __init__(self, **kwargs): + # self._shadow = MDApp.get_running_app().theme_cls.round_shadow + self.draw_shadow = WeakMethod(self.__draw_shadow__) + self._fake_elevation = True + self._update_shadow(self, self.elevation) + super().__init__(**kwargs) + + 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. + center_x, center_y = self._get_center() + self.hard_shadow_pos = self.soft_shadow_pos = ( + center_x - soft_width / 2, + center_y - soft_height / 2 - dp(self._elevation * 0.5), + ) + # Set transparency + self._soft_shadow_a = 0.1 * 1.05**self._elevation + self._hard_shadow_a = 0.4 * 0.8**self._elevation + t = int(round(self._elevation)) + if 0 < t <= 23: + self._soft_shadow_texture = ( + self._hard_shadow_texture + ) = self._shadow.textures[str(t)] + else: + self._soft_shadow_texture = ( + self._hard_shadow_texture + ) = self._shadow.textures["23"] + else: + self._soft_shadow_a = 0 + self._hard_shadow_a = 0 + + def __draw_shadow__(self, origin, end, context=None): + pass + + +class FakeCircularElevationBehavior(CommonElevationBehavior): + """ + `FakeCircularElevationBehavior` is a shadow mockup for widgets. Improves + performance using cached images inside `kivymd.images` dir + + This class cast a fake elliptic shadow behaind the widget. + + You can either use this behavior to overwrite the elevation of a prefab + widget, or use it directly inside a new widget class definition. + + Use this class as follows for new widgets: + + .. code-block:: python + + class NewWidget( + ThemableBehavior, + FakeCircularElevationBehavior, + SpecificBackgroundColorBehavior, + # here you add the other front end classes for the widget front_end, + ): + [...] + + With this method each class can draw it's content in the canvas in the + correct order, avoiding some visual errors. + + `FakeCircularElevationBehavior` will load prefabricated textures to optimize + loading times. + + .. note:: About rounded corners: + be careful, since this behavior is a mockup and will not draw any rounded + corners. only perfect ellipses. + """ + + def __init__(self, **kwargs): + self._shadow = MDApp.get_running_app().theme_cls.round_shadow + self.draw_shadow = WeakMethod(self.__draw_shadow__) + self._fake_elevation = True + self._update_shadow(self, self.elevation) + super().__init__(**kwargs) + + def _update_shadow(self, *args): + if self._elevation > 0: + # set shadow size + width = self.width * 2 + height = self.height * 2 + center_x, center_y = self._get_center() + + x = center_x - width / 2 + self.soft_shadow_size = (width, height) + self.hard_shadow_size = (width, height) + # set ``soft_shadow`` parameters + y = center_y - height / 2 - dp(0.5 * self._elevation) + self.soft_shadow_pos = (x, y) + + # set ``hard_shadow`` parameters + y = center_y - height / 2 - dp(0.5 * self._elevation) + self.hard_shadow_pos = (x, y) + + # shadow transparency + self._soft_shadow_a = 0.1 * 1.05**self._elevation + self._hard_shadow_a = 0.4 * 0.8**self._elevation + t = int(round(self._elevation)) + if 0 < t <= 23: + if hasattr(self, "_shadow"): + self._soft_shadow_texture = ( + self._hard_shadow_texture + ) = self._shadow.textures[str(t)] + else: + self._soft_shadow_texture = ( + self._hard_shadow_texture + ) = self._shadow.textures["23"] + else: + self._soft_shadow_a = 0 + self._hard_shadow_a = 0 + + def __draw_shadow__(self, origin, end, context=None): + pass diff --git a/sbapp/kivymd/uix/behaviors/focus_behavior.py b/sbapp/kivymd/uix/behaviors/focus_behavior.py new file mode 100644 index 0000000..15d3169 --- /dev/null +++ b/sbapp/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, ColorProperty +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 = ColorProperty(None) + """ + The color of the widget when the mouse enters the bbox of the widget. + + :attr:`focus_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + unfocus_color = ColorProperty(None) + """ + The color of the widget when the mouse exits the bbox widget. + + :attr:`unfocus_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + 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/sbapp/kivymd/uix/behaviors/hover_behavior.py b/sbapp/kivymd/uix/behaviors/hover_behavior.py new file mode 100644 index 0000000..40f519e --- /dev/null +++ b/sbapp/kivymd/uix/behaviors/hover_behavior.py @@ -0,0 +1,235 @@ +""" +Behaviors/Hover +=============== + +.. rubric:: Changing when the mouse is on the widget and the widget is visible. + +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. + +.. note:: + + :class:`~HoverBehavior` will by default check to see if the current Widget is visible (i.e. not covered by a modal or popup and not a part of a Relative Layout, MDTab or Carousel that is not currently visible etc) and will only issue events if the widget is visible. + + To get the legacy behavior that the events are always triggered, you can set `detect_visible` on the Widget to `False`. + +.. 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.properties import BooleanProperty, ObjectProperty +from kivy.uix.widget import Widget + + +class HoverBehavior(object): + """ + :Events: + :attr:`on_enter` + Called when mouse enters the bbox of the widget AND the widget is visible + :attr:`on_leave` + Called when the mouse exits the widget AND the widget is visible + """ + + hovering = BooleanProperty(False) + """ + `True`, if the mouse cursor is within the borders of the widget. + + Note that this is set and cleared even if the widget is not visible + + :attr:`hover` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + hover_visible = BooleanProperty(False) + """ + `True` if hovering is True AND is the current widget is visible + + :attr:`hover_visible` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + enter_point = ObjectProperty(allownone=True) + """ + Holds the last position where the mouse pointer crossed into the Widget + if the Widget is visible and is currently in a hovering state + + :attr:`enter_point` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + detect_visible = BooleanProperty(True) + """ + Should this widget perform the visibility check? + + :attr:`detect_visible` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + def __init__(self, **kwargs): + self.register_event_type("on_enter") + self.register_event_type("on_leave") + Window.bind(mouse_pos=self.on_mouse_update) + super(HoverBehavior, self).__init__(**kwargs) + + def on_mouse_update(self, *args): + # If the Widget currently has no parent, do nothing + if not self.get_root_window(): + return + pos = args[1] + # + # is the pointer in the same position as the widget? + # If not - then issue an on_exit event if needed + # + if not self.collide_point(*self.to_widget(*pos)): + self.hovering = False + self.enter_point = None + if self.hover_visible: + self.hover_visible = False + self.dispatch("on_leave") + return + + # + # The pointer is in the same position as the widget + # + + if self.hovering: + # + # nothing to do here. Not - this does not handle the case where + # a popup comes over an existing hover event. + # This seems reasonable + # + return + + # + # Otherwise - set the hovering attribute + # + self.hovering = True + + # + # We need to traverse the tree to see if the Widget is visible + # + # This is a two stage process: + # - first go up the tree to the root Window. + # At each stage - check that the Widget is actually visible + # - Second - At the root Window check that there is not another branch + # covering the Widget + # + + self.hover_visible = True + if self.detect_visible: + widget: Widget = self + while True: + # Walk up the Widget tree from the target Widget + parent = widget.parent + try: + # See if the mouse point collides with the parent + # using both local and glabal coordinates to cover absoluet and relative layouts + pinside = parent.collide_point( + *parent.to_widget(*pos) + ) or parent.collide_point(*pos) + except Exception: + # The collide_point will error when you reach the root Window + break + if not pinside: + self.hover_visible = False + break + # Iterate upwards + widget = parent + + # + # parent = root window + # widget = first Widget on the current branch + # + + children = parent.children + for child in children: + # For each top level widget - check if is current branch + # If it is - then break. + # If not then - since we start at 0 - this widget is visible + # + # Check to see if it should take the hover + # + if child == widget: + # this means that the current widget is visible + break + if child.collide_point(*pos): + # this means that the current widget is covered by a modal or popup + self.hover_visible = False + break + if self.hover_visible: + self.enter_point = pos + self.dispatch("on_enter") + + def on_enter(self): + """Called when mouse enters the bbox of the widget AND the widget is visible.""" + + def on_leave(self): + """Called when the mouse exits the widget AND the widget is visible.""" diff --git a/sbapp/kivymd/uix/behaviors/magic_behavior.py b/sbapp/kivymd/uix/behaviors/magic_behavior.py new file mode 100644 index 0000000..9e7f862 --- /dev/null +++ b/sbapp/kivymd/uix/behaviors/magic_behavior.py @@ -0,0 +1,190 @@ +""" +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 kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + + + + MDFloatLayout: + + 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.lang import Builder +from kivy.properties import NumericProperty + +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: + + magic_speed = NumericProperty(1) + """ + Animation playback speed. + + :attr:`magic_speed` is a :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + def grow(self) -> None: + """Grow effect animation.""" + + ( + Animation( + scale_x=1.2, + scale_y=1.2, + t="out_quad", + d=0.03 / self.magic_speed, + ) + + Animation( + scale_x=1, scale_y=1, t="out_elastic", d=0.4 / self.magic_speed + ) + ).start(self) + + def shake(self) -> None: + """Shake effect animation.""" + + ( + Animation(translate_x=50, t="out_quad", d=0.02 / self.magic_speed) + + Animation( + translate_x=0, t="out_elastic", d=0.5 / self.magic_speed + ) + ).start(self) + + def wobble(self) -> None: + """Wobble effect animation.""" + + ( + ( + Animation(scale_y=0.7, t="out_quad", d=0.03 / self.magic_speed) + & Animation( + scale_x=1.4, t="out_quad", d=0.03 / self.magic_speed + ) + ) + + ( + Animation(scale_y=1, t="out_elastic", d=0.5 / self.magic_speed) + & Animation( + scale_x=1, t="out_elastic", d=0.4 / self.magic_speed + ) + ) + ).start(self) + + def twist(self) -> None: + """Twist effect animation.""" + + ( + Animation(rotate=25, t="out_quad", d=0.05 / self.magic_speed) + + Animation(rotate=0, t="out_elastic", d=0.5 / self.magic_speed) + ).start(self) + + def shrink(self) -> None: + """Shrink effect animation.""" + + Animation( + scale_x=0.95, scale_y=0.95, t="out_quad", d=0.1 / self.magic_speed + ).start(self) + + def on_touch_up(self, *args): + Animation.stop_all(self) + return super().on_touch_up(*args) diff --git a/sbapp/kivymd/uix/behaviors/ripple_behavior.py b/sbapp/kivymd/uix/behaviors/ripple_behavior.py new file mode 100755 index 0000000..f06e724 --- /dev/null +++ b/sbapp/kivymd/uix/behaviors/ripple_behavior.py @@ -0,0 +1,528 @@ +""" +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 = ''' + MDScreen: + + CircularRippleButton: + source: "data/logo/kivy-icon-256.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): + 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 = ''' + MDScreen: + + 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): + 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 typing import NoReturn + +from kivy.animation import Animation +from kivy.graphics import ( + Color, + Ellipse, + StencilPop, + StencilPush, + StencilUnUse, + StencilUse, +) +from kivy.graphics.vertex_instructions import RoundedRectangle +from kivy.properties import ( + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + StringProperty, +) +from kivy.uix.behaviors import ToggleButtonBehavior + + +class CommonRipple(object): + """Base class for ripple effect.""" + + ripple_rad_default = NumericProperty(1) + """ + The starting value of the radius of the ripple effect. + + .. code-block:: kv + + CircularRippleButton: + ripple_rad_default: 100 + + .. 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 = ColorProperty(None) + """ + Ripple color in (r, g, b, a) format. + + .. code-block:: kv + + CircularRippleButton: + ripple_color: app.theme_cls.primary_color + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-color.gif + :align: center + + :attr:`ripple_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + ripple_alpha = NumericProperty(0.5) + """ + Alpha channel values for ripple effect. + + .. code-block:: kv + + CircularRippleButton: + ripple_alpha: .9 + ripple_color: app.theme_cls.primary_color + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-alpha.gif + :align: center + + :attr:`ripple_alpha` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.5`. + """ + + ripple_scale = NumericProperty(None) + """ + Ripple effect scale. + + .. code-block:: kv + + CircularRippleButton: + ripple_scale: .5 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-scale-05.gif + :align: center + + .. code-block:: kv + + CircularRippleButton: + ripple_scale: 1 + + .. 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. + + .. code-block:: kv + + CircularRippleButton: + ripple_duration_in_fast: .1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-duration-in-fast.gif + :align: center + + :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. + + .. code-block:: kv + + CircularRippleButton: + ripple_duration_in_slow: 5 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-duration-in-slow.gif + :align: center + + :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. + + .. code-block:: kv + + CircularRippleButton: + ripple_duration_out: 5 + + .. 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_canvas_after = BooleanProperty(True) + """ + The ripple effect is drawn above/below the content. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDIconButton: + ripple_canvas_after: True + icon: "android" + ripple_alpha: .8 + ripple_color: app.theme_cls.primary_color + icon_size: "100sp" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-canvas-after-true.gif + :align: center + + .. code-block:: kv + + MDIconButton: + ripple_canvas_after: False + icon: "android" + ripple_alpha: .8 + ripple_color: app.theme_cls.primary_color + icon_size: "100sp" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ripple-canvas-after-false.gif + :align: center + + :attr:`ripple_canvas_after` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + 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_out` 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) + _round_rad = ListProperty([0, 0, 0, 0]) + + def lay_canvas_instructions(self) -> NoReturn: + raise NotImplementedError + + def start_ripple(self) -> None: + 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) -> None: + 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) -> None: + 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) -> None: + self._doing_ripple = False + self._finishing_ripple = False + self._fading_out = False + + if not self.ripple_canvas_after: + canvas = self.canvas.before + else: + canvas = self.canvas.after + + canvas.remove_group("circular_ripple_behavior") + canvas.remove_group("rectangular_ripple_behavior") + + def on_touch_down(self, touch): + # FIXME: in fact, the output of the super method is extra. + # But without this, the list (`ScrollView`) placed in the `MDCard` + # widget will not scroll. + super().on_touch_down(touch) + if touch.is_mouse_scrolling: + return False + if not self.collide_point(touch.x, touch.y): + return False + if not self.disabled: + self.call_ripple_animation_methods(touch) + # FIXME: this check is needed for the `MDTabsLabel` object. + # With the normal `return True`, events for tabs from the `MDTabs` + # class are not processed. + # There may be problems with other widgets. + # Status: requires check. + if isinstance(self, ToggleButtonBehavior): + return super().on_touch_down(touch) + else: + return True + + def call_ripple_animation_methods(self, touch) -> None: + 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() + + 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) -> None: + if self._no_ripple_effect: + return + + with self.canvas.after if self.ripple_canvas_after else self.canvas.before: + if hasattr(self, "radius"): + if isinstance(self.radius, (float, int)): + self.radius = [ + self.radius, + ] + self._round_rad = self.radius + StencilPush(group="rectangular_ripple_behavior") + RoundedRectangle( + pos=self.pos, + size=self.size, + radius=self._round_rad, + group="rectangular_ripple_behavior", + ) + StencilUse(group="rectangular_ripple_behavior") + self.col_instruction = Color( + rgba=self.ripple_color, group="rectangular_ripple_behavior" + ) + 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, + ), + group="rectangular_ripple_behavior", + ) + StencilUnUse(group="rectangular_ripple_behavior") + RoundedRectangle( + pos=self.pos, + size=self.size, + radius=self._round_rad, + group="rectangular_ripple_behavior", + ) + StencilPop(group="rectangular_ripple_behavior") + 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) -> None: + if self._no_ripple_effect: + return + + with self.canvas.after if self.ripple_canvas_after else self.canvas.before: + StencilPush(group="circular_ripple_behavior") + 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, + ), + group="circular_ripple_behavior", + ) + StencilUse(group="circular_ripple_behavior") + 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, + ), + group="circular_ripple_behavior", + ) + StencilUnUse(group="circular_ripple_behavior") + Ellipse( + pos=self.pos, size=self.size, group="circular_ripple_behavior" + ) + StencilPop(group="circular_ripple_behavior") + 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/sbapp/kivymd/uix/behaviors/toggle_behavior.py b/sbapp/kivymd/uix/behaviors/toggle_behavior.py new file mode 100644 index 0000000..054849c --- /dev/null +++ b/sbapp/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, ColorProperty +from kivy.uix.behaviors import ToggleButtonBehavior + +from kivymd.uix.button import ( + MDFillRoundFlatButton, + MDFillRoundFlatIconButton, + MDFlatButton, + MDRaisedButton, + MDRectangleFlatButton, + MDRectangleFlatIconButton, + MDRoundFlatButton, + MDRoundFlatIconButton, +) + + +class MDToggleButton(ToggleButtonBehavior): + background_normal = ColorProperty(None) + """ + Color of the button in ``rgba`` format for the 'normal' state. + + :attr:`background_normal` is a :class:`~kivy.properties.ColorProperty` + and is defaults to `None`. + """ + + background_down = ColorProperty(None) + """ + Color of the button in ``rgba`` format for the 'down' state. + + :attr:`background_down` is a :class:`~kivy.properties.ColorProperty` + and is defaults to `None`. + """ + + font_color_normal = ColorProperty(None) + """ + Color of the font's button in ``rgba`` format for the 'normal' state. + + :attr:`font_color_normal` is a :class:`~kivy.properties.ColorProperty` + and is defaults to `None`. + """ + + font_color_down = ColorProperty([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.ColorProperty` + 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/sbapp/kivymd/uix/behaviors/touch_behavior.py b/sbapp/kivymd/uix/behaviors/touch_behavior.py new file mode 100644 index 0000000..49b54fc --- /dev/null +++ b/sbapp/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 + + if touch.is_double_tap: + self.on_double_tap(touch, *args) + if touch.is_triple_tap: + self.on_triple_tap(touch, *args) + + def delete_clock(self, widget, touch, *args): + if self.collide_point(touch.x, touch.y): + try: + Clock.unschedule(touch.ud["event"]) + except KeyError: + pass + + 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/sbapp/kivymd/uix/bottomnavigation/__init__.py b/sbapp/kivymd/uix/bottomnavigation/__init__.py new file mode 100644 index 0000000..d62fda6 --- /dev/null +++ b/sbapp/kivymd/uix/bottomnavigation/__init__.py @@ -0,0 +1,2 @@ +# NOQA F401 +from .bottomnavigation import MDBottomNavigation, MDBottomNavigationItem diff --git a/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.kv b/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.kv new file mode 100644 index 0000000..46c29f5 --- /dev/null +++ b/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.kv @@ -0,0 +1,119 @@ +#:import sm kivy.uix.screenmanager +#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT + + + + orientation: "vertical" + height: + STANDARD_INCREMENT if app.theme_cls.material_style == "M2" else "80dp" + + ScreenManager: + id: tab_manager + transition: sm.FadeTransition(duration=.2) + on_current: + root.dispatch( \ + "on_switch_tabs", \ + root._get_switchig_tab(self.current), \ + self.current \ + ) + + MDBottomNavigationBar: + id: bottom_panel + size_hint_y: None + radius: root.radius + height: + STANDARD_INCREMENT \ + if app.theme_cls.material_style == "M2" else \ + "80dp" + md_bg_color: + root.theme_cls.bg_dark \ + if not root.panel_color \ + else root.panel_color + + MDBoxLayout: + id: tab_bar + pos_hint: {"center_x": .5, "center_y": .5} + size_hint: None, None + height: + STANDARD_INCREMENT \ + if app.theme_cls.material_style == "M2" else \ + "80dp" + + + + md_bg_color: root.panel_color + 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) + 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: + 0, "12dp", 0, "12dp" if app.theme_cls.material_style == "M2" else "16dp" + + RelativeLayout: + id: item_container + + MDIcon: + id: _label_icon + icon: root.tab.icon + height: self.height + badge_icon: root.tab.badge_icon + theme_text_color: "Custom" + text_color: root._text_color_normal + opposite_colors: root.opposite_colors + pos: [self.pos[0], self.pos[1]] + font_size: "24dp" + y: item_container.height - self.height + pos_hint: + {"center_x": .5, "center_y": .5} \ + if not root.panel.use_text else \ + {"center_x": .5, "top": 1} + on_icon: + if self.icon not in md_icons.keys(): \ + self.size_hint = (None, None); \ + self.width = self.font_size; \ + self.height = self.font_size + + canvas.before: + Color: + rgba: + ( \ + ( \ + app.theme_cls.disabled_hint_text_color \ + if not root.selected_color_background else \ + root.selected_color_background \ + ) \ + if root.active else \ + (0, 0, 0, 0) \ + ) \ + if app.theme_cls.material_style == "M3" else \ + (0, 0, 0, 0) + RoundedRectangle: + radius: [16,] + size: root._selected_region_width, dp(32) + pos: + self.center_x - self.width - dp(8), \ + self.center_y - (dp(16)) + + MDLabel: + id: _label + text: root.tab.text + size_hint_x: None + text_size: None, root.height + adaptive_height: True + theme_text_color: "Custom" + text_color: root._text_color_normal + opposite_colors: root.opposite_colors + font_size: root._label_font_size + pos_hint: {"center_x": .5} + y: -dp(4) if app.theme_cls.material_style == "M2" else 0 + font_style: + "Button" if app.theme_cls.material_style == "M2" else "Body2" + + + + md_bg_color: root.theme_cls.bg_normal diff --git a/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py b/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py new file mode 100755 index 0000000..a3a77bc --- /dev/null +++ b/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py @@ -0,0 +1,766 @@ +""" +Components/BottomNavigation +=========================== + +.. seealso:: + + `Material Design 2 spec, Bottom navigation `_ and + `Material Design 3 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 kivy.lang import Builder + + from kivymd.app import MDApp + + + class Test(MDApp): + + def build(self): + self.theme_cls.material_style = "M3" + return Builder.load_string( + ''' + MDScreen: + + MDBottomNavigation: + panel_color: "#eeeaea" + selected_color_background: "#97ecf8" + text_color_active: 0, 0, 0, 1 + + MDBottomNavigationItem: + name: 'screen 1' + text: 'Mail' + icon: 'gmail' + badge_icon: "numeric-10" + + MDLabel: + text: 'Mail' + halign: 'center' + + MDBottomNavigationItem: + name: 'screen 2' + text: 'Discord' + icon: 'discord' + badge_icon: "numeric-5" + + MDLabel: + text: 'Discord' + halign: 'center' + + MDBottomNavigationItem: + name: 'screen 3' + text: 'LinkedIN' + icon: 'linkedin' + + MDLabel: + text: 'LinkedIN' + 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", + ) + +.. 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. + +Use custom icon +--------------- + +.. code-block:: kv + + MDBottomNavigation: + + MDBottomNavigationItem: + icon: "icon.png" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-custom-icon.png + :align: center +""" + +__all__ = ( + "TabbedPanelBase", + "MDBottomNavigationItem", + "MDBottomNavigation", + "MDTab", +) + +import os +from typing import Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.core.window.window_sdl2 import WindowSDL +from kivy.lang import Builder +from kivy.metrics import dp, sp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + StringProperty, +) +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.screenmanager import ScreenManagerException + +from kivymd import uix_path +from kivymd.material_resources import STANDARD_INCREMENT +from kivymd.theming import ThemableBehavior, ThemeManager +from kivymd.uix.anchorlayout import MDAnchorLayout +from kivymd.uix.behaviors import FakeRectangularElevationBehavior +from kivymd.uix.behaviors.backgroundcolor_behavior import ( + SpecificBackgroundColorBehavior, +) +from kivymd.uix.floatlayout import MDFloatLayout +from kivymd.uix.screen import MDScreen +from kivymd.utils.set_bars_colors import set_bars_colors + +with open( + os.path.join(uix_path, "bottomnavigation", "bottomnavigation.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDBottomNavigationHeader( + ThemableBehavior, ButtonBehavior, MDAnchorLayout +): + panel_color = ColorProperty([1, 1, 1, 0]) + """ + Panel color of bottom navigation. + + :attr:`panel_color` is an :class:`~kivy.properties.ColorProperty` + 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 = ColorProperty([1, 1, 1, 1]) + """ + Text color of the label when it is not selected. + + :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[1, 1, 1, 1]`. + """ + + text_color_active = ColorProperty([1, 1, 1, 1]) + """ + Text color of the label when it is selected. + + :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[1, 1, 1, 1]`. + """ + + selected_color_background = ColorProperty(None) + """ + The background color of the highlighted item when using Material Design v3. + + .. versionadded:: 1.0.0 + + :attr:`selected_color_background` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + opposite_colors = BooleanProperty(True) + + _label = ObjectProperty() + _label_font_size = NumericProperty("12sp") + _text_color_normal = ColorProperty([1, 1, 1, 1]) + _text_color_active = ColorProperty([1, 1, 1, 1]) + _selected_region_width = NumericProperty(dp(64)) + + def __init__(self, panel, tab): + self.panel = panel + 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) -> None: + """Called when clicking on a panel item.""" + + if self.theme_cls.material_style == "M2": + Animation(_label_font_size=sp(14), d=0.1).start(self) + elif self.theme_cls.material_style == "M3": + Animation( + _selected_region_width=dp(64), + t="in_out_sine", + d=0, + ).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_theme_manager: ThemeManager, color: list + ): + """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(MDScreen, 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'`. + """ + + badge_icon = StringProperty() + """ + Tab header badge icon. + + .. versionadded:: 1.0.0 + + :attr:`badge_icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + 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) -> None: + """Called when clicking on a panel item.""" + + bottom_navigation_object = self.parent_widget + bottom_navigation_header_object = ( + bottom_navigation_object.previous_tab.header + ) + bottom_navigation_object.ids.tab_manager.current = self.name + + if bottom_navigation_object.previous_tab is not self: + if bottom_navigation_object.use_text: + Animation(_label_font_size=sp(12), d=0.1).start( + bottom_navigation_object.previous_tab.header + ) + Animation( + _selected_region_width=0, + t="in_out_sine", + d=0, + ).start(bottom_navigation_header_object) + Animation( + _text_color_normal=bottom_navigation_header_object.text_color_normal + if bottom_navigation_object.previous_tab.header.text_color_normal + != [1, 1, 1, 1] + else self.theme_cls.disabled_hint_text_color, + d=0.1, + ).start(bottom_navigation_object.previous_tab.header) + bottom_navigation_object.previous_tab.header.active = False + self.header.active = True + bottom_navigation_object.previous_tab = self + + def on_disabled( + self, instance_bottom_navigation_item, disabled_value: bool + ) -> None: + self.header.disabled = disabled_value + + 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 = ColorProperty(None) + """ + Panel color of bottom navigation. + + :attr:`panel_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + tabs = ListProperty() + + +class MDBottomNavigation(TabbedPanelBase): + """ + A bottom navigation that is implemented by delegating all items to a + :class:`~kivy.uix.screenmanager.ScreenManager`. + + :Events: + :attr:`on_switch_tabs` + Called when switching tabs. Returns the object of the tab to be + opened. + + .. versionadded:: 1.0.0 + """ + + text_color_normal = ColorProperty([1, 1, 1, 1]) + """ + Text color of the label when it is not selected. + + .. 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 + + :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[1, 1, 1, 1]`. + """ + + text_color_active = ColorProperty([1, 1, 1, 1]) + """ + Text color of the label when it is selected. + + .. code-block:: kv + + MDBottomNavigation: + text_color_active: 0, 0, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-text_color_active.png + + :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[1, 1, 1, 1]`. + """ + + use_text = BooleanProperty(True) + """ + Use text for :class:`~MDBottomNavigationItem` or not. + If ``True``, the :class:`~MDBottomNavigation` panel height will be reduced + by the text height. + + .. versionadded:: 1.0.0 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-use-text.png + :align: center + + :attr:`use_text` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + selected_color_background = ColorProperty(None) + """ + The background color of the highlighted item when using Material Design v3. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDBottomNavigation: + selected_color_background: 0, 0, 1, .4 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation=selected-color-background.png + + :attr:`selected_color_background` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + font_name = StringProperty("Roboto") + """ + Font name of the label. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDBottomNavigation: + font_name: "path/to/font.ttf" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-navigation-font-name.png + + :attr:`font_name` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Roboto'`. + """ + + 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`. + """ + + set_bars_color = BooleanProperty(False) + """ + If `True` the background color of the navigation bar will be set + automatically according to the current color of the toolbar. + + .. versionadded:: 1.0.0 + + :attr:`set_bars_color` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + # Text active color if it is selected. + _active_color = ColorProperty([1, 1, 1, 1]) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_switch_tabs") + self.previous_tab = None + self.widget_index = 0 + self.theme_cls.bind(material_style=self.refresh_tabs) + Window.bind(on_resize=self.on_resize) + Clock.schedule_once(lambda x: self.on_resize()) + Clock.schedule_once(self.set_status_bar_color) + + def set_status_bar_color(self, interval: Union[int, float]) -> None: + if self.set_bars_color: + set_bars_colors(self.panel_color, None, self.theme_cls.theme_style) + + def switch_tab(self, name_tab) -> None: + """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, *args) -> None: + """Refresh all tabs.""" + + if self.ids: + tab_bar = self.ids.tab_bar + tab_bar.clear_widgets() + tab_manager = self.ids.tab_manager + self._active_color = self.theme_cls.primary_color + + if self.text_color_active != [1, 1, 1, 1]: + self._active_color = self.text_color_active + + for tab in tab_manager.screens: + self.tab_header = MDBottomNavigationHeader(tab=tab, panel=self) + 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._active_color + self.tab_header._label_font_size = sp(14) + self.tab_header.active = True + else: + self.tab_header.ids._label.font_size = sp(12) + self.tab_header._label_font_size = sp(12) + + def on_font_name(self, instance_bottom_navigation, font_name: str) -> None: + for tab in self.ids.tab_bar.children: + tab.ids._label.font_name = font_name + + def on_selected_color_background( + self, instance_bottom_navigation, color: list + ) -> None: + for tab in self.ids.tab_bar.children: + tab.selected_color_background = color + + def on_use_text( + self, instance_bottom_navigation, use_text_value: bool + ) -> None: + if not use_text_value: + for instance_bottom_navigation_header in self.ids.tab_bar.children: + instance_bottom_navigation_header.ids.item_container.remove_widget( + instance_bottom_navigation_header.ids._label + ) + if self.theme_cls.material_style == "M2": + height = dp(42) + else: + height = dp(80) + self.height = height + self.ids.bottom_panel.height = height + self.ids.tab_bar.height = height + else: + if self.theme_cls.material_style == "M2": + height = STANDARD_INCREMENT + else: + height = dp(80) + self.height = height + self.ids.bottom_panel.height = height + self.ids.tab_bar.height = height + + def on_text_color_normal( + self, instance_bottom_navigation, color: list + ) -> None: + MDBottomNavigationHeader.text_color_normal = color + for tab in self.ids.tab_bar.children: + if not tab.active: + tab._text_color_normal = color + + def on_text_color_active( + self, instance_bottom_navigation, color: list + ) -> None: + MDBottomNavigationHeader.text_color_active = color + self.text_color_active = color + for tab in self.ids.tab_bar.children: + tab.text_color_active = color + if tab.active: + tab._text_color_normal = color + + def on_switch_tabs(self, bottom_navigation_item, name_tab: str) -> None: + """ + Called when switching tabs. Returns the object of the tab to be opened. + """ + + def on_size(self, *args) -> None: + self.on_resize() + + def on_resize( + self, + instance: Union[WindowSDL, None] = None, + width: Union[int, None] = None, + do_again: bool = True, + ) -> None: + """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) + + def _get_switchig_tab(self, name_tab: str) -> MDBottomNavigationItem: + bottom_navigation_item = None + for bottom_navigation_header_instance in self.ids.tab_bar.children: + if bottom_navigation_header_instance.tab.name == name_tab: + bottom_navigation_item = bottom_navigation_header_instance.tab + break + return bottom_navigation_item + + +class MDBottomNavigationBar( + ThemableBehavior, + FakeRectangularElevationBehavior, + MDFloatLayout, +): + pass diff --git a/sbapp/kivymd/uix/bottomsheet/__init__.py b/sbapp/kivymd/uix/bottomsheet/__init__.py new file mode 100644 index 0000000..439811d --- /dev/null +++ b/sbapp/kivymd/uix/bottomsheet/__init__.py @@ -0,0 +1,8 @@ +# NOQA F401 +from .bottomsheet import ( + GridBottomSheetItem, + MDBottomSheet, + MDCustomBottomSheet, + MDGridBottomSheet, + MDListBottomSheet, +) diff --git a/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv b/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv new file mode 100644 index 0000000..a840629 --- /dev/null +++ b/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv @@ -0,0 +1,73 @@ +#: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) + ] + + + + theme_text_color: "Primary" + pos_hint: {"center_x": .5, "center_y": .5} + + + + orientation: "vertical" + padding: 0, dp(24), 0, 0 + size_hint_y: None + size: dp(64), dp(96) + + AnchorLayout: + anchor_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" diff --git a/sbapp/kivymd/uix/bottomsheet/bottomsheet.py b/sbapp/kivymd/uix/bottomsheet/bottomsheet.py new file mode 100755 index 0000000..bf7e98f --- /dev/null +++ b/sbapp/kivymd/uix/bottomsheet/bottomsheet.py @@ -0,0 +1,509 @@ +""" +Components/BottomSheet +====================== + +.. 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 = ''' + MDScreen: + + MDTopAppBar: + 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 = ''' + MDScreen: + + MDTopAppBar: + 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" + + MDTopAppBar: + title: 'Custom bottom sheet:' + + ScrollView: + + MDGridLayout: + cols: 1 + adaptive_height: True + + ItemForCustomBottomSheet: + icon: "page-previous" + text: "Preview" + + ItemForCustomBottomSheet: + icon: "exit-to-app" + text: "Exit" + + + MDScreen: + + MDTopAppBar: + 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", +) + +import os + +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, + ColorProperty, + 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, uix_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 + +with open( + os.path.join(uix_path, "bottomsheet", "bottomsheet.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +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`. + """ + + duration_closing = NumericProperty(0.15) + """ + The duration of the bottom sheet dialog closing animation. + + :attr:`duration_closing` 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) + """ + Whether to use animation for opening and closing of the bottomsheet or not. + + :attr:`animation` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + bg_color = ColorProperty(None) + """ + Dialog background color in ``rgba`` format. + + :attr:`bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[]`. + """ + + value_transparent = ColorProperty([0, 0, 0, 0.8]) + """ + Background transparency value when opening a dialog. + + :attr:`value_transparent` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0.8]`. + """ + + _upper_padding = ObjectProperty() + _gl_content = ObjectProperty() + _position_content = NumericProperty() + + def open(self, *args): + super().open(*args) + + def add_widget(self, widget, index=0, canvas=None): + super().add_widget(widget, index, canvas) + + def dismiss(self, *args, **kwargs): + def dismiss(*args): + self.dispatch("on_pre_dismiss") + self._gl_content.clear_widgets() + self._real_remove_widget() + self.dispatch("on_dismiss") + + if self.animation: + a = Animation(height=0, d=self.duration_closing) + a.bind(on_complete=dismiss) + a.start(self._gl_content) + else: + dismiss() + + 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 + + +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) + + +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 = NumericProperty("24sp") + """ + Icon size. + + :attr:`caption` is an :class:`~kivy.properties.StringProperty` + and defaults to `'24sp'`. + """ + + +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/sbapp/kivymd/uix/boxlayout.py b/sbapp/kivymd/uix/boxlayout.py new file mode 100644 index 0000000..b5e0a52 --- /dev/null +++ b/sbapp/kivymd/uix/boxlayout.py @@ -0,0 +1,94 @@ +""" +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 +""" + +__all__ = ("MDBoxLayout",) + +from kivy.uix.boxlayout import BoxLayout + +from kivymd.uix import MDAdaptiveWidget + + +class MDBoxLayout(BoxLayout, MDAdaptiveWidget): + pass diff --git a/sbapp/kivymd/uix/button/__init__.py b/sbapp/kivymd/uix/button/__init__.py new file mode 100644 index 0000000..dddf186 --- /dev/null +++ b/sbapp/kivymd/uix/button/__init__.py @@ -0,0 +1,16 @@ +# NOQA F401 +from .button import ( + BaseButton, + MDFillRoundFlatButton, + MDFillRoundFlatIconButton, + MDFlatButton, + MDFloatingActionButton, + MDFloatingActionButtonSpeedDial, + MDIconButton, + MDRaisedButton, + MDRectangleFlatButton, + MDRectangleFlatIconButton, + MDRoundFlatButton, + MDRoundFlatIconButton, + MDTextButton, +) diff --git a/sbapp/kivymd/uix/button/button.kv b/sbapp/kivymd/uix/button/button.kv new file mode 100644 index 0000000..c7523a4 --- /dev/null +++ b/sbapp/kivymd/uix/button/button.kv @@ -0,0 +1,240 @@ + + canvas: + Clear + Color: + rgba: + (self._md_bg_color or [0.0, 0.0, 0.0, 0.0]) \ + if not self.disabled else \ + (self._md_bg_color_disabled or [0.0, 0.0, 0.0, 0.0]) + RoundedRectangle: + size: self.size + pos: self.pos + source: self.source if hasattr(self, "source") else "" + radius: [root._radius, ] + Color: + rgba: + root._line_color or [0.0, 0.0, 0.0, 0.0] \ + if not root.disabled else \ + ( \ + root._line_color_disabled \ + or self._disabled_color \ + or [0.0, 0.0, 0.0, 0.0] \ + ) + 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) + + size_hint: None, None + anchor_x: root.halign + anchor_y: root.valign + _round_rad: [self._radius] * 4 + + + + + lbl_txt: lbl_txt + width: + max(root._min_width, \ + root.padding[0] + lbl_txt.texture_size[0] + root.padding[2]) + size_hint_min_x: + max(root._min_width, \ + root.padding[0] + lbl_txt.texture_size[0] + root.padding[2]) + height: + max(root._min_height, \ + root.padding[1] + lbl_txt.texture_size[1] + root.padding[3]) + size_hint_min_y: + max(root._min_height, \ + root.padding[1] + lbl_txt.texture_size[1] + root.padding[3]) + + MDLabel: + id: lbl_txt + text: root.text + font_size: root.font_size + font_style: root.font_style + halign: 'center' + valign: 'middle' + adaptive_size: True + -text_size: None, None + theme_text_color: root._theme_text_color + text_color: root._text_color + markup: True + disabled: root.disabled + opposite_colors: root.opposite_colors + font_name: root.font_name if root.font_name else self.font_name + + + + lbl_ic: lbl_ic + size: "48dp", "48dp" + padding: "12dp" if root.icon in md_icons else (0, 0, 0, 0) + # Backwards compatibility. + theme_icon_color: root.theme_icon_color or root.theme_text_color + + MDIcon: + id: lbl_ic + icon: root.icon + font_size: root.icon_size if root.icon_size else self.font_size + font_name: root.font_name if root.font_name else self.font_name + opposite_colors: root.opposite_colors + text_color: + # FIXME: ValueError: None is not allowed for MDIcon.text_color. + # This is only a temporary fix and does not fix the cause of the error. + (root._icon_color if root._icon_color else root.theme_cls.text_color) \ + if not root.disabled else \ + root.theme_cls.disabled_hint_text_color + on_icon: + if self.icon not in md_icons.keys(): self.size_hint = (1, 1) + theme_text_color: root._theme_icon_color + + + + lbl_txt: lbl_txt + lbl_ic: lbl_ic + + width: + max( \ + root._min_width, \ + root.padding[0] \ + + lbl_ic.texture_size[0] \ + + box.spacing \ + + lbl_txt.texture_size[0] \ + + root.padding[2] \ + ) + size_hint_min_x: + max( \ + root._min_width, \ + root.padding[0] \ + + lbl_ic.texture_size[0] \ + + box.spacing \ + + lbl_txt.texture_size[0] \ + + root.padding[2] \ + ) + height: + max( \ + root._min_height, \ + root.padding[1] \ + + max(lbl_ic.texture_size[1], lbl_txt.texture_size[1]) \ + + root.padding[3] \ + ) + size_hint_min_y: + max( \ + root._min_height, \ + root.padding[1] \ + + max(lbl_ic.texture_size[1], lbl_txt.texture_size[1]) \ + + root.padding[3] \ + ) + + MDBoxLayout: + id: box + adaptive_size: True + padding: 0 + spacing: "4dp" + + MDIcon: + id: lbl_ic + size_hint_x: None + pos_hint: {"center_y": .5} + icon: root.icon + opposite_colors: root.opposite_colors + font_size: + root.icon_size \ + if root.icon_size else \ + (18 / 14 * lbl_txt.font_size) + text_color: + root._icon_color \ + if not root.disabled else \ + root.theme_cls.disabled_hint_text_color + theme_text_color: root._theme_icon_color + + MDLabel: + id: lbl_txt + adaptive_size: True + -text_size: None, None + pos_hint: {"center_y": .5} + halign: 'center' + valign: 'middle' + text: root.text + font_size: root.font_size + font_style: root.font_style + 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 + markup: True + disabled: root.disabled + opposite_colors: root.opposite_colors + + + + adaptive_size: True + color: root.theme_cls.primary_color if not root.color else root.color + opacity: 1 + + + + 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 + + +# FIXME: Use :class:`~kivymd.uix.boxlayout.MDBoxLayout` instead +# :class:`~kivy.uix.boxlayout.BoxLayout`. + + 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 + + # TODO: Use `md_bg_color` and `radius` instead `canvasю + 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 diff --git a/sbapp/kivymd/uix/button/button.py b/sbapp/kivymd/uix/button/button.py new file mode 100755 index 0000000..ddfa81e --- /dev/null +++ b/sbapp/kivymd/uix/button/button.py @@ -0,0 +1,1982 @@ +""" +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 + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + 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.icon_size` attribute to resize the button: + +.. code-block:: kv + + MDIconButton: + icon: "android" + icon_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`, substituting ``theme_icon_color`` for +``theme_text_color`` and ``icon_color`` for ``text_color``. +The use of ``user_font_size``, ``text_color`` and ``theme_text_color`` for +:class:`~MDIconButton` is deprecated. + +.. code-block:: kv + + MDIconButton: + icon: "android" + theme_icon_color: "Custom" + icon_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 + +Material design style 3 +----------------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.button import MDFloatingActionButton + + KV = ''' + MDScreen: + md_bg_color: "#f7f2fa" + + MDBoxLayout: + id: box + spacing: "56dp" + adaptive_size: True + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class TestNavigationDrawer(MDApp): + def build(self): + self.theme_cls.material_style = "M3" + return Builder.load_string(KV) + + def on_start(self): + data = { + "standard": {"md_bg_color": "#fefbff", "text_color": "#6851a5"}, + "small": {"md_bg_color": "#e9dff7", "text_color": "#211c29"}, + "large": {"md_bg_color": "#f8d7e3", "text_color": "#311021"}, + } + for type_button in data.keys(): + self.root.ids.box.add_widget( + MDFloatingActionButton( + icon="pencil", + type=type_button, + theme_icon_color="Custom", + md_bg_color=data[type_button]["md_bg_color"], + icon_color=data[type_button]["text_color"], + ) + ) + + + TestNavigationDrawer().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-floating-action-button-m3.gif + :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" + theme_text_color: "Custom" + 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]" + +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" + +.. 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 + +.. code-block:: kv + + MDRectangleFlatButton: + text: "MDRECTANGLEFLATBUTTON" + theme_text_color: "Custom" + text_color: 1, 0, 0, 1 + line_color: 0, 0, 1, 1 + +.. 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:`~MDRectangleFlatIconButton` are the same as +button :class:`~MDRectangleFlatButton`, with the addition of the +``theme_icon_color`` and ``icon_color`` parameters as for :class:`~MDIconButton`. + +.. code-block:: kv + + MDRectangleFlatIconButton: + icon: "android" + text: "MDRECTANGLEFLATICONBUTTON" + theme_text_color: "Custom" + text_color: 0, 0, 1, 1 + line_color: 1, 0, 1, 1 + theme_icon_color: "Custom" + icon_color: 1, 0, 0, 1 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-rectangle-flat-icon-button-custom.png + :align: center + +Without border +-------------- + +.. code-block:: python + + from kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + from kivymd.uix.button import MDRectangleFlatIconButton + + + class Example(MDApp): + def build(self): + screen = MDScreen() + 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 +----------------- + +.. 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`, with the addition of the +``theme_icon_color`` and ``icon_color`` parameters as for :class:`~MDIconButton`: + +.. 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`, with the addition of the +``theme_icon_color`` and ``icon_color`` parameters as for :class:`~MDIconButton`. + +.. 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 = ''' + MDScreen: + + MDFloatingActionButtonSpeedDial: + data: app.data + root_button_anim: True + ''' + + + class Example(MDApp): + data = { + 'Python': 'language-python', + 'PHP': 'language-php', + 'C++': 'language-cpp', + } + + 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 kivymd.uix.screen import MDScreen + from kivymd.app import MDApp + from kivymd.uix.button import MDFloatingActionButtonSpeedDial + + + class Example(MDApp): + data = { + 'Python': 'language-python', + 'PHP': 'language-php', + 'C++': 'language-cpp', + } + + def build(self): + screen = MDScreen() + speed_dial = MDFloatingActionButtonSpeedDial() + speed_dial.data = self.data + speed_dial.root_button_anim = 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__ = ( + "BaseButton", + "MDIconButton", + "MDFloatingActionButton", + "MDFlatButton", + "MDRaisedButton", + "MDRectangleFlatButton", + "MDRectangleFlatIconButton", + "MDRoundFlatButton", + "MDRoundFlatIconButton", + "MDFillRoundFlatButton", + "MDFillRoundFlatIconButton", + "MDTextButton", + "MDFloatingActionButtonSpeedDial", +) + +import os +from typing import Union + +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, sp +from kivy.properties import ( + BooleanProperty, + BoundedNumericProperty, + ColorProperty, + DictProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.behaviors import ButtonBehavior +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout + +from kivymd import uix_path +from kivymd.color_definitions import text_colors +from kivymd.font_definitions import theme_font_styles +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + CommonElevationBehavior, + FakeRectangularElevationBehavior, + RectangularRippleBehavior, + RoundedRectangularElevationBehavior, +) +from kivymd.uix.label import MDLabel +from kivymd.uix.tooltip import MDTooltip + +with open( + os.path.join(uix_path, "button", "button.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +theme_text_color_options = ( + "Primary", + "Secondary", + "Hint", + "Error", + "Custom", + "ContrastParentBackground", +) + + +class BaseButton( + RectangularRippleBehavior, ThemableBehavior, ButtonBehavior, AnchorLayout +): + """Base class for all buttons.""" + + padding = VariableListProperty([dp(16), dp(8), dp(16), dp(8)]) + """ + Padding between the widget box and its children, in pixels: + [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]. + + .. versionadded:: 1.0.0 + + :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` + and defaults to [16dp, 8dp, 16dp, 8dp]. + """ + + halign = OptionProperty("center", options=("left", "center", "right")) + """ + Horizontal anchor. + + .. versionadded:: 1.0.0 + + :attr:`anchor_x` is an :class:`~kivy.properties.OptionProperty` + and defaults to 'center'. It accepts values of 'left', 'center' or 'right'. + """ + + valign = OptionProperty("center", options=("top", "center", "bottom")) + """ + Vertical anchor. + + .. versionadded:: 1.0.0 + + :attr:`anchor_y` is an :class:`~kivy.properties.OptionProperty` + and defaults to 'center'. It accepts values of 'top', 'center' or 'bottom'. + """ + + text = StringProperty("") + """ + Button text. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon = StringProperty("") + """ + Button icon. + + :attr:`icon` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + font_style = OptionProperty("Body1", options=theme_font_styles) + """ + Button text 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 a :class:`~kivy.properties.StringProperty` + and defaults to `'Body1'`. + """ + + theme_text_color = OptionProperty(None, options=theme_text_color_options) + """ + 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 `None` (set by button class). + """ + + theme_icon_color = OptionProperty(None, options=theme_text_color_options) + """ + Button icon type. Available options are: (`"Primary"`, `"Secondary"`, + `"Hint"`, `"Error"`, `"Custom"`, `"ContrastParentBackground"`). + + .. versionadded:: 1.0.0 + + :attr:`theme_icon_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `None` (set by button subclass). + """ + + text_color = ColorProperty(None) + """ + Button text color in (r, g, b, a) format. + + :attr:`text_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + icon_color = ColorProperty(None) + """ + Button icon color in (r, g, b, a) format. + + :attr:`icon_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + font_name = StringProperty() + """ + Button text font name. + + :attr:`font_name` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + font_size = NumericProperty("14sp") + """ + Button text font size. + + :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` + and defaults to `14sp`. + """ + + icon_size = NumericProperty() + """ + Icon font size. + Use this parameter as the font size, that is, in sp units. + + .. versionadded:: 1.0.0 + + :attr:`icon_size` is a :class:`~kivy.properties.NumericProperty` + and defaults to `None`. + """ + + user_font_size = NumericProperty(0, deprecated=True) + """ + Custom font size for :class:`~MDIconButton`. + + .. deprecated in 1.0.0:: + + Use :attr:`icon_size` instead. + + :attr:`user_font_size` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + line_width = NumericProperty(1) + """ + Line width for button border. + + :attr:`line_width` is a :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + line_color = ColorProperty(None) + """ + Line color for button border. + + :attr:`line_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + line_color_disabled = ColorProperty(None) + """ + Disabled line color for button border. + + .. versionadded:: 1.0.0 + + :attr:`line_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + md_bg_color = ColorProperty(None) + """ + Button background color. + + :attr:`md_bg_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + md_bg_color_disabled = ColorProperty(None) + """ + The background color of the button when the button is disabled. + + :attr:`md_bg_color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + disabled_color = ColorProperty(None) + """ + The color of the text and icon when the button is disabled, in the + (r, g, b, a) format. + + .. versionadded:: 1.0.0 + + :attr:`disabled_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + rounded_button = BooleanProperty(False) + """ + Should the button have fully rounded corners (e.g. like M3 buttons)? + + .. versionadded:: 1.0.0 + + :attr:`rounded_button` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + # Note - _radius must be > 0 to avoid rendering issues. + _radius = BoundedNumericProperty(dp(4), min=0.0999, errorvalue=0.1) + # Properties used for rendering. + _disabled_color = ColorProperty(None) + _md_bg_color = ColorProperty(None) + _md_bg_color_disabled = ColorProperty(None) + _line_color = ColorProperty(None) + _line_color_disabled = ColorProperty(None) + _theme_text_color = OptionProperty(None, options=theme_text_color_options) + _theme_icon_color = OptionProperty(None, options=theme_text_color_options) + _text_color = ColorProperty(None) + _icon_color = ColorProperty(None) + + # Defaults which can be overridden in subclasses + _min_width = NumericProperty(dp(64)) + _min_height = NumericProperty(dp(36)) + + # Default colors - set to None to use primary theme colors + _default_md_bg_color = [0.0, 0.0, 0.0, 0.0] + _default_md_bg_color_disabled = [0.0, 0.0, 0.0, 0.0] + _default_line_color = [0.0, 0.0, 0.0, 0.0] + _default_line_color_disabled = [0.0, 0.0, 0.0, 0.0] + _default_theme_text_color = StringProperty("Primary") + _default_theme_icon_color = StringProperty("Primary") + _default_text_color = ColorProperty(None) + _default_icon_color = ColorProperty(None) + + _animation_fade_bg = ObjectProperty(None, allownone=True) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.theme_cls.bind( + primary_palette=self.set_all_colors, + theme_style=self.set_all_colors, + ) + self.bind( + md_bg_color=self.set_button_colors, + md_bg_color_disabled=self.set_button_colors, + line_color=self.set_button_colors, + line_color_disabled=self.set_button_colors, + theme_text_color=self.set_text_color, + text_color=self.set_text_color, + theme_icon_color=self.set_icon_color, + icon_color=self.set_icon_color, + disabled_color=self.set_disabled_color, + rounded_button=self.set_radius, + height=self.set_radius, + ) + Clock.schedule_once(self.set_all_colors) + Clock.schedule_once(self.set_radius) + + def set_disabled_color(self, *args): + """ + Sets the color for the icon, text and line of the button when button + is disabled. + """ + + if self.disabled: + disabled_color = ( + self.disabled_color + if self.disabled_color + else self.theme_cls.disabled_hint_text_color + ) + self._disabled_color = disabled_color + # Button icon color. + if "lbl_ic" in self.ids: + self.ids.lbl_ic.disabled_color = disabled_color + # Button text color. + if "lbl_txt" in self.ids: + self.ids.lbl_txt.disabled_color = disabled_color + else: + self._disabled_color = self._line_color + + def set_all_colors(self, *args) -> None: + """Set all button colours.""" + + self.set_button_colors() + self.set_text_color() + self.set_icon_color() + + def set_button_colors(self, *args) -> None: + """Set all button colours (except text/icons).""" + + # Set main color + self._md_bg_color = ( + self.md_bg_color + or self._default_md_bg_color + or self.theme_cls.primary_color + ) + + # Set disabled color + self._md_bg_color_disabled = ( + self.md_bg_color_disabled + or ( + [sum(self.md_bg_color[0:3]) / 3.0] * 3 + + [0.38 if self.theme_cls.theme_style == "Light" else 0.5] + if self.md_bg_color + else None + ) + or self._default_md_bg_color_disabled + or self.theme_cls.disabled_primary_color + ) + + # Set line color + self._line_color = ( + self.line_color + or self._default_line_color + or self.theme_cls.primary_color + ) + + # Set disabled line color + self._line_color_disabled = ( + self.line_color_disabled + or ( + [sum(self.line_color[0:3]) / 3.0] * 3 + + [0.38 if self.theme_cls.theme_style == "Light" else 0.5] + if self.line_color + else None + ) + or self._default_line_color_disabled + or self.theme_cls.disabled_primary_color + ) + + def set_text_color(self, *args) -> None: + """ + Set _theme_text_color and _text_color based on defaults and options. + """ + + self._theme_text_color = ( + self.theme_text_color or self._default_theme_text_color + ) + if self._default_text_color == "PrimaryHue": + default_text_color = text_colors[self.theme_cls.primary_palette][ + self.theme_cls.primary_hue + ] + elif self._default_text_color == "Primary": + default_text_color = self.theme_cls.primary_color + else: + default_text_color = self.theme_cls.text_color + self._text_color = self.text_color or default_text_color + + def set_icon_color(self, *args) -> None: + """ + Set _theme_icon_color and _icon_color based on defaults and options. + """ + + self._theme_icon_color = ( + self.theme_icon_color or self._default_theme_icon_color + ) + if self._default_icon_color == "PrimaryHue": + default_icon_color = text_colors[self.theme_cls.primary_palette][ + self.theme_cls.primary_hue + ] + elif self._default_icon_color == "Primary": + default_icon_color = self.theme_cls.primary_color + else: + default_icon_color = self.theme_cls.text_color + self._icon_color = self.icon_color or default_icon_color + + def set_radius(self, *args) -> None: + """ + Set the radius, if we are a rounded button, based on the + current height. + """ + + if self.rounded_button: + self._radius = self.height / 2 + + # Touch events that cause transparent buttons to fade to background + def on_touch_down(self, touch): + """ + Animates fade to background on press, for buttons with no + background color. + """ + + 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: + if self._md_bg_color[3] == 0.0: + self._animation_fade_bg = Animation( + duration=0.5, _md_bg_color=[0.0, 0.0, 0.0, 0.1] + ) + self._animation_fade_bg.start(self) + return super().on_touch_down(touch) + + def on_touch_up(self, touch): + """Animates return to original background on touch release.""" + + if not self.disabled and self._animation_fade_bg: + self._animation_fade_bg.stop_property(self, "_md_bg_color") + self._animation_fade_bg = None + md_bg_color = ( + self.md_bg_color + or self._default_md_bg_color + or self.theme_cls.primary_color + ) + Animation(duration=0.05, _md_bg_color=md_bg_color).start(self) + return super().on_touch_up(touch) + + def on_disabled(self, instance_button, disabled_value: bool) -> None: + Clock.schedule_once(self.set_disabled_color) + + +class ButtonElevationBehaviour(CommonElevationBehavior): + """ + Implements elevation behavior as well as the recommended down/disabled + colors for raised buttons. + + The minimum elevation for any raised button is `'1dp'`, + by default, set to `'2dp'`. + + The `_elevation_raised` is automatically computed and is set to + `self.elevation + 6` each time `self.elevation` is updated. + """ + + _elevation_raised = NumericProperty() + _anim_raised = ObjectProperty(None, allownone=True) + _default_elevation = 2 + + def __init__(self, **kwargs): + if self.elevation == 0: + self.elevation = self._default_elevation + super().__init__(**kwargs) + self.bind(_radius=self.setter("radius")) + self.on_elevation(self, self.elevation) + + def on_elevation(self, instance_button, elevation_value: int) -> None: + super().on_elevation(instance_button, elevation_value) + self._elevation_raised = self.elevation + 6 + self.on_disabled(self, self.disabled) + + def on__elevation_raised( + self, instance_button, elevation_value: int + ) -> None: + Animation.cancel_all(self, "_elevation") + self._anim_raised = Animation(_elevation=self._elevation_raised, d=0.15) + + def on_disabled(self, instance_button, disabled_value: bool) -> None: + if self.disabled is True: + Animation.cancel_all(self, "_elevation") + super().on_disabled(instance_button, disabled_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: + self.stop_elevation_anim() + return super().on_touch_up(touch) + self.stop_elevation_anim() + return super().on_touch_up(touch) + + def stop_elevation_anim(self): + Animation.cancel_all(self, "_elevation") + self._elevation = self.elevation + + +class ButtonContentsText: + """Contents for :class:`~BaseButton` class consisting of a single label.""" + + +class ButtonContentsIcon: + """ + Contents for a round BaseButton consisting of an :class:`~MDIcon` class. + """ + + _min_width = NumericProperty(0) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if self.user_font_size: + self.icon_size = self.user_font_size + self.bind(user_font_size=self.setter("icon_size")) + + def on_text_color(self, instance_button, color: list) -> None: + """ + Set icon_color equal to text_color. + For backwards compatibility - can use text_color instead + of icon_color. + """ + + if color: + self.icon_color = color + + +class ButtonContentsIconText: + """ + Contents for :class:`~BaseButton` class consisting of a + :class:`~kivy.uix.boxlayout.BoxLayout` with an icon and a label. + """ + + padding = VariableListProperty([dp(12), dp(8), dp(16), dp(8)]) + """ + Padding between the widget box and its children, in pixels: + [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]. + + .. versionadded:: 1.0.0 + + :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` + and defaults to [12dp, 8dp, 16dp, 8dp]. + """ + + +# Old MD Button classes + + +class OldButtonIconMixin: + """Backwards-compatibility for icons.""" + + icon = StringProperty("android") + + def on_icon_color(self, instance_button, color: list) -> None: + """ + If we are setting an icon color, set theme_icon_color to Custom. + For backwards compatibility (before theme_icon_color existed). + """ + + if color and (self.theme_text_color == "Custom"): + self.theme_icon_color = "Custom" + + +class MDFlatButton(ButtonContentsText, BaseButton): + """ + A flat rectangular button with (by default) no border or background. + Text is the default text color. + """ + + padding = VariableListProperty([dp(8), dp(8), dp(8), dp(8)]) + """ + Padding between the widget box and its children, in pixels: + [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]. + + .. versionadded:: 1.0.0 + + :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` + and defaults to [8dp, 8dp, 8dp, 8dp]. + """ + + +class MDRaisedButton( + FakeRectangularElevationBehavior, + ButtonElevationBehaviour, + ButtonContentsText, + BaseButton, +): + """ + A flat button with (by default) a primary color fill and matching + color text. + """ + + # FIXME: Move the underlying attributes to the :class:`~BaseButton` class. + # This applies to all classes of buttons that have similar attributes. + _default_md_bg_color = None + _default_md_bg_color_disabled = None + _default_theme_text_color = "Custom" + _default_text_color = "PrimaryHue" + + +class MDRectangleFlatButton(ButtonContentsText, BaseButton): + """ + A flat button with (by default) a primary color border and primary + color text. + """ + + _default_line_color = None + _default_line_color_disabled = None + _default_theme_text_color = "Custom" + _default_text_color = "Primary" + + +class MDRectangleFlatIconButton( + OldButtonIconMixin, ButtonContentsIconText, BaseButton +): + """ + A flat button with (by default) a primary color border, primary color text + and a primary color icon on the left. + """ + + _default_line_color = None + _default_line_color_disabled = None + _default_theme_text_color = "Custom" + _default_theme_icon_color = "Custom" + _default_text_color = "Primary" + _default_icon_color = "Primary" + + +class MDRoundFlatButton(ButtonContentsText, BaseButton): + """ + A flat button with (by default) fully rounded corners, a primary + color border and primary color text. + """ + + _default_line_color = None + _default_line_color_disabled = None + _default_theme_text_color = "Custom" + _default_text_color = "Primary" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.rounded_button = True + + +class MDRoundFlatIconButton( + OldButtonIconMixin, + ButtonContentsIconText, + BaseButton, +): + """ + A flat button with (by default) rounded corners, a primary color border, + primary color text and a primary color icon on the left. + """ + + _default_line_color = None + _default_line_color_disabled = None + _default_theme_text_color = "Custom" + _default_theme_icon_color = "Custom" + _default_text_color = "Primary" + _default_icon_color = "Primary" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.rounded_button = True + + +class MDFillRoundFlatButton(ButtonContentsText, BaseButton): + """ + A flat button with (by default) rounded corners, a primary color fill + and primary color text. + """ + + _default_md_bg_color = None + _default_md_bg_color_disabled = None + _default_theme_text_color = "Custom" + _default_text_color = "PrimaryHue" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.rounded_button = True + + +class MDFillRoundFlatIconButton( + OldButtonIconMixin, + ButtonContentsIconText, + BaseButton, +): + """ + A flat button with (by default) rounded corners, a primary color fill, + primary color text and a primary color icon on the left. + """ + + _default_md_bg_color = None + _default_md_bg_color_disabled = None + _default_theme_text_color = "Custom" + _default_theme_icon_color = "Custom" + _default_text_color = "PrimaryHue" + _default_icon_color = "PrimaryHue" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.rounded_button = True + + +class MDIconButton(OldButtonIconMixin, ButtonContentsIcon, BaseButton): + """A simple rounded icon button.""" + + icon = StringProperty("checkbox-blank-circle") + """ + Button icon. + + :attr:`icon` is a :class:`~kivy.properties.StringProperty` + and defaults to `'checkbox-blank-circle'`. + """ + + text_color = ColorProperty(None, deprecated=True) + """ + Button icon color in (r, g, b, a) format. + + .. deprecated in 1.0.0:: + + Deprecated for :class:`~MDIconButton`. Use ``icon_color`` instead. + + :attr:`text_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + theme_text_color = OptionProperty( + None, options=theme_text_color_options, deprecated=True + ) + """ + Button icon type. Available options are: (`"Primary"`, `"Secondary"`, + `"Hint"`, `"Error"`, `"Custom"`, `"ContrastParentBackground"`). + + .. deprecated in 1.0.0:: + + Deprecated for :class:`~MDIconButton`. Use ``theme_icon_color`` instead. + + + :attr:`theme_text_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `None` (set by button class). + """ + + _min_width = NumericProperty(0) + _default_icon_pad = max(dp(48) - sp(24), 0) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.rounded_button = True + # FIXME: GraphicException: Invalid width value, must be > 0 + self.line_width = 0.001 + Clock.schedule_once(self.set_size) + + def set_size(self, interval: Union[int, float]) -> None: + """ + Sets the icon width/height based on the current `icon_size` + attribute, or the default value if it is zero. The icon size + is set to `(48, 48)` for an icon with the default font_size 24sp. + """ + diameter = self._default_icon_pad + (self.icon_size or sp(24)) + self.width = diameter + self.height = diameter + + +class MDFloatingActionButton( + OldButtonIconMixin, + RoundedRectangularElevationBehavior, + ButtonElevationBehaviour, + ButtonContentsIcon, + BaseButton, +): + """ + Implementation + `FAB `_ + button. + """ + + type = OptionProperty("standard", options=["small", "large", "standard"]) + """ + Type of M3 button. + + .. versionadded:: 1.0.0 + + Available options are: 'small', 'large', 'standard'. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-floating-action-button-types.png + :align: center + + :attr:`type` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'standard'`. + """ + + _default_md_bg_color = None + _default_md_bg_color_disabled = None + _default_theme_icon_color = "Custom" + _default_icon_color = "PrimaryHue" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + # FIXME: GraphicException: Invalid width value, must be > 0 + self.line_width = 0.001 + self.theme_cls.bind(material_style=self.set_size) + self.theme_cls.bind(material_style=self.set__radius) + Clock.schedule_once(self.set_size) + Clock.schedule_once(self.set__radius) + Clock.schedule_once(self.set_font_size) + + def set_font_size(self, *args) -> None: + if self.theme_cls.material_style == "M3": + if self.type == "large": + self.icon_size = "36sp" + else: + self.icon_size = 0 + + def set__radius(self, *args) -> None: + if self.theme_cls.material_style == "M2": + self.rounded_button = True + else: + self.rounded_button = False + if self.type == "small": + self._radius = dp(12) + elif self.type == "standard": + self._radius = dp(16) + elif self.type == "large": + self._radius = dp(28) + + def set_size(self, *args) -> None: + if self.theme_cls.material_style == "M2": + self.size = dp(56), dp(56) + else: + if self.type == "small": + self.size = dp(40), dp(40) + elif self.type == "standard": + self.size = dp(56), dp(56) + elif self.type == "large": + self.size = dp(96), dp(96) + + def on_type(self, instance_md_floating_action_button, type: str) -> None: + self.set_size() + self.set_font_size() + + +class MDTextButton(ButtonBehavior, MDLabel): + color = ColorProperty(None) + """ + Button color in (r, g, b, a) format. + + :attr:`color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + color_disabled = ColorProperty(None) + """ + Button color disabled in (r, g, b, a) format. + + :attr:`color_disabled` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _color = ColorProperty(None) # last current button text color + + def animation_label(self) -> None: + 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_button, disabled_value) -> None: + if disabled_value: + if not self.color_disabled: + self.color_disabled = self.theme_cls.disabled_hint_text_color + self._color = self.color + self.text_color = self.color_disabled + else: + self.text_color = self._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 = ColorProperty(None) + + def set_size(self, interval: Union[int, float]) -> None: + self.width = "46dp" + self.height = "46dp" + + +# FIXME: Use :class:`~kivymd.uix.boxlayout.MDBoxLayout` instead +# :class:`~kivy.uix.boxlayout.BoxLayout`. +class BaseFloatingLabel( + ThemableBehavior, FakeRectangularElevationBehavior, BoxLayout +): + text = StringProperty() + text_color = ColorProperty(None) + bg_color = ColorProperty(None) + + +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 = ColorProperty([0, 0, 0, 1]) + """ + Floating text color in (r, g, b, a) format. + + :attr:`label_text_color` is a :class:`~kivy.properties.ColorProperty` + 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 pixels + 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`. + """ + + root_button_anim = BooleanProperty(False) + """ + If ``True`` then the root button will rotate 45 degrees when the stack + is opened. + + :attr:`root_button_anim` 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 = ColorProperty(None) + """ + Root button color in (r, g, b, a) format. + + :attr:`bg_color_root_button` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[]`. + """ + + bg_color_stack_button = ColorProperty(None) + """ + The color of the buttons in the stack (r, g, b, a) format. + + :attr:`bg_color_stack_button` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[]`. + """ + + color_icon_stack_button = ColorProperty(None) + """ + The color icon of the buttons in the stack (r, g, b, a) format. + + :attr:`color_icon_stack_button` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[]`. + """ + + color_icon_root_button = ColorProperty(None) + """ + The color icon of the root button (r, g, b, a) format. + + :attr:`color_icon_root_button` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[]`. + """ + + bg_hint_color = ColorProperty(None) + """ + Background color for the text of the buttons in the stack (r, g, b, a) format. + + :attr:`bg_hint_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + 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_button: MDFloatingBottomButton) -> None: + """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) + for item in self.data.items(): + if widget.text in item: + Animation( + _canvas_width=0, + _padding_right=0, + d=self.opening_time, + t=self.opening_transition, + _elevation=0, + ).start(instance_button) + Animation( + opacity=0, d=0.1, t=self.opening_transition + ).start(widget) + + def on_enter(self, instance_button: MDFloatingBottomButton) -> None: + """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 + Animation.cancel_all(widget) + for item in self.data.items(): + if widget.text in item: + 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_button) + if ( + instance_button.icon + == self.data[f"{widget.text}"] + ): + Animation( + opacity=1, + d=self.opening_time, + t=self.opening_transition, + ).start(widget) + else: + Animation( + opacity=0, d=0.1, t=self.opening_transition + ).start(widget) + + def on_data(self, instance_speed_dial, data: dict) -> None: + """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, name_icon in data.items(): + 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 = name + 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_speed_dial, name_icon: str) -> None: + self._get_count_widget(MDFloatingRootButton).icon = name_icon + + def on_label_text_color(self, instance_speed_dial, color: list) -> None: + for widget in self.children: + if isinstance(widget, MDFloatingLabel): + widget.text_color = color + + def on_color_icon_stack_button( + self, instance_speed_dial, color: list + ) -> None: + for widget in self.children: + if isinstance(widget, MDFloatingBottomButton): + widget.text_color = color + + def on_hint_animation(self, instance_speed_dial, value: bool) -> None: + for widget in self.children: + if isinstance(widget, MDFloatingLabel): + widget.bg_color = (0, 0, 0, 0) + + def on_bg_hint_color(self, instance_speed_dial, color: list) -> None: + for widget in self.children: + if isinstance(widget, MDFloatingBottomButton): + widget._bg_color = color + + def on_color_icon_root_button( + self, instance_speed_dial, color: list + ) -> None: + self._get_count_widget(MDFloatingRootButton).text_color = color + + def on_bg_color_stack_button( + self, instance_speed_dial, color: list + ) -> None: + for widget in self.children: + if isinstance(widget, MDFloatingBottomButton): + widget.md_bg_color = color + + def on_bg_color_root_button(self, instance_speed_dial, color: list) -> None: + self._get_count_widget(MDFloatingRootButton).md_bg_color = color + + def set_pos_labels(self, instance_floating_label: MDFloatingLabel) -> None: + """ + Sets the position of the floating labels. + Called when the application's root window is resized. + """ + + if self.anchor == "right": + instance_floating_label.x = ( + Window.width - instance_floating_label.width - dp(86) + ) + + def set_pos_root_button( + self, instance_floating_root_button: MDFloatingRootButton + ) -> None: + """ + Sets the position of the root button. + Called when the application's root window is resized. + """ + + if self.anchor == "right": + instance_floating_root_button.y = dp(20) + instance_floating_root_button.x = Window.width - (dp(56) + dp(20)) + + def set_pos_bottom_buttons( + self, instance_floating_bottom_button: MDFloatingBottomButton + ) -> None: + """ + Sets the position of the bottom buttons in a stack. + Called when the application's root window is resized. + """ + + if self.anchor == "right": + if self.state != "open": + instance_floating_bottom_button.y = ( + instance_floating_bottom_button.height / 2 + ) + instance_floating_bottom_button.x = Window.width - ( + instance_floating_bottom_button.height + + instance_floating_bottom_button.width / 2 + ) + + def open_stack( + self, instance_floating_root_button: MDFloatingRootButton + ) -> None: + """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.root_button_anim + ): + # 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: dict) -> None: + """ + :param 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.root_button_anim + ): + 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/sbapp/kivymd/uix/card/__init__.py b/sbapp/kivymd/uix/card/__init__.py new file mode 100644 index 0000000..7577fc9 --- /dev/null +++ b/sbapp/kivymd/uix/card/__init__.py @@ -0,0 +1,8 @@ +# NOQA F401 +from .card import ( + MDCard, + MDCardSwipe, + MDCardSwipeFrontBox, + MDCardSwipeLayerBox, + MDSeparator, +) diff --git a/sbapp/kivymd/uix/card/card.kv b/sbapp/kivymd/uix/card/card.kv new file mode 100644 index 0000000..ac67a51 --- /dev/null +++ b/sbapp/kivymd/uix/card/card.kv @@ -0,0 +1,20 @@ +: + md_bg_color: app.theme_cls.divider_color + + + + canvas.before: + Color: + rgba: self.md_bg_color + RoundedRectangle: + size: self.size + pos: self.pos + radius: root.radius + source: root.background + + + + md_bg_color: + self.theme_cls.divider_color \ + if not root.color \ + else root.color diff --git a/sbapp/kivymd/uix/card/card.py b/sbapp/kivymd/uix/card/card.py new file mode 100755 index 0000000..85f7f27 --- /dev/null +++ b/sbapp/kivymd/uix/card/card.py @@ -0,0 +1,938 @@ +""" +Components/Card +=============== + +.. seealso:: + + `Material Design spec, Cards `_ and + `Material Design 3 spec, Cards `_ + +.. rubric:: Cards contain content and actions about a single subject. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/cards.png + :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 +------ + +.. warning:: Starting from the KivyMD 1.0.0 library version, it is necessary + to manually inherit the card class from one of the ``Elevation`` classes + from ``kivymd/uix/behaviors/elevation.py`` module to draw the card shadow. + +.. code-block:: python + + from kivymd.uix.behaviors import RoundedRectangularElevationBehavior + from kivymd.uix.card import MDCard + + + class MD3Card(MDCard, RoundedRectangularElevationBehavior): + '''Implements a material design v3 card.''' + +This may sound awkward to you, but it actually allows for better control over +the providers that implement the rendering of the shadows. + +.. note:: You can read more information about the classes that implement the + rendering of shadows on this `documentation page `_. + +An example of the implementation of a card in the style of material design version 3 +------------------------------------------------------------------------------------ + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import StringProperty + + from kivymd.app import MDApp + from kivymd.uix.behaviors import RoundedRectangularElevationBehavior + from kivymd.uix.card import MDCard + + KV = ''' + + padding: 16 + size_hint: None, None + size: "200dp", "100dp" + + MDRelativeLayout: + size_hint: None, None + size: root.size + + MDIconButton: + icon: "dots-vertical" + pos: + root.width - (self.width + root.padding[0] + dp(4)), \ + root.height - (self.height + root.padding[0] + dp(4)) + + MDLabel: + id: label + text: root.text + adaptive_size: True + color: .2, .2, .2, .8 + + + MDScreen: + + MDBoxLayout: + id: box + adaptive_size: True + spacing: "56dp" + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class MD3Card(MDCard, RoundedRectangularElevationBehavior): + '''Implements a material design v3 card.''' + + text = StringProperty() + + + class TestCard(MDApp): + def build(self): + self.theme_cls.material_style = "M3" + return Builder.load_string(KV) + + def on_start(self): + styles = { + "elevated": "#f6eeee", "filled": "#f4dedc", "outlined": "#f8f5f4" + } + for style in styles.keys(): + self.root.ids.box.add_widget( + MD3Card( + line_color=(0.2, 0.2, 0.2, 0.8), + style=style, + text=style.capitalize(), + md_bg_color=styles[style], + ) + ) + + + TestCard().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/cards-m3.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 + + + MDScreen: + + MDBoxLayout: + orientation: "vertical" + spacing: "10dp" + + MDTopAppBar: + 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 + + +.. Note:: 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 + + + MDScreen: + + MDBoxLayout: + orientation: "vertical" + spacing: "10dp" + + MDTopAppBar: + 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 + + + MDScreen: + + MDBoxLayout: + orientation: "vertical" + spacing: "10dp" + + MDTopAppBar: + 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" + + + MDScreen: + + 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 + adaptive_height: True + + MDLabel: + text: "July 27, 1984" + adaptive_height: True + theme_text_color: "Primary" + + MDSeparator: + + MDBoxLayout: + id: box_bottom + adaptive_height: True + padding: "10dp", 0, 0, 0 + + MDLabel: + text: "Rate this album" + adaptive_height: True + 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", +) + +import os +from typing import Union + +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, + ColorProperty, + NumericProperty, + OptionProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.relativelayout import RelativeLayout + +from kivymd import uix_path +from kivymd.color_definitions import colors +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + BackgroundColorBehavior, + FocusBehavior, + RectangularRippleBehavior, +) +from kivymd.uix.boxlayout import MDBoxLayout + +with open( + os.path.join(uix_path, "card", "card.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDSeparator(ThemableBehavior, MDBoxLayout): + """A separator line.""" + + color = ColorProperty(None) + """ + Separator color in ``rgba`` format. + + :attr:`color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.on_orientation() + + def on_orientation(self, *args) -> None: + 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, + RectangularRippleBehavior, + FocusBehavior, + BoxLayout, +): + 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. + """ + + radius = VariableListProperty([dp(6), dp(6), dp(6), dp(6)]) + """ + Card radius by default. + + .. versionadded:: 1.0.0 + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[dp(6), dp(6), dp(6), dp(6)]`. + """ + + style = OptionProperty(None, options=("filled", "elevated", "outlined")) + """ + Card type. + + .. versionadded:: 1.0.0 + + Available options are: 'filled', 'elevated', 'outlined'. + + :attr:`style` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'elevated'`. + """ + + _bg_color_map = ( + colors["Light"]["CardsDialogs"], + 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) + self.theme_cls.bind(material_style=self.set_style) + Clock.schedule_once(self.set_style) + Clock.schedule_once( + lambda x: self.on_ripple_behavior(0, self.ripple_behavior) + ) + self.update_md_bg_color(self, self.theme_cls.theme_style) + + def update_md_bg_color(self, instance_card, theme_style: str) -> None: + if self.md_bg_color in self._bg_color_map: + self.md_bg_color = colors[theme_style]["CardsDialogs"] + + def set_style(self, *args) -> None: + self.set_radius() + self.set_elevation() + self.set_line_color() + + def set_line_color(self): + if self.theme_cls.material_style == "M3": + if self.style == "elevated" or self.style == "filled": + self.line_color = [0, 0, 0, 0] + + def set_elevation(self): + if self.theme_cls.material_style == "M3": + if self.style == "outlined" or self.style == "filled": + self.elevation = 0 + elif self.style == "elevated": + self.elevation = 1 + + def set_radius(self) -> None: + if ( + self.radius == [dp(6), dp(6), dp(6), dp(6)] + and self.theme_cls.material_style == "M3" + ): + self.radius = [dp(16), dp(16), dp(16), dp(16)] + elif ( + self.radius == [dp(16), dp(16), dp(16), dp(16)] + and self.theme_cls.material_style == "M2" + ): + self.radius = [dp(6), dp(6), dp(6), dp(6)] + + def on_ripple_behavior( + self, interval: Union[int, float], value_behavior: bool + ) -> None: + self._no_ripple_effect = False if value_behavior 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 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_swipe_to_delete_item, anchor_value: str + ) -> None: + if anchor_value == "right": + self.open_progress = 1.0 + else: + self.open_progress = 0.0 + + def on_open_progress( + self, instance_swipe_to_delete_item, progress_value: float + ) -> None: + if self.anchor == "left": + self.children[0].x = self.width * progress_value + else: + self.children[0].x = self.width * progress_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) -> None: + 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) -> None: + 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) -> None: + 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 _on_swipe_complete(self, *args): + self.dispatch("on_swipe_complete") + + 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(MDBoxLayout): + pass diff --git a/sbapp/kivymd/uix/carousel.py b/sbapp/kivymd/uix/carousel.py new file mode 100644 index 0000000..600df2b --- /dev/null +++ b/sbapp/kivymd/uix/carousel.py @@ -0,0 +1,215 @@ +""" +Components/Carousel +=================== + +:class:`~kivy.uix.boxlayout.Carousel` class equivalent. Simplifies working +with some widget properties. For example: + + +Carousel +--------- + +.. code-block:: python + + kv=''' + YourCarousel: + BoxLayout: + [...] + BoxLayout: + [...] + BoxLayout: + [...] + ''' + builder.load_string(kv) + + class YourCarousel(Carousel): + def __init__(self,*kwargs): + self.register_event_type("on_slide_progress") + self.register_event_type("on_slide_complete") + + def on_touch_down(self, *args): + ["Code to detect when the slide changes"] + + def on_touch_up(self, *args): + ["Code to detect when the slide changes"] + + def Calculate_slide_pos(self, *args): + ["Code to calculate the current position of the slide"] + + def do_custom_animation(self, *args): + ["Code to recreate an animation"] + + +MDCarousel +----------- + +.. code-block:: kv + + MDCarousel: + on_slide_progress: + do_something() + on_slide_complete: + do_something() + +""" +# TODO: Add documentation. + +from kivy.animation import Animation +from kivy.uix.carousel import Carousel + + +class MDCarousel(Carousel): + """ + based on kivy's carousel. + + .. seealso:: + `kivy.uix.carousel.Carousel `_ + """ + + _scrolling = False + + 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): + """ + Event launched when the Slide animation is progress. + remember to bind and unbid to this method. + """ + + def on_slide_complete(self, *args): + """ + Event launched when the Slide animation is complete. + remember to bind and unbid to this method. + """ + + 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) + + if self._scrolling: + self.dispatch("on_slide_progress", xoff) + + 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 on_touch_down(self, touch): + self._scrolling = True + return super().on_touch_down(touch) + + def on_touch_up(self, touch): + self._scrolling = False + return super().on_touch_up(touch) + + 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, + on_progress=lambda *args: self.dispatch( + "on_slide_progress", self._offset + ), + ) + anim.start(self) diff --git a/sbapp/kivymd/uix/chip/__init__.py b/sbapp/kivymd/uix/chip/__init__.py new file mode 100644 index 0000000..86420ec --- /dev/null +++ b/sbapp/kivymd/uix/chip/__init__.py @@ -0,0 +1 @@ +from .chip import MDChip, MDChooseChip # NOQA F401 diff --git a/sbapp/kivymd/uix/chip/chip.kv b/sbapp/kivymd/uix/chip/chip.kv new file mode 100644 index 0000000..361bc98 --- /dev/null +++ b/sbapp/kivymd/uix/chip/chip.kv @@ -0,0 +1,110 @@ + + scale_value_x: 0 + scale_value_y: 0 + scale_value_z: 0 + + + + size_hint_y: None + height: "32dp" + spacing: "8dp" + adaptive_width: True + radius: 16 if self.radius == [0, 0, 0, 0] else self.radius + padding: + "12dp" if not self.icon_left else "4dp", \ + 0, \ + "12dp" if not self.icon_right else "8dp", \ + 0 + md_bg_color: + ( \ + app.theme_cls.bg_darkest \ + if app.theme_cls.theme_style == "Light" else \ + app.theme_cls.bg_light \ + ) \ + if not self.disabled else app.theme_cls.disabled_hint_text_color + + canvas.before: + Color: + rgba: + self.line_color \ + if not self.disabled else \ + app.theme_cls.disabled_hint_text_color + Line: + width: 1 + rounded_rectangle: + ( \ + self.x, \ + self.y, \ + self.width, \ + self.height, \ + *self.radius, \ + self.height \ + ) + + MDRelativeLayout: + id: relative_box + size_hint: None, None + size: ("24dp", "24dp") if root.icon_left else (0, 0) + pos_hint: {"center_y": .5} + radius: [int(self.height / 2),] + + MDIcon: + id: icon_left + icon: root.icon_left + size_hint: None, None + size: ("28dp", "28dp") if root.icon_left else (0, 0) + theme_text_color: "Custom" + pos_hint: {"center_y": .5} + pos: 0, -2 + text_color: + ( \ + root.icon_left_color \ + if root.icon_left_color else \ + root.theme_cls.disabled_hint_text_color \ + ) \ + if not self.disabled else app.theme_cls.disabled_hint_text_color + + MDBoxLayout: + id: icon_left_box + size_hint: None, None + radius: [int(self.height / 2),] + size: ("28dp", "28dp") if root.icon_left else (0, 0) + pos: 0, -2 + + MDScalableCheckIcon: + id: check_icon + icon: "check" + size_hint: None, None + size: "28dp", "28dp" + color: (1, 1, 1, 1) if not root.icon_check_color else root.icon_check_color + pos: 2, -2 + + MDLabel: + id: label + text: root.text + adaptive_size: True + markup: True + pos_hint: {"center_y": .5} + color: + ( \ + root.text_color \ + if root.text_color else \ + root.theme_cls.disabled_hint_text_color \ + ) \ + if not self.disabled else app.theme_cls.disabled_hint_text_color + + MDIcon: + id: icon_right + icon: root.icon_right + size_hint: None, None + size: ("18dp", "18dp") if root.icon_right else (0, 0) + font_size: "18sp" if root.icon_right else 0 + theme_text_color: "Custom" + pos_hint: {"center_y": .5} + text_color: + ( \ + root.icon_right_color \ + if root.icon_right_color else \ + root.theme_cls.disabled_hint_text_color \ + ) \ + if not self.disabled else app.theme_cls.disabled_hint_text_color diff --git a/sbapp/kivymd/uix/chip/chip.py b/sbapp/kivymd/uix/chip/chip.py new file mode 100755 index 0000000..54f0b77 --- /dev/null +++ b/sbapp/kivymd/uix/chip/chip.py @@ -0,0 +1,644 @@ +""" +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:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDChip: + text: "Portland" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.on_release_chip(self) + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def on_release_chip(self, instance_check): + print(instance_check) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ordinary-chip.png + :align: center + +Use with right icon +------------------- + +.. code-block:: kv + + MDChip: + text: "Portland" + icon_right: "close-circle-outline" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-right-icon.png + :align: center + +Use with left icon +------------------ + +.. code-block:: kv + + MDChip: + text: "Portland" + icon_left: "map-marker" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-left-icon.png + :align: center + +Use with custom left icon +------------------------- + +.. code-block:: kv + + MDChip: + text: "Portland" + icon_left: "avatar.png" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-custom-left-icon.png + :align: center + +Use with left and right icon +---------------------------- + +.. code-block:: kv + + MDChip: + text: "Portland" + icon_left: "avatar.png" + icon_right: "close-circle-outline" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-left-right-icon.png + :align: center + +Use with outline +---------------- + +.. code-block:: kv + + MDChip: + text: "Portland" + icon_left: "avatar.png" + icon_right: "close-circle-outline" + line_color: app.theme_cls.disabled_hint_text_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-outline.png + :align: center + +Use with custom color +--------------------- + +.. code-block:: kv + + MDChip: + text: "Portland" + icon_left: "avatar.png" + icon_right: "close-circle-outline" + line_color: app.theme_cls.disabled_hint_text_color + md_bg_color: 1, 0, 0, .5 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-custom-color.png + :align: center + +Use with elevation +------------------ + +.. code-block:: kv + + MDChip: + text: "Portland" + icon_left: "avatar.png" + icon_right: "close-circle-outline" + line_color: app.theme_cls.disabled_hint_text_color + md_bg_color: 1, 0, 0, .5 + elevation: 12 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-elevation.png + :align: center + +Behavior +======== + +Long press on the chip, it will be marked. +When you click on the marked chip, the mark will be removed: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-activate.gif + :align: center + +Examples +======== + +Multiple choose +--------------- + +Selecting a single choice chip automatically deselects all other chips in the set. + +.. code-block:: python + + from kivy.animation import Animation + from kivy.lang import Builder + + from kivymd.uix.screen import MDScreen + from kivymd.uix.chip import MDChip + from kivymd.app import MDApp + + KV = ''' + + + MDBoxLayout: + orientation: "vertical" + adaptive_size: True + spacing: "12dp" + padding: "56dp" + pos_hint: {"center_x": .5, "center_y": .5} + + MDLabel: + text: "Multiple choice" + bold: True + font_style: "H5" + adaptive_size: True + + MDBoxLayout: + id: chip_box + adaptive_size: True + spacing: "8dp" + + MyChip: + text: "Elevator" + on_press: if self.active: root.removes_marks_all_chips() + + MyChip: + text: "Washer / Dryer" + on_press: if self.active: root.removes_marks_all_chips() + + MyChip: + text: "Fireplace" + on_press: if self.active: root.removes_marks_all_chips() + + + ScreenManager: + + MyScreen: + ''' + + + class MyChip(MDChip): + icon_check_color = (0, 0, 0, 1) + text_color = (0, 0, 0, 0.5) + _no_ripple_effect = True + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.bind(active=self.set_chip_bg_color) + self.bind(active=self.set_chip_text_color) + + def set_chip_bg_color(self, instance_chip, active_value: int): + ''' + Will be called every time the chip is activated/deactivated. + Sets the background color of the chip. + ''' + + self.md_bg_color = ( + (0, 0, 0, 0.4) + if active_value + else ( + self.theme_cls.bg_darkest + if self.theme_cls.theme_style == "Light" + else ( + self.theme_cls.bg_light + if not self.disabled + else self.theme_cls.disabled_hint_text_color + ) + ) + ) + + def set_chip_text_color(self, instance_chip, active_value: int): + Animation( + color=(0, 0, 0, 1) if active_value else (0, 0, 0, 0.5), d=0.2 + ).start(self.ids.label) + + + class MyScreen(MDScreen): + def removes_marks_all_chips(self): + for instance_chip in self.ids.chip_box.children: + if instance_chip.active: + instance_chip.active = False + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-multiple-choose.gif + :align: center + +Only choose +----------- + +Only one chip will be selected. + +.. code-block:: python + + KV = ''' + + + [...] + + MDBoxLayout: + id: chip_box + adaptive_size: True + spacing: "8dp" + + MyChip: + text: "Elevator" + on_active: if self.active: root.removes_marks_all_chips(self) + + MyChip: + text: "Washer / Dryer" + on_active: if self.active: root.removes_marks_all_chips(self) + + MyChip: + text: "Fireplace" + on_active: if self.active: root.removes_marks_all_chips(self) + + + [...] + ''' + + + class MyScreen(MDScreen): + def removes_marks_all_chips(self, selected_instance_chip): + for instance_chip in self.ids.chip_box.children: + if instance_chip != selected_instance_chip: + instance_chip.active = False + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-only-choose.gif + :align: center +""" + +__all__ = ("MDChip",) + +import os + +from kivy import Logger +from kivy.animation import Animation +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import BooleanProperty, ColorProperty, StringProperty +from kivy.uix.behaviors import ButtonBehavior + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + FakeRectangularElevationBehavior, + RectangularRippleBehavior, + TouchBehavior, +) +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.label import MDIcon +from kivymd.uix.stacklayout import MDStackLayout +from kivymd.uix.templates import ScaleWidget + +with open( + os.path.join(uix_path, "chip", "chip.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDChip( + ThemableBehavior, + RectangularRippleBehavior, + FakeRectangularElevationBehavior, + TouchBehavior, + ButtonBehavior, + MDBoxLayout, +): + text = StringProperty() + """ + Chip text. + + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon = StringProperty("checkbox-blank-circle", deprecated=True) + """ + Chip icon. + + .. deprecated:: 1.0.0 + Use :attr:`icon_right` and :attr:`icon_left` instead. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'checkbox-blank-circle'`. + """ + + icon_left = StringProperty() + """ + Chip left icon. + + .. versionadded:: 1.0.0 + + :attr:`icon_left` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon_right = StringProperty() + """ + Chip right icon. + + .. versionadded:: 1.0.0 + + :attr:`icon_right` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + color = ColorProperty(None, deprecated=True) + """ + Chip color in ``rgba`` format. + + .. deprecated:: 1.0.0 + + :attr:`color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text_color = ColorProperty(None) + """ + Chip's text color in ``rgba`` format. + + :attr:`text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + icon_color = ColorProperty(None, deprecated=True) + """ + Chip's icon color in ``rgba`` format. + + .. deprecated:: 1.0.0 + Use :attr:`icon_right_color` and :attr:`icon_left_color` instead. + + :attr:`icon_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + icon_right_color = ColorProperty(None) + """ + Chip's right icon color in ``rgba`` format. + + .. versionadded:: 1.0.0 + + :attr:`icon_right_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + icon_left_color = ColorProperty(None) + """ + Chip's left icon color in ``rgba`` format. + + .. versionadded:: 1.0.0 + + :attr:`icon_left_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + icon_check_color = ColorProperty(None) + """ + Chip's check icon color in ``rgba`` format. + + .. versionadded:: 1.0.0 + + :attr:`icon_check_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + check = BooleanProperty(False, deprecated=True) + """ + If `True`, a checkmark is added to the left when touch to the chip. + + .. deprecated:: 1.0.0 + + :attr:`check` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + selected_chip_color = ColorProperty(None, deprecated=True) + """ + The color of the chip that is currently selected in ``rgba`` format. + + .. deprecated:: 1.0.0 + + :attr:`selected_chip_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + active = BooleanProperty(False) + """ + Whether the check is marked or not. + + .. versionadded:: 1.0.0 + + :attr:`active` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def on_long_touch(self, *args) -> None: + if self.active: + return + self.active = True if not self.active else False + + def on_active(self, instance_check, active_value: bool) -> None: + if active_value: + self.do_animation_check((0, 0, 0, 0.4), 1) + else: + self.do_animation_check((0, 0, 0, 0), 0) + + def do_animation_check(self, md_bg_color: list, scale_value: int) -> None: + Animation(md_bg_color=md_bg_color, t="out_sine", d=0.1).start( + self.ids.icon_left_box + ) + Animation( + scale_value_x=scale_value, + scale_value_y=scale_value, + scale_value_z=scale_value, + t="out_sine", + d=0.1, + ).start(self.ids.check_icon) + + if not self.icon_left: + if scale_value: + self.ids.check_icon.x = -dp(4) + Animation(size=(dp(24), dp(24)), t="out_sine", d=0.1).start( + self.ids.relative_box + ) + else: + self.ids.check_icon.x = 0 + Animation(size=(0, 0), t="out_sine", d=0.1).start( + self.ids.relative_box + ) + + def on_press(self, *args): + if self.active: + self.active = False + + +class MDChooseChip(MDStackLayout): + def __init__(self, **kwargs): + super().__init__(**kwargs) + Logger.warning( + "MDChooseChip: " + "class is deprecated and will be removed in a future version" + ) + + +class MDScalableCheckIcon(MDIcon, ScaleWidget): + pos_hint = {"center_y": 0.5} + + +if __name__ == "__main__": + from kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + + KV = """ + + + MDBoxLayout: + orientation: "vertical" + adaptive_size: True + spacing: "12dp" + padding: "56dp" + pos_hint: {"center_x": .5, "center_y": .5} + + MDLabel: + text: "Multiple choose" + bold: True + font_style: "H5" + adaptive_size: True + + MDBoxLayout: + id: chip_box + adaptive_size: True + spacing: "8dp" + + MyChip: + text: "Elevator" + on_press: if self.active: root.removes_marks_all_chips() + + MyChip: + text: "Washer / Dryer" + on_press: if self.active: root.removes_marks_all_chips() + + MyChip: + text: "Fireplace" + on_press: if self.active: root.removes_marks_all_chips() + + MDSeparator: + + MDLabel: + text: "Only choose" + bold: True + font_style: "H5" + adaptive_size: True + + MDBoxLayout: + id: chip_only_box + adaptive_size: True + spacing: "8dp" + + MyChip: + text: "Elevator" + on_active: if self.active: root.removes_marks_all_chips(self, False) + + MyChip: + text: "Washer / Dryer" + on_active: if self.active: root.removes_marks_all_chips(self, False) + + MyChip: + text: "Fireplace" + on_active: if self.active: root.removes_marks_all_chips(self, False) + + +ScreenManager: + + MyScreen: + """ + + class MyChip(MDChip): + icon_check_color = (0, 0, 0, 1) + text_color = (0, 0, 0, 0.5) + _no_ripple_effect = True + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.bind(active=self.set_chip_bg_color) + self.bind(active=self.set_chip_text_color) + + def set_chip_bg_color(self, instance_chip, active_value: int): + """ + Will be called every time the chip is activated/deactivated. + Sets the background color of the chip. + """ + + self.md_bg_color = ( + (0, 0, 0, 0.4) + if active_value + else ( + self.theme_cls.bg_darkest + if self.theme_cls.theme_style == "Light" + else ( + self.theme_cls.bg_light + if not self.disabled + else self.theme_cls.disabled_hint_text_color + ) + ) + ) + + def set_chip_text_color(self, instance_chip, active_value: int): + Animation( + color=(0, 0, 0, 1) if active_value else (0, 0, 0, 0.5), d=0.2 + ).start(self.ids.label) + + class MyScreen(MDScreen): + def removes_marks_all_chips( + self, selected_instance_chip=None, multiple=True + ): + if multiple: + for instance_chip in self.ids.chip_box.children: + if instance_chip.active: + instance_chip.active = False + else: + for instance_chip in self.ids.chip_only_box.children: + if instance_chip != selected_instance_chip: + instance_chip.active = False + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + Test().run() diff --git a/sbapp/kivymd/uix/circularlayout.py b/sbapp/kivymd/uix/circularlayout.py new file mode 100644 index 0000000..9f02e85 --- /dev/null +++ b/sbapp/kivymd/uix/circularlayout.py @@ -0,0 +1,208 @@ +""" +Components/CircularLayout +========================= + +CircularLayout is a special layout that places widgets around a circle. + +MDCircularLayout +---------------- + +.. rubric:: Usage + +.. code-block:: + + from kivy.lang.builder import Builder + from kivy.uix.label import Label + + from kivymd.app import MDApp + + kv = ''' + MDScreen: + + MDCircularLayout: + id: container + pos_hint: {"center_x": .5, "center_y": .5} + row_spacing: min(self.size) * 0.1 + ''' + + + class Main(MDApp): + def build(self): + return Builder.load_string(kv) + + def on_start(self): + for x in range(1, 49): + self.root.ids.container.add_widget( + Label(text=f"{x}", color=[0, 0, 0, 1]) + ) + + + Main().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/circular-layout.png + :align: center +""" + +__all__ = ("MDCircularLayout",) + +from math import atan2, cos, degrees, radians, sin + +from kivy.properties import BooleanProperty, NumericProperty + +from kivymd.uix.floatlayout import MDFloatLayout + + +class MDCircularLayout(MDFloatLayout): + + degree_spacing = NumericProperty(30) + """ + The space between children in degree. + + :attr:`degree_spacing` is an :class:`~kivy.properties.NumericProperty` + and defaults to `30`. + """ + + circular_radius = NumericProperty(None, allownone=True) + """ + Radius of circle. Radius will be the greatest value in the layout if `circular_radius` if not specified. + + :attr:`circular_radius` is an :class:`~kivy.properties.NumericProperty` + and defaults to `None`. + """ + + start_from = NumericProperty(60) + """ + The positon of first child in degree. + + :attr:`start_from` is an :class:`~kivy.properties.NumericProperty` + and defaults to `60`. + """ + + max_degree = NumericProperty(360) + """ + Maximum range in degree allowed for each row of widgets before jumping to the next row. + + :attr:`max_degree` is an :class:`~kivy.properties.NumericProperty` + and defaults to `360`. + """ + + circular_padding = NumericProperty("25dp") + """ + Padding between outer widgets and the edge of the biggest circle. + + :attr:`circular_padding` is an :class:`~kivy.properties.NumericProperty` + and defaults to `25dp`. + """ + + row_spacing = NumericProperty("50dp") + """ + Space between each row of widget. + + :attr:`row_spacing` is an :class:`~kivy.properties.NumericProperty` + and defaults to `50dp`. + """ + + clockwise = BooleanProperty(True) + """ + Direction of widgets in circular direction. + + :attr:`clockwise` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.bind( + row_spacing=self._update_layout, + ) + + def get_angle(self, pos: tuple) -> float: + """Returns the angle of given pos.""" + + center = [self.pos[0] + self.width / 2, self.pos[1] + self.height / 2] + (dx, dy) = (center[0] - pos[0], center[1] - pos[1]) + angle = degrees(atan2(float(dy), float(dx))) + angle += 180 + return angle + + def remove_widget(self, widget, **kwargs): + super().remove_widget(widget, **kwargs) + self._update_layout() + + def do_layout(self, *largs, **kwargs): + self._update_layout() + return super().do_layout(*largs, **kwargs) + + def _max_per_row(self): + return int(self.max_degree / self.degree_spacing) + + def _update_layout(self, *args): + for index, child in enumerate(reversed(self.children)): + pos = self._point_on_circle( + self._calculate_radius(index), + self._calculate_degree(index), + ) + child.center = pos + + def _calculate_radius(self, index): + """Calculates the radius for given index.""" + + idx = int(index / self._max_per_row()) + + if not self.circular_radius: + init_radius = ( + min([self.width / 2, self.height / 2]) - self.circular_padding + ) + else: + init_radius = self.circular_radius + + if idx != 0: + space = self.row_spacing * idx + init_radius -= space + + return init_radius + + def _calculate_degree(self, index): + """Calculates the angle for given index.""" + + if self.clockwise: + degree = self.start_from - index * self.degree_spacing + else: + degree = self.start_from + index * self.degree_spacing + + return degree + + def _point_on_circle(self, radius, degree): + angle = radians(degree) + center = [self.pos[0] + self.width / 2, self.pos[1] + self.height / 2] + x = center[0] + (radius * cos(angle)) + y = center[1] + (radius * sin(angle)) + return [x, y] + + +if __name__ == "__main__": + from kivy.lang.builder import Builder + from kivy.uix.label import Label + + from kivymd.app import MDApp + + kv = """ +MDScreen: + + MDCircularLayout: + id: container + pos_hint: {"center_x": .5, "center_y": .5} + row_spacing: min(self.size) * 0.1 + """ + + class Main(MDApp): + def build(self): + return Builder.load_string(kv) + + def on_start(self): + for x in range(1, 49): + self.root.ids.container.add_widget( + Label(text=f"{x}", color=[0, 0, 0, 1]) + ) + + Main().run() diff --git a/sbapp/kivymd/uix/controllers/__init__.py b/sbapp/kivymd/uix/controllers/__init__.py new file mode 100644 index 0000000..add3847 --- /dev/null +++ b/sbapp/kivymd/uix/controllers/__init__.py @@ -0,0 +1,11 @@ +""" +Controllers +=========== + +.. versionadded:: 1.0.0 + +Modules and classes that implement useful methods for getting information +about the state of the current application window. +""" + +from .windowcontroller import WindowController diff --git a/sbapp/kivymd/uix/controllers/windowcontroller.py b/sbapp/kivymd/uix/controllers/windowcontroller.py new file mode 100644 index 0000000..9ace0c4 --- /dev/null +++ b/sbapp/kivymd/uix/controllers/windowcontroller.py @@ -0,0 +1,59 @@ +""" +Controllers/WindowController +============================ + +Modules and classes that implement useful methods for getting information +about the state of the current application window. + +Controlling the resizing direction of the application window +------------------------------------------------------------ + +.. code-block:: python + + # When resizing the application window, the direction of change will be + # printed - 'left' or 'right'. + + from kivymd.app import MDApp + from kivymd.uix.controllers import WindowController + from kivymd.uix.screen import MDScreen + + + class MyScreen(MDScreen, WindowController): + def on_width(self, *args): + print(self.get_window_width_resizing_direction()) + + + class Test(MDApp): + def build(self): + return MyScreen() + + + Test().run() +""" + +from kivy.core.window import Window +from kivy.core.window.window_sdl2 import WindowSDL + + +class WindowController: + def __init__(self): + self.window_resizing_direction = "unknown" + self.__width = Window.width + Window.bind(on_resize=self._on_resize) + + def get_window_width_resizing_direction(self) -> str: + """Return window width resizing direction - 'left' or 'right'""" + + return self.window_resizing_direction + + def _set_window_width_resizing_direction(self, width: int) -> None: + if self.__width > width: + self.window_resizing_direction = "left" + elif self.__width < width: + self.window_resizing_direction = "right" + + def _on_resize( + self, window_sdl2: WindowSDL, width: int, height: int + ) -> None: + self._set_window_width_resizing_direction(width) + self.__width = width diff --git a/sbapp/kivymd/uix/datatables/__init__.py b/sbapp/kivymd/uix/datatables/__init__.py new file mode 100644 index 0000000..fc4115d --- /dev/null +++ b/sbapp/kivymd/uix/datatables/__init__.py @@ -0,0 +1 @@ +from .datatables import MDDataTable # NOQA F401 diff --git a/sbapp/kivymd/uix/datatables/datatables.kv b/sbapp/kivymd/uix/datatables/datatables.kv new file mode 100644 index 0000000..188ddde --- /dev/null +++ b/sbapp/kivymd/uix/datatables/datatables.kv @@ -0,0 +1,229 @@ +#:import DEVICE_TYPE kivymd.material_resources.DEVICE_TYPE +#:import FakeRectangularElevationBehavior kivymd.uix.behaviors.FakeRectangularElevationBehavior + + + + orientation: "vertical" + md_bg_color: + ( \ + ( \ + root.theme_cls.bg_darkest \ + if root.theme_cls.theme_style == "Light" else \ + root.theme_cls.bg_light \ + ) \ + if not root.background_color_selected_cell \ + else root.background_color_selected_cell \ + ) \ + if self.selected \ + else \ + ( \ + root.theme_cls.bg_normal \ + if not root.background_color_cell \ + else root.background_color_cell \ + ) + 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 + + MDBoxLayout: + id: inner_box + + MDIcon: + id: icon + size_hint: None, None + pos_hint: {"center_y": 0.5} + size: ("24dp", "24dp") if root.icon else (0, 0) + icon: root.icon if root.icon else "" + theme_text_color: "Custom" + text_color: + root.icon_color if root.icon_color else \ + root.theme_cls.primary_color + + MDLabel: + id: label + text: " " + root.text + markup: True + 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 + + BoxLayout: + id: box + size_hint_y: None + height: lbl.height + + MDLabel: + id: lbl + text: " " + root.text + adaptive_height: True + bold: True + markup: True + color: + (1, 1, 1, 1) \ + if root.theme_cls.theme_style == "Dark" else \ + (0, 0, 0, 1) + + MDSeparator: + id: separator + + + + id: sort_btn + icon: "arrow-up" + pos_hint: {"center_y": 0.5} + size: [dp(24), dp(0)] + theme_text_color: "Custom" + text_color: self.theme_cls.secondary_text_color + opacity: 0 + + + + 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 + md_bg_color: + root.theme_cls.bg_light \ + if not root.background_color_header \ + else root.background_color_header + + 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_release: root.table_data.select_all(self.state) + + CellHeader: + id: first_cell + + MDSeparator: + + + + data: root.recycle_data + data_first_cells: root.data_first_cells + key_viewclass: "viewclass" + + 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 + multiselect: True + touch_multiselect: True + + + + adaptive_height: True + spacing: "8dp" + + MDLabel: + text: "Rows per page" + shorten: True + halign: "right" + 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} + font_size: "14sp" + on_release: root.table_data.open_pagination_menu() + text: + f"{root.table_data.rows_num if root.table_data.rows_num < len(root.table_data.row_data) else len(root.table_data.row_data)}" + + Widget: + size_hint_x: None + width: "32dp" if DEVICE_TYPE != "mobile" else "8dp" + + MDLabel: + id: label_rows_per_page + adaptive_size: True + -text_size: None, None + pos_hint: {"center_y": .5} + font_style: "Caption" + color: + (1, 1, 1, 1) \ + if root.theme_cls.theme_style == "Dark" else \ + (0, 0, 0, 1) + text: + f"1-" \ + f"{root.table_data.rows_num if root.table_data.rows_num > len(root.table_data.row_data) else len(root.table_data.row_data)} " \ + f"of {len(root.table_data.row_data)}" + + MDIconButton: + id: button_back + icon: "chevron-left" + user_font_size: "20sp" if DEVICE_TYPE != "mobile" else "16dp" + ripple_scale: .5 if DEVICE_TYPE == "mobile" else 1 + pos_hint: {'center_y': .5} + disabled: True + md_bg_color_disabled: 0, 0, 0, 0 + on_release: root.table_data.set_next_row_data_parts("back") + + MDIconButton: + id: button_forward + icon: "chevron-right" + user_font_size: "20sp" if DEVICE_TYPE != "mobile" else "16dp" + ripple_scale: .5 if DEVICE_TYPE == "mobile" else 1 + pos_hint: {'center_y': .5} + disabled: True + md_bg_color_disabled: 0, 0, 0, 0 + on_release: root.table_data.set_next_row_data_parts("forward") + + + + + + + + TableContainer: + id: container + orientation: "vertical" + elevation: root.elevation + padding: "24dp", "24dp", "8dp", "8dp" diff --git a/sbapp/kivymd/uix/datatables/datatables.py b/sbapp/kivymd/uix/datatables/datatables.py new file mode 100644 index 0000000..b574d06 --- /dev/null +++ b/sbapp/kivymd/uix/datatables/datatables.py @@ -0,0 +1,1911 @@ +""" +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 + +Warnings +--------- + +.. warning:: Data tables are still far from perfect. The class is in constant + change, because of optimizations and bug fixes. If you find a bug or have + an improvement you want to share, take some time and share your discoveries + with us over the main git repo. + Any help is well appreciated. + +.. warning:: In versions prior to `Kivy 2.1.0-dev0` exists an error in which is + the table has only one row in the current page, the table will only render + one column instead of the whole row. + +.. note:: `MDDataTable` allows developers to sort the data provided by column. + This happens thanks to the use of an external function that you can bind + while you're defining the table columns. Be aware that the sorting function + must return a 2 value list in the format of: + `[Index, Sorted_Row_Data]` + + This is because the index list is needed to allow MDDataTable to keep track + of the selected rows. and, after the data is sorted, update the row + checkboxes. + +""" + +# Special thanks for the info - +# https://stackoverflow.com/questions/50219281/python-how-to-add-vertical-scroll-in-recycleview + +__all__ = ("MDDataTable",) + +import os +from collections import defaultdict +from typing import Union + +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + DictProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.anchorlayout import AnchorLayout +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 import uix_path +from kivymd.effects.stiffscroll import StiffScrollEffect +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import HoverBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.button import MDIconButton +from kivymd.uix.menu import MDDropdownMenu +from kivymd.uix.selectioncontrol import MDCheckbox +from kivymd.uix.tooltip import MDTooltip + +with open( + os.path.join(uix_path, "datatables", "datatables.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +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 CellHeader(MDTooltip, BoxLayout): + """ + Implements the label text in the column header panel from + :attr:`~MDDataTable.column_data` data. + """ + + text = StringProperty() + """ + Column text. + + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + # TODO: Added example. + sort_action = ObjectProperty() + """ + Custom function for sorting. + + :attr:`sort_action` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + table_data = ObjectProperty() + """ + :class:`~TableData` class. + + :attr:`table_data` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + is_sorted = BooleanProperty(False) + sorted_order = StringProperty() + + def __init__(self, *args, **kwargs): + super().__init__(**kwargs) + if self.sort_action: + box = self.ids.box + ib = SortButton() + ib.bind(on_release=self._sort_release) + + if self.is_sorted: + ib.icon = ( + "arrow-down" if self.sorted_order == "ASC" else "arrow-up" + ) + ib.size = [dp(24), dp(24)] + ib.opacity = 1 + else: + self.bind(on_enter=self.set_sort_btn) + self.bind(on_leave=self.set_sort_btn) + + box.add_widget(ib, index=1) + + def restore_checks(self, indices: dict) -> None: + curr_checks = self.table_data.current_selection_check + rows_num = self.table_data.rows_num + columns = self.table_data.total_col_headings + + new_checks = defaultdict(list) + for i, x in enumerate(curr_checks): + for j, y in enumerate(curr_checks[x]): + new_page = (indices[y // columns + x * rows_num]) // rows_num + new_indice = ( + (indices[y // columns + x * rows_num]) % rows_num + ) * columns + new_checks[new_page].append(new_indice) + self.table_data.current_selection_check = dict(new_checks) + + def set_sort_btn(self, instance_cell_header) -> None: + btn = instance_cell_header.ids.box.children[-1] + if btn.opacity: + btn.size = [dp(24), dp(0)] + btn.opacity = 0 + else: + btn.size = [dp(24), dp(24)] + btn.opacity = 1 + + def _sort_release(self, inst): + inst.icon = "arrow-down" if inst.icon == "arrow-up" else "arrow-up" + + if not self.parent.parent._col_with_sort: + c = self.parent.children + col_with_sort = [ + each + for each in c + if each.ids.get("box", None) and len(each.ids.box.children) == 2 + ] + self.parent.parent._col_with_sort = col_with_sort + else: + col_with_sort = self.parent.parent._col_with_sort + + for each in col_with_sort: + if each == self: + self.unbind(on_enter=self.set_sort_btn) + self.unbind(on_leave=self.set_sort_btn) + else: + btn = each.ids.box.children[-1] + btn.size = [dp(24), dp(0)] + btn.opacity = 0 + each.bind(on_enter=each.set_sort_btn) + each.bind(on_leave=each.set_sort_btn) + + if self.sort_action: + if not self.table_data: + th = self.parent.parent + self.table_data = th.table_data + + indices, sorted_data = self.sort_action(self.table_data.row_data) + + if not sorted_data: + return + + if inst.icon == "arrow-down": + sorted_data = sorted_data[::-1] + indices = indices[::-1] + + self.table_data.row_data = sorted_data + self.table_data.on_rows_num(self, self.table_data.rows_num) + self.restore_checks(dict(zip(indices, range(len(indices))))) + self.table_data.set_next_row_data_parts("reset") + self.table_data.cell_row_obj_dict = {} + self.table_data.table_header.ids.check.state = "normal" + + +class TableHeader(ThemableBehavior, ScrollView): + """ + Implements a panel for column heading labels - + :attr:`~MDDataTable.column_data`. + """ + + table_data = ObjectProperty() + """ + Class :class:`~TableData`. + + :attr:`table_data` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + column_data = ListProperty() + """ + See :attr:`~MDDataTable.sorted_on` + + :attr:`column_data` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + sorted_on = StringProperty() + """ + See :attr:`~MDDataTable.sorted_on`. + + :attr:`sorted_on` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + cols_minimum = DictProperty() + """ + See :attr:`~kivy.uix.gridlayout.GridLayout.cols_minimum`. + + :attr:`cols_minimum` is an :class:`~kivy.properties.DictProperty` + and defaults to `{}`. + """ + + sorted_order = StringProperty() + """ + See :attr:`~MDDataTable.sorted_order`. + + :attr:`sorted_order` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + background_color_header = ColorProperty(None) + """ + See :attr:`~MDDataTable.background_color_header`. + + .. versionadded:: 1.0.0 + + :attr:`background_color_header` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _col_with_sort = [] # store cols which contain sort functions + _col_headings = ListProperty() # column names list + + 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], + sort_action=col_heading[2], + width=self.cols_minimum[i], + table_data=self.table_data, + is_sorted=(col_heading[0] == self.sorted_on), + sorted_order=self.sorted_order, + ) + if len(col_heading) == 3 + else CellHeader( + text=col_heading[0], + width=self.cols_minimum[i], + table_data=self.table_data, + ) + ) + ) + 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_table_header, instance_table_data) -> None: + """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 + + +class TableData(RecycleView): + """Implements a list of table data.""" + + recycle_data = ListProperty() + """ + See :attr:`~kivy.uix.recycleview.RecycleView.data`. + + :attr:`recycle_data` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + data_first_cells = ListProperty() + """ + List of first row cells. + + :attr:`data_first_cells` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + row_data = ListProperty() + """ + See :attr:`~MDDataTable.row_data`. + + :attr:`row_data` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + total_col_headings = NumericProperty(0) # TableHeader._col_headings + """ + See :attr:`~TableHeader._col_headings`. + + :attr:`total_col_headings` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + cols_minimum = DictProperty() + """ + See :attr:`~TableHeader.cols_minimum`. + + :attr:`cols_minimum` is an :class:`~kivy.properties.DictProperty` + and defaults to `{}`. + """ + + table_header = ObjectProperty() + """ + :class:`~TableHeader` class. + + :attr:`table_header` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + pagination_menu = ObjectProperty() + """ + :class:`~kivymd.uix.menu.MDDropdownMenu` class. + + :attr:`pagination_menu` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + pagination = ObjectProperty() + """ + :class:`~TablePagination` class. + + :attr:`pagination` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + check = ObjectProperty() + """ + See :attr:`~MDDataTable.check`. + + :attr:`check` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + rows_num = NumericProperty() + """ + Number of rows displayed on the table page. + + :attr:`rows_num` is an :class:`~kivy.properties.NumericProperty` + and defaults to `None`. + """ + + pagination_menu_open = BooleanProperty(False) + """ + Open or close the menu for selecting the number of rows displayed + on the table page. + + :attr:`pagination_menu_open` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + current_selection_check = DictProperty() + """ + List of indexes of marked checkboxes. + + :attr:`current_selection_check` is an :class:`~kivy.properties.DictProperty` + and defaults to `{}`. + """ + + cell_row_obj_dict = {} + + _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() + self.effect_cls = self._parent.effect_cls + Clock.schedule_once(self.set_default_first_row, 0) + + def get_select_row(self, index: int) -> None: + """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) + self._get_row_checks() # update the dict + + def set_default_first_row(self, interval: Union[int, float]) -> None: + """Set default first row as selected.""" + + self.ids.row_controller.select_next(self) + + def set_row_data(self) -> None: + 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[2][0]) + self.recycle_data.append( + { + "text": str(x[0]), + "Index": str(j), + "range": x[2], + "selectable": True, + "viewclass": "CellRow", + "table": self, + "background_color_cell": self._parent.background_color_cell, + "background_color_selected_cell": self._parent.background_color_selected_cell, + } + ) + else: + r_data = { + "Index": str(j), + "range": x[2], + "selectable": True, + "viewclass": "CellRow", + "table": self, + "background_color_cell": self._parent.background_color_cell, + "background_color_selected_cell": self._parent.background_color_selected_cell, + } + + if ( + isinstance(x[0], tuple) or isinstance(x[0], list) + ) and len(x[0]) == 3: + r_data["icon"] = x[0][0] + r_data["icon_color"] = x[0][1] + r_data["text"] = str(x[0][2]) + self.recycle_data.append(r_data) + + elif ( + isinstance(x[0], tuple) or isinstance(x[0], list) + ) and len(x[0]) == 2: + r_data["icon"] = x[0][0] + r_data["text"] = str(x[0][1]) + + self.recycle_data.append(r_data) + + else: + r_data["text"] = str(x[0]) + self.recycle_data.append(r_data) + + 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: str) -> None: + """Sets the text of the numbers of displayed pages in table.""" + + if self.pagination: + if direction == "reset": + self._current_value = 1 + self._to_value = len(self._row_data_parts[self._rows_number]) + elif 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} " + f"of {len(self.row_data)}" + ) + + def select_all(self, state: str) -> None: + """Sets the checkboxes of all rows to the active/inactive position.""" + + for i in range(0, len(self.recycle_data), self.total_col_headings): + cell_row_obj = self.view_adapter.get_visible_view(i) + if cell_row_obj: + self.cell_row_obj_dict[i] = cell_row_obj + self.on_mouse_select(cell_row_obj) + cell_row_obj.ids.check.state = state + + if state == "down": + # select all checks on all pages + rows_num = self.rows_num + columns = self.total_col_headings + full_pages = len(self.row_data) // self.rows_num + left_over_rows = len(self.row_data) % self.rows_num + + new_checks = {} + for page in range(full_pages): + new_checks[page] = list(range(0, rows_num * columns, columns)) + + if left_over_rows: + new_checks[full_pages] = list( + range(0, left_over_rows * columns, columns) + ) + + self.current_selection_check = new_checks + return + + # resets all checks on all pages + self.current_selection_check = {} + + def check_all(self, state: str) -> bool: + """Checks if checkboxes of all rows are in the same state.""" + + tmp = [] + for i in range(0, len(self.recycle_data), self.total_col_headings): + if self.cell_row_obj_dict.get(i, None): + cell_row_obj = self.cell_row_obj_dict[i] + else: + cell_row_obj = self.view_adapter.get_visible_view(i) + if cell_row_obj: + self.cell_row_obj_dict[i] = cell_row_obj + if cell_row_obj: + tmp.append(cell_row_obj.ids.check.state == state) + return all(tmp) + + def close_pagination_menu(self, *args) -> None: + """Called when the pagination menu window is closed.""" + + self.pagination_menu_open = False + + def open_pagination_menu(self) -> None: + """Open pagination menu window.""" + + if self.pagination_menu.items: + self.pagination_menu_open = True + self.pagination_menu.open() + + def set_number_displayed_lines(self, text_item) -> None: + """ + Called when the user sets the number of pages displayed + in the table. + """ + + # self.rows_num = int(text_item) + self.rows_num = int(text_item) + self.set_next_row_data_parts("reset") + self.set_text_from_of("reset") + self.pagination_menu.caller.text = text_item + + def set_next_row_data_parts(self, direction: str) -> None: + """Called when switching the pages of the table.""" + + if direction == "reset": + self._rows_number = 0 + self.pagination.ids.button_back.disabled = True + self.pagination.ids.button_forward.disabled = False + elif 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 on_mouse_select(self, instance_cell_row) -> None: + """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_cell_row.index: + self.ids.row_controller.selected_row = instance_cell_row.index + self.ids.row_controller.select_current(self) + + def on_rows_num(self, instance_table_date, value_rows_num: int) -> None: + if not self._to_value: + self._to_value = value_rows_num + + self._rows_number = 0 + self._row_data_parts = list( + self._split_list_into_equal_parts(self.row_data, value_rows_num) + ) + + def on_pagination( + self, instance_table_date, instance_table_pagination + ) -> None: + if self._to_value < len(self.row_data): + self.pagination.ids.button_forward.disabled = False + + def _split_list_into_equal_parts(self, lst, parts): + for i in range(0, len(lst), parts): + yield lst[i : i + parts] + + def _get_row_checks(self): + """Returns all rows that are checked.""" + + tmp = [] + for i in range(0, len(self.recycle_data), self.total_col_headings): + if self.cell_row_obj_dict.get(i, None): + cell_row_obj = self.cell_row_obj_dict[i] + else: + cell_row_obj = self.view_adapter.get_visible_view(i) + if cell_row_obj: + self.cell_row_obj_dict[i] = cell_row_obj + + if cell_row_obj and cell_row_obj.ids.check.state == "down": + idx = cell_row_obj.index + row = [] + for data in self.recycle_data: + if idx in data["range"]: + row.append(data["text"]) + + tmp.append(row) + return tmp + + # 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:`~TableData` class. + + :attr:`table_data` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + +class MDDataTable(ThemableBehavior, AnchorLayout): + """ + :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 + from kivymd.uix.screen import MDScreen + + + class Example(MDApp): + def build(self): + self.data_tables = MDDataTable( + use_pagination=True, + check=True, + column_data=[ + ("No.", dp(30)), + ("Status", dp(30)), + ("Signal Name", dp(60), self.sort_on_signal), + ("Severity", dp(30)), + ("Stage", dp(30)), + ("Schedule", dp(30), self.sort_on_schedule), + ("Team Lead", dp(30), self.sort_on_team), + ], + row_data=[ + ( + "1", + ("alert", [255 / 256, 165 / 256, 0, 1], "No Signal"), + "Astrid: NE shared managed", + "Medium", + "Triaged", + "0:33", + "Chase Nguyen", + ), + ( + "2", + ("alert-circle", [1, 0, 0, 1], "Offline"), + "Cosmo: prod shared ares", + "Huge", + "Triaged", + "0:39", + "Brie Furman", + ), + ( + "3", + ( + "checkbox-marked-circle", + [39 / 256, 174 / 256, 96 / 256, 1], + "Online", + ), + "Phoenix: prod shared lyra-lists", + "Minor", + "Not Triaged", + "3:12", + "Jeremy lake", + ), + ( + "4", + ( + "checkbox-marked-circle", + [39 / 256, 174 / 256, 96 / 256, 1], + "Online", + ), + "Sirius: NW prod shared locations", + "Negligible", + "Triaged", + "13:18", + "Angelica Howards", + ), + ( + "5", + ( + "checkbox-marked-circle", + [39 / 256, 174 / 256, 96 / 256, 1], + "Online", + ), + "Sirius: prod independent account", + "Negligible", + "Triaged", + "22:06", + "Diane Okuma", + ), + ], + sorted_on="Schedule", + sorted_order="ASC", + elevation=2, + ) + self.data_tables.bind(on_row_press=self.on_row_press) + self.data_tables.bind(on_check_press=self.on_check_press) + screen = MDScreen() + screen.add_widget(self.data_tables) + return screen + + 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) + + # Sorting Methods: + # since the https://github.com/kivymd/KivyMD/pull/914 request, the + # sorting method requires you to sort out the indexes of each data value + # for the support of selections. + # + # The most common method to do this is with the use of the builtin function + # zip and enumerate, see the example below for more info. + # + # The result given by these funcitons must be a list in the format of + # [Indexes, Sorted_Row_Data] + + def sort_on_signal(self, data): + return zip(*sorted(enumerate(data), key=lambda l: l[1][2])) + + def sort_on_schedule(self, data): + return zip( + *sorted( + enumerate(data), + key=lambda l: sum( + [ + int(l[1][-2].split(":")[0]) * 60, + int(l[1][-2].split(":")[1]), + ] + ), + ) + ) + + def sort_on_team(self, data): + return zip(*sorted(enumerate(data), key=lambda l: l[1][-1])) + + + 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 + from kivy.uix.anchorlayout import AnchorLayout + + + class Example(MDApp): + def build(self): + layout = AnchorLayout() + self.data_tables = MDDataTable( + size_hint=(0.7, 0.6), + use_pagination=True, + check=True, + # name column, width column, sorting function column(optional) + column_data=[ + ("No.", dp(30)), + ("Status", dp(30)), + ("Signal Name", dp(60)), + ("Severity", dp(30)), + ("Stage", dp(30)), + ("Schedule", dp(30), lambda *args: print("Sorted using Schedule")), + ("Team Lead", dp(30)), + ], + ) + layout.add_widget(self.data_tables) + return layout + + + 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 `[]`. + + .. note:: The functions which will be called for sorting must accept a data + argument and return the sorted data. Incoming data format will be + similar to the provided row_data except that it'll be all list instead + of tuple like below. Any icon provided initially will also be there in + this data so handle accordingly. + + .. code-block:: python + + [ + [ + "1", + ["icon", "No Signal"], + "Astrid: NE shared managed", + "Medium", + "Triaged", + "0:33", + "Chase Nguyen", + ], + [ + "2", + "Offline", + "Cosmo: prod shared ares", + "Huge", + "Triaged", + "0:39", + "Brie Furman", + ], + [ + "3", + "Online", + "Phoenix: prod shared lyra-lists", + "Minor", + "Not Triaged", + "3:12", + "Jeremy lake", + ], + [ + "4", + "Online", + "Sirius: NW prod shared locations", + "Negligible", + "Triaged", + "13:18", + "Angelica Howards", + ], + [ + "5", + "Online", + "Sirius: prod independent account", + "Negligible", + "Triaged", + "22:06", + "Diane Okuma", + ], + ] + + You must sort inner lists in ascending order and return the sorted data + in the same format. + """ + + row_data = ListProperty() + """ + Data for rows. To add icon in addition to a row data, include a tuple with + This property stores the row data used to display each row in the DataTable + To show an icon inside a column in a row, use the folowing format in the + row's columns. + + Format: + + `("MDicon-name", [icon color in rgba], "Column Value")` + + Example: + + .. code-block:: python + [...] + row_data = [ + + # row 1 + [ + "value 1", + "value 2", + # the third value will have an icon inside the box + ["home", [128/255, 48/255, 76/255, 1], "Offie" ] + ], + + # row 2 + [ + "value 1", + "value 2", + # the third value will have an icon inside the box + ["git", [1, 0.1, 0.1, 1], "Git Repo" ] + ] + ] + + For a more complex example see below. + + .. code-block:: python + + from kivy.metrics import dp + from kivy.uix.anchorlayout import AnchorLayout + + from kivymd.app import MDApp + from kivymd.uix.datatables import MDDataTable + + + class Example(MDApp): + def build(self): + layout = AnchorLayout() + data_tables = MDDataTable( + size_hint=(0.9, 0.6), + column_data=[ + ("Column 1", dp(20)), + ("Column 2", dp(30)), + ("Column 3", dp(50), self.sort_on_col_3), + ("Column 4", dp(30)), + ("Column 5", dp(30)), + ("Column 6", dp(30)), + ("Column 7", dp(30), self.sort_on_col_2), + ], + row_data=[ + # The number of elements must match the length + # of the `column_data` list. + ( + "1", + ("alert", [255 / 256, 165 / 256, 0, 1], "No Signal"), + "Astrid: NE shared managed", + "Medium", + "Triaged", + "0:33", + "Chase Nguyen", + ), + ( + "2", + ("alert-circle", [1, 0, 0, 1], "Offline"), + "Cosmo: prod shared ares", + "Huge", + "Triaged", + "0:39", + "Brie Furman", + ), + ( + "3", + ( + "checkbox-marked-circle", + [39 / 256, 174 / 256, 96 / 256, 1], + "Online", + ), + "Phoenix: prod shared lyra-lists", + "Minor", + "Not Triaged", + "3:12", + "Jeremy lake", + ), + ( + "4", + ( + "checkbox-marked-circle", + [39 / 256, 174 / 256, 96 / 256, 1], + "Online", + ), + "Sirius: NW prod shared locations", + "Negligible", + "Triaged", + "13:18", + "Angelica Howards", + ), + ( + "5", + ( + "checkbox-marked-circle", + [39 / 256, 174 / 256, 96 / 256, 1], + "Online", + ), + "Sirius: prod independent account", + "Negligible", + "Triaged", + "22:06", + "Diane Okuma", + ), + ], + ) + layout.add_widget(data_tables) + return layout + + def sort_on_col_3(self, data): + return zip( + *sorted( + enumerate(data), + key=lambda l: l[1][3] + ) + ) + + def sort_on_col_2(self, data): + return zip( + *sorted( + enumerate(data), + key=lambda l: l[1][-1] + ) + ) + + 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 `[]`. + """ + + sorted_on = StringProperty() + """ + Column name upon which the data is already sorted. + + If the table data is showing an already sorted data then this can be used + to indicate upon which column the data is sorted. + + :attr:`sorted_on` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + sorted_order = OptionProperty("ASC", options=["ASC", "DSC"]) + """ + Order of already sorted data. Must be one of `'ASC'` for ascending or + `'DSC'` for descending order. + + :attr:`sorted_order` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'ASC'`. + """ + + 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 kivy.metrics import dp + from kivy.uix.anchorlayout import AnchorLayout + + from kivymd.app import MDApp + from kivymd.uix.datatables import MDDataTable + + + class Example(MDApp): + def build(self): + layout = AnchorLayout() + 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) + ], + ) + layout.add_widget(data_tables) + return layout + + + 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`. + """ + + elevation = NumericProperty(8) + """ + Table elevation. + + :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` + and defaults to `8`. + """ + + 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( + "top", options=["center", "auto", "top"] + ) + """ + 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 = ColorProperty([0, 0, 0, 0]) + """ + Background color in the format (r, g, b, a). + See :attr:`~kivy.uix.modalview.ModalView.background_color`. + + Use markup strings + ------------------ + + .. code-block:: python + + from kivy.metrics import dp + from kivy.uix.anchorlayout import AnchorLayout + + from kivymd.app import MDApp + from kivymd.uix.datatables import MDDataTable + + + class Example(MDApp): + def build(self): + layout = AnchorLayout() + data_tables = MDDataTable( + size_hint=(0.9, 0.6), + use_pagination=True, + column_data=[ + ("No.", dp(30)), + ("Column 1", dp(30)), + ("[color=#52251B]Column 2[/color]", dp(30)), + ("Column 3", dp(30)), + ("[size=24][color=#C042B8]Column 4[/color][/size]", dp(30)), + ("Column 5", dp(30)), + ], + row_data=[ + ( + f"{i + 1}", + "[color=#297B50]1[/color]", + "[color=#C552A1]2[/color]", + "[color=#6C9331]3[/color]", + "4", + "5", + ) + for i in range(50) + ], + ) + layout.add_widget(data_tables) + return layout + + + Example().run() + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/datatables-use-markup-strings.png + :align: center + + :attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and + defaults to `[0, 0, 0, 0]`. + """ + + background_color_header = ColorProperty(None) + """ + Background color for :class:`~TableHeader` class. + + .. versionadded:: 1.0.0 + + .. code-block:: python + + self.data_tables = MDDataTable( + ..., + background_color_header="#65275d", + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-background-color-header.png + :align: center + + :attr:`background_color_header` is a :class:`~kivy.properties.ColorProperty` and + defaults to `None`. + """ + + background_color_cell = ColorProperty(None) + """ + Background color for :class:`~CellRow` class. + + .. versionadded:: 1.0.0 + + .. code-block:: python + + self.data_tables = MDDataTable( + ..., + background_color_header="#65275d", + background_color_cell="#451938", + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-background-color-cell.png + :align: center + + :attr:`background_color_cell` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + background_color_selected_cell = ColorProperty(None) + """ + Background selected color for :class:`~CellRow` class. + + .. versionadded:: 1.0.0 + + .. code-block:: python + + self.data_tables = MDDataTable( + ..., + background_color_header="#65275d", + background_color_cell="#451938", + background_color_selected_cell="e4514f", + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-background-color-selected-cell.gif + :align: center + + :attr:`background_color_selected_cell` is a :class:`~kivy.properties.ColorProperty` and + defaults to `None`. + """ + + effect_cls = ObjectProperty(StiffScrollEffect) + """ + Effect class. See ``kivy/effects`` package for more information. + + .. versionadded:: 1.0.0 + + :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` + and defaults to :class:`~kivymd.effects.stiffscroll.StiffScrollEffect`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.header = TableHeader( + column_data=self.column_data, + sorted_on=self.sorted_on, + sorted_order=self.sorted_order, + background_color_header=self.background_color_header, + ) + self.table_data = TableData( + self.header, + row_data=self.row_data, + check=self.check, + rows_num=self.rows_num, + _parent=self, + ) + self.register_event_type("on_row_press") + self.register_event_type("on_check_press") + 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) + self.bind(row_data=self.update_row_data) + + def update_row_data(self, instance_data_table, data: list) -> None: + """ + Called when a the widget data must be updated. + + Remember that this is a heavy function. since the whole data set must + be updated. you can get better results calling this metod with in a + coroutine. + """ + + self.table_data.row_data = data + self.row_data = data + self.table_data.on_rows_num(self, self.table_data.rows_num) + # Set cursors to 0. + self.table_data._rows_number = 0 + self.table_data._current_value = 1 + + if len(data) < self.table_data.rows_num: + self.table_data._to_value = len(data) + self.table_data.pagination.ids.button_forward.disabled = True + else: + self.table_data._to_value = self.table_data.rows_num + self.table_data.pagination.ids.button_forward.disabled = False + + self.table_data.set_next_row_data_parts("") + self.pagination.ids.button_back.disabled = True + if self.use_pagination: + Clock.schedule_once(self.create_pagination_menu, 0.5) + + def add_row(self, data: Union[list, tuple]) -> None: + """ + Added new row to common table. + Argument `data` is the row data from the list :attr:`row_data`. + + .. rubric:: Add/remove row + + .. code-block:: python + + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.datatables import MDDataTable + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.floatlayout import MDFloatLayout + from kivymd.uix.button import MDRaisedButton + + + class Example(MDApp): + data_tables = None + + def build(self): + layout = MDFloatLayout() # root layout + # Creating control buttons. + button_box = MDBoxLayout( + pos_hint={"center_x": 0.5}, + adaptive_size=True, + padding="24dp", + spacing="24dp", + ) + + for button_text in ["Add row", "Remove row"]: + button_box.add_widget( + MDRaisedButton( + text=button_text, on_release=self.on_button_press + ) + ) + + # Create a table. + self.data_tables = MDDataTable( + pos_hint={"center_y": 0.5, "center_x": 0.5}, + size_hint=(0.9, 0.6), + use_pagination=False, + column_data=[ + ("No.", dp(30)), + ("Column 1", dp(40)), + ("Column 2", dp(40)), + ("Column 3", dp(40)), + ], + row_data=[("1", "1", "2", "3")], + ) + # Adding a table and buttons to the toot layout. + layout.add_widget(self.data_tables) + layout.add_widget(button_box) + + return layout + + def on_button_press(self, instance_button: MDRaisedButton) -> None: + '''Called when a control button is clicked.''' + + try: + { + "Add row": self.add_row, + "Remove row": self.remove_row, + }[instance_button.text]() + except KeyError: + pass + + def add_row(self) -> None: + last_num_row = int(self.data_tables.row_data[-1][0]) + self.data_tables.add_row((str(last_num_row + 1), "1", "2", "3")) + + def remove_row(self) -> None: + if len(self.data_tables.row_data) > 1: + self.data_tables.remove_row(self.data_tables.row_data[-1]) + + + Example().run() + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-add-remove-row.gif + :align: center + + .. versionadded:: 1.0.0 + """ + + self.row_data.append(data) + + def remove_row(self, data: Union[list, tuple]) -> None: + """ + Removed row from common table. + Argument `data` is the row data from the list :attr:`row_data`. + + See the code in the doc string for the :attr:`add_row` method for more + information. + + .. versionadded:: 1.0.0 + """ + + self.row_data.remove(data) + + def update_row( + self, old_data: Union[list, tuple], new_data: Union[list, tuple] + ) -> None: + """ + Updates a table row. + Argument `old_data/new_data` is the row data from the list :attr:`row_data`. + + .. rubric:: Update row + + .. code-block:: python + + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.datatables import MDDataTable + from kivymd.uix.floatlayout import MDFloatLayout + from kivymd.uix.button import MDRaisedButton + + + class Example(MDApp): + data_tables = None + + def build(self): + layout = MDFloatLayout() + layout.add_widget( + MDRaisedButton( + text="Change 2 row", + pos_hint={"center_x": 0.5}, + on_release=self.update_row, + y=24, + ) + ) + self.data_tables = MDDataTable( + pos_hint={"center_y": 0.5, "center_x": 0.5}, + size_hint=(0.9, 0.6), + use_pagination=False, + column_data=[ + ("No.", dp(30)), + ("Column 1", dp(40)), + ("Column 2", dp(40)), + ("Column 3", dp(40)), + ], + row_data=[(f"{i + 1}", "1", "2", "3") for i in range(3)], + ) + layout.add_widget(self.data_tables) + + return layout + + def update_row(self, instance_button: MDRaisedButton) -> None: + self.data_tables.update_row( + self.data_tables.row_data[1], # old row data + ["2", "A", "B", "C"], # new row data + ) + + + Example().run() + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-change-row.gif + :align: center + + .. versionadded:: 1.0.0 + """ + + for data in self.row_data: + if data == old_data: + index_data = self.row_data.index(data) + self.row_data[index_data] = new_data + break + + def on_row_press(self, instance_cell_row) -> None: + """Called when a table row is clicked.""" + + def on_check_press(self, row_data: list) -> None: + """ + Called when the check box in the table row is checked. + + :param row_data: One of the elements from the :attr:`MDDataTable.row_data` list. + """ + + def get_row_checks(self) -> list: + """Returns all rows that are checked.""" + + return self.table_data._get_row_checks() + + def create_pagination_menu(self, interval: Union[int, float]) -> None: + menu_items = [ + { + "text": f"{i}", + "viewclass": "OneLineListItem", + "height": dp(56), + "on_release": lambda x=f"{i}": self.table_data.set_number_displayed_lines( + x + ), + } + for i in range(self.rows_num, len(self.row_data), 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_dismiss=self.table_data.close_pagination_menu, + ) + self.table_data.pagination_menu = pagination_menu + + def _scroll_with_header(self, instance, value): + self.header.scroll_x = value + + +class CellRow( + ThemableBehavior, + RecycleDataViewBehavior, + HoverBehavior, + ButtonBehavior, + MDBoxLayout, +): + """Implements a data row from :attr:`~MDDataTable.column_data`.""" + + background_color_cell = ColorProperty(None) + """ + See :attr:`~MDDataTable.background_color_cell.`. + + .. versionadded:: 1.0.0 + + :attr:`background_color_cell` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + background_color_selected_cell = ColorProperty(None) + """ + See :attr:`~MDDataTable.background_color_selected_cell.`. + + .. versionadded:: 1.0.0 + + :attr:`background_color_selected_cell` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text = StringProperty() + """ + Row text. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + table = ObjectProperty() + """ + Class class:`~TableData`. + + :attr:`table` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + icon = StringProperty() + """ + Row icon name. + + :attr:`icon` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon_color = ColorProperty(None) + """ + Row icon color. + + :attr:`icon_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + selected = BooleanProperty(False) + selectable = BooleanProperty(True) + index = None + icon_copy = icon + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.ids.check.bind(active=self.select_check) + self.ids.check.bind(active=self.notify_checkbox_click) + + def notify_checkbox_click( + self, instance_check: MDCheckbox, active: bool + ) -> None: + """Called when the table row checkbox is activated/deactivated.""" + + self.table.get_select_row(self.index) + + def refresh_view_attrs( + self, instance_table_data: TableData, index: int, data: dict + ): + """ + 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(instance_table_data, index, data) + + def apply_selection( + self, instance_table_data: TableData, index: int, is_selected: bool + ) -> None: + """Called when list items of table appear on the screen.""" + + self.selected = is_selected + + # Fixes cloning of icons. + ic = instance_table_data.recycle_data[index].get("icon", None) + cell_row_obj = instance_table_data.view_adapter.get_visible_view(index) + + if not ic: + cell_row_obj.icon = "" + else: + cell_row_obj.icon = cell_row_obj.icon_copy + + # Set checkboxes. + if instance_table_data.check: + if self.index in instance_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 ( + instance_table_data._rows_number + in instance_table_data.current_selection_check + ): + for index in instance_table_data.current_selection_check[ + instance_table_data._rows_number + ]: + if ( + self.index + in instance_table_data.current_selection_check[ + instance_table_data._rows_number + ] + ): + self.change_check_state_no_notify("down") + else: + self.change_check_state_no_notify("normal") + else: + self.change_check_state_no_notify("normal") + + def change_check_state_no_notify(self, new_state: str) -> None: + checkbox = self.ids.check + checkbox.unbind(active=self.notify_checkbox_click) + checkbox.state = new_state + checkbox.bind(active=self.notify_checkbox_click) + + def select_check( + self, instance_table_data: MDDataTable, active: bool + ) -> None: + """Called upon activation/deactivation of the checkbox.""" + + if active: + 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) + + 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 on_icon(self, instance_cell_row, name_icon: str) -> None: + self.icon_copy = name_icon + + def on_table( + self, instance_cell_row, instance_table_data: TableData + ) -> None: + """Sets padding/spacing to zero if no checkboxes are used for rows.""" + + if not instance_table_data.check: + self.ids.box.padding = 0 + self.ids.box.spacing = 0 + + def _check_all(self, state): + """Checks if all checkboxes are in same state.""" + + if state == "down" and self.table.check_all(state): + self.table.table_header.ids.check.state = "down" + else: + self.table.table_header.ids.check.state = "normal" + + +class SortButton(MDIconButton): + """Implements a sort button in the :class:`~CellHeader` class.""" diff --git a/sbapp/kivymd/uix/dialog/__init__.py b/sbapp/kivymd/uix/dialog/__init__.py new file mode 100644 index 0000000..40adbe4 --- /dev/null +++ b/sbapp/kivymd/uix/dialog/__init__.py @@ -0,0 +1 @@ +from .dialog import BaseDialog, MDDialog # NOQA F401 diff --git a/sbapp/kivymd/uix/dialog/dialog.kv b/sbapp/kivymd/uix/dialog/dialog.kv new file mode 100644 index 0000000..1f057a7 --- /dev/null +++ b/sbapp/kivymd/uix/dialog/dialog.kv @@ -0,0 +1,87 @@ +#:import images_path kivymd.images_path + + + + background: '{}/transparent.png'.format(images_path) + + canvas.before: + PushMatrix + RoundedRectangle: + pos: self.pos + size: self.size + radius: root.radius + Scale: + origin: self.center + x: root._scale_x + y: root._scale_y + canvas.after: + PopMatrix + + + + + + + + DialogContainer: + id: container + orientation: "vertical" + size_hint_y: None + height: self.minimum_height + elevation: 24 + padding: "24dp", "24dp", "8dp", "8dp" + radius: root.radius + md_bg_color: + root.theme_cls.bg_dark \ + if not root.md_bg_color else root.md_bg_color + + 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" diff --git a/sbapp/kivymd/uix/dialog/dialog.py b/sbapp/kivymd/uix/dialog/dialog.py new file mode 100755 index 0000000..10f4d67 --- /dev/null +++ b/sbapp/kivymd/uix/dialog/dialog.py @@ -0,0 +1,568 @@ +""" +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 = ''' + MDFloatLayout: + + 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", + theme_text_color="Custom", + text_color=self.theme_cls.primary_color, + ), + MDFlatButton( + text="DISCARD", + theme_text_color="Custom", + 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", "BaseDialog") + +import os + +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 ( + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.modalview import ModalView + +from kivymd import uix_path +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 + +with open( + os.path.join(uix_path, "dialog", "dialog.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class BaseDialog(ThemableBehavior, ModalView): + radius = ListProperty([dp(7), dp(7), dp(7), dp(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]`. + """ + + _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", + theme_text_color="Custom", + text_color=self.theme_cls.primary_color, + ), + MDFlatButton( + text="ACCEPT", + theme_text_color="Custom", + 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", + theme_text_color="Custom", + text_color=self.theme_cls.primary_color, + ), + MDFlatButton( + text="ACCEPT", + theme_text_color="Custom", + 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 `''`. + """ + + 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 + + + MDFloatLayout: + + 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" + + + MDFloatLayout: + + 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", + theme_text_color="Custom", + text_color=self.theme_cls.primary_color, + ), + MDFlatButton( + text="OK", + theme_text_color="Custom", + 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 `[]`. + """ + + width_offset = NumericProperty(dp(48)) + """ + Dialog offset from device width. + + :attr:`width_offset` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(48)`. + """ + + 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:: python + + 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" + + + MDFloatLayout: + + 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", + theme_text_color="Custom", + text_color=self.theme_cls.primary_color, + ), + MDFlatButton( + text="OK", + theme_text_color="Custom", + 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 = ColorProperty(None) + """ + Background color in the format (r, g, b, a). + + :attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _scroll_height = NumericProperty("28dp") + _spacer_top = NumericProperty("24dp") + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Window.bind(on_resize=self.update_width) + + if self.size_hint == [1, 1] and ( + DEVICE_TYPE == "desktop" or DEVICE_TYPE == "tablet" + ): + self.size_hint = (None, None) + self.width = min(dp(560), Window.width - self.width_offset) + elif self.size_hint == [1, 1] and DEVICE_TYPE == "mobile": + self.size_hint = (None, None) + self.width = min(dp(280), Window.width - self.width_offset) + + 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_width(self, *args) -> None: + self.width = max( + self.height + self.width_offset, + min( + dp(560) if DEVICE_TYPE != "mobile" else dp(280), + Window.width - self.width_offset, + ), + ) + + def update_height(self, *args) -> None: + self._spacer_top = self.content_cls.height + dp(24) + + def update_items(self, items: list) -> None: + self.ids.box_items.clear_widgets() + self.items = items + self.create_items() + + def on_open(self) -> None: + # TODO: Add scrolling text. + self.height = self.ids.container.height + + def get_normal_height(self) -> float: + 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) -> None: + instance_item.ids._left_container.x = 0 + instance_item._txt_left_pad = "56dp" + + def create_items(self) -> None: + if not self.text: + self.ids.container.remove_widget(self.ids.text) + height = 0 + else: + height = self.ids.text.height + + 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.ids.scroll.height = self.get_normal_height() + else: + self.ids.scroll.height = height + + def create_buttons(self) -> None: + for button in self.buttons: + if issubclass(button.__class__, BaseButton): + self.ids.button_box.add_widget(button) diff --git a/sbapp/kivymd/uix/dropdownitem/__init__.py b/sbapp/kivymd/uix/dropdownitem/__init__.py new file mode 100644 index 0000000..a2ca639 --- /dev/null +++ b/sbapp/kivymd/uix/dropdownitem/__init__.py @@ -0,0 +1 @@ +from .dropdownitem import MDDropDownItem # NOQA F401 diff --git a/sbapp/kivymd/uix/dropdownitem/dropdownitem.kv b/sbapp/kivymd/uix/dropdownitem/dropdownitem.kv new file mode 100644 index 0000000..4e6d163 --- /dev/null +++ b/sbapp/kivymd/uix/dropdownitem/dropdownitem.kv @@ -0,0 +1,37 @@ +<_Triangle>: + canvas: + Color: + rgba: root.theme_cls.text_color + Triangle: + points: + [ \ + self.right-dp(14), self.y+dp(7), \ + self.right-dp(7), self.y+dp(7), \ + self.right-dp(7), self.y+dp(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 + disabled_color: root.theme_cls.disabled_hint_text_color + font_size: root.font_size + + + _Triangle: + size_hint: None, None + size: "20dp", "20dp" + + MDSeparator: diff --git a/sbapp/kivymd/uix/dropdownitem/dropdownitem.py b/sbapp/kivymd/uix/dropdownitem/dropdownitem.py new file mode 100644 index 0000000..e8a98fb --- /dev/null +++ b/sbapp/kivymd/uix/dropdownitem/dropdownitem.py @@ -0,0 +1,105 @@ +""" +Components/DropdownItem +======================= + +.. 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",) + +import os + +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 import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import FakeRectangularElevationBehavior +from kivymd.uix.boxlayout import MDBoxLayout + +with open( + os.path.join(uix_path, "dropdownitem", "dropdownitem.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class _Triangle(ThemableBehavior, Widget): + pass + + +class MDDropDownItem( + ThemableBehavior, + FakeRectangularElevationBehavior, + 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_drop_down_item, text_item: str) -> None: + self.ids.label_item.text = text_item + + def set_item(self, name_item: str) -> None: + """Sets new text for an item.""" + + self.ids.label_item.text = name_item + self.current_item = name_item diff --git a/sbapp/kivymd/uix/expansionpanel/__init__.py b/sbapp/kivymd/uix/expansionpanel/__init__.py new file mode 100644 index 0000000..2f5badd --- /dev/null +++ b/sbapp/kivymd/uix/expansionpanel/__init__.py @@ -0,0 +1,8 @@ +# NOQA F401 +from .expansionpanel import ( + MDExpansionPanel, + MDExpansionPanelLabel, + MDExpansionPanelOneLine, + MDExpansionPanelThreeLine, + MDExpansionPanelTwoLine, +) diff --git a/sbapp/kivymd/uix/expansionpanel/expansionpanel.kv b/sbapp/kivymd/uix/expansionpanel/expansionpanel.kv new file mode 100644 index 0000000..4aadaef --- /dev/null +++ b/sbapp/kivymd/uix/expansionpanel/expansionpanel.kv @@ -0,0 +1,18 @@ +: + icon: "chevron-right" + disabled: True + md_bg_color_disabled: 0, 0, 0, 0 + + canvas.before: + PushMatrix + Rotate: + angle: self._angle + axis: (0, 0, 1) + origin: self.center + canvas.after: + PopMatrix + + + + size_hint_y: None + # height: dp(68) diff --git a/sbapp/kivymd/uix/expansionpanel/expansionpanel.py b/sbapp/kivymd/uix/expansionpanel/expansionpanel.py new file mode 100755 index 0000000..8a10a4a --- /dev/null +++ b/sbapp/kivymd/uix/expansionpanel/expansionpanel.py @@ -0,0 +1,461 @@ +""" +Components/ExpansionPanel +========================= + +.. 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", + "MDExpansionPanelLabel", +) + +import os +from typing import Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp +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 import uix_path +from kivymd.icon_definitions import md_icons +from kivymd.uix.button import MDIconButton +from kivymd.uix.list import ( + IconLeftWidget, + ImageLeftWidget, + IRightBodyTouch, + OneLineAvatarIconListItem, + ThreeLineAvatarIconListItem, + TwoLineAvatarIconListItem, + TwoLineListItem, +) + +with open( + os.path.join(uix_path, "expansionpanel", "expansionpanel.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +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 MDExpansionPanelLabel(TwoLineListItem): + """ + Label panel. + + ..warning:: This class is created for use in the + :class:`~kivymd.uix.stepper.MDStepperVertical` and + :class:`~kivymd.uix.stepper.MDStepper` classes, and has not + been tested for use outside of these classes. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self.set_paddings) + + def set_paddings(self, interval: Union[int, float]) -> None: + self._txt_bot_pad = dp(36) + self._txt_left_pad = dp(0) + + +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`. + """ + + _state = StringProperty("close") + _anim_playing = False + + 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, + MDExpansionPanelLabel, + ), + ): + 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) + ) + if not isinstance(self.panel_cls, MDExpansionPanelLabel): + 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: + self.panel_cls.remove_widget( + self.panel_cls.ids._left_container + ) + self.panel_cls._txt_left_pad = 0 + 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_panel: [ + MDExpansionPanelThreeLine, + MDExpansionPanelTwoLine, + MDExpansionPanelThreeLine, + MDExpansionPanelLabel, + ], + ) -> None: + """ + 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_panel is panel.children[1]: + press_current_panel = True + panel.remove_widget(panel.children[0]) + if not isinstance(self.panel_cls, MDExpansionPanelLabel): + chevron = panel.children[0].children[0].children[0] + self.set_chevron_up(chevron) + self.close_panel(panel, press_current_panel) + self.dispatch("on_close") + break + if not press_current_panel: + self.set_chevron_down() + + def set_chevron_down(self) -> None: + """Sets the chevron down.""" + + if not isinstance(self.panel_cls, MDExpansionPanelLabel): + Animation(_angle=-90, d=self.opening_time).start(self.chevron) + self.open_panel() + self.dispatch("on_open") + + def set_chevron_up(self, instance_chevron: MDExpansionChevronRight) -> None: + """Sets the chevron up.""" + + if not isinstance(self.panel_cls, MDExpansionPanelLabel): + Animation(_angle=0, d=self.closing_time).start(instance_chevron) + + def close_panel( + self, instance_expansion_panel, press_current_panel: bool + ) -> None: + """Method closes the panel.""" + + if self._anim_playing: + return + + if press_current_panel: + self._anim_playing = True + + self._state = "close" + + anim = Animation( + height=self.panel_cls.height, + d=self.closing_time, + t=self.closing_transition, + ) + anim.bind(on_complete=self._disable_anim) + anim.start(instance_expansion_panel) + + def open_panel(self, *args) -> None: + """Method opens a panel.""" + + if self._anim_playing: + return + + self._anim_playing = True + self._state = "open" + + anim = Animation( + height=self.content.height + self.height, + d=self.opening_time, + t=self.opening_transition, + ) + anim.bind(on_complete=self._add_content) + anim.bind(on_complete=self._disable_anim) + anim.start(self) + + def get_state(self) -> str: + """Returns the state of panel. Can be `close` or `open` .""" + + return self._state + + def add_widget(self, widget, index=0, canvas=None): + if isinstance( + widget, + ( + MDExpansionPanelOneLine, + MDExpansionPanelTwoLine, + MDExpansionPanelThreeLine, + MDExpansionPanelLabel, + ), + ): + self.height = widget.height + return super().add_widget(widget) + + def _disable_anim(self, *args): + self._anim_playing = False + + def _add_content(self, *args): + if self.content: + try: + if isinstance(self.panel_cls, MDExpansionPanelLabel): + self.content.y = dp(36) + self.add_widget(self.content) + except WidgetException: + pass diff --git a/sbapp/kivymd/uix/filemanager/__init__.py b/sbapp/kivymd/uix/filemanager/__init__.py new file mode 100644 index 0000000..2ce3c20 --- /dev/null +++ b/sbapp/kivymd/uix/filemanager/__init__.py @@ -0,0 +1 @@ +from .filemanager import MDFileManager # NOQA F401 diff --git a/sbapp/kivymd/uix/filemanager/filemanager.kv b/sbapp/kivymd/uix/filemanager/filemanager.kv new file mode 100644 index 0000000..57ab89b --- /dev/null +++ b/sbapp/kivymd/uix/filemanager/filemanager.kv @@ -0,0 +1,119 @@ +#: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 + + MDBoxLayout: + orientation: "vertical" + spacing: dp(5) + + MDTopAppBar: + 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 + + RecycleGridLayout: + padding: "10dp" + spacing: "2dp" + 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 + + + + + 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) diff --git a/sbapp/kivymd/uix/filemanager/filemanager.py b/sbapp/kivymd/uix/filemanager/filemanager.py new file mode 100755 index 0000000..ab88234 --- /dev/null +++ b/sbapp/kivymd/uix/filemanager/filemanager.py @@ -0,0 +1,635 @@ +""" +Components/FileManager +====================== + +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 + +.. warning:: Be careful! To use the `/` path on Android devices, you need + special permissions. Therefore, you are likely to get an error. + +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 = ''' + MDBoxLayout: + orientation: 'vertical' + + MDTopAppBar: + title: "MDFileManager" + left_action_items: [['menu', lambda x: None]] + elevation: 10 + + MDFloatLayout: + + 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() + +.. versionadded:: 1.0.0 + +Added a feature that allows you to show the available disks first, then the +files contained in them. Works correctly on: `Windows`, `Linux`, `OSX`, `Android`. +Not tested on `iOS`. + +.. code-block:: python + + def file_manager_open(self): + self.file_manager.show_disks() +""" + +__all__ = ("MDFileManager",) + +import locale +import os +import re +from typing import List, Tuple, Union + +from kivy import platform +from kivy.factory import Factory +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + 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, uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import CircularRippleBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.list import BaseListItem, ContainerSupport +from kivymd.uix.relativelayout import MDRelativeLayout +from kivymd.utils.fitimage import FitImage + +with open( + os.path.join(uix_path, "filemanager", "filemanager.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class BodyManager(MDBoxLayout): + """ + Base class for folders and files icons. + """ + + +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 = ColorProperty([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, MDRelativeLayout): + 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`. + """ + + selector = OptionProperty("any", options=["any", "file", "folder", "multi"]) + """ + It can take the values 'any' 'file' 'folder' 'multi' + By default, any. + Available options are: `'any'`, `'file'`, `'folder'`, `'multi'`. + + :attr:`selector` is an :class:`~kivy.properties.OptionProperty` + and defaults to `any`. + """ + + 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" + if ( + self.selector == "any" + or self.selector == "multi" + or self.selector == "folder" + ): + 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"] + self.disks = [] + + def show_disks(self) -> None: + if platform == "win": + self.disks = sorted( + re.findall( + r"[A-Z]+:.*$", + os.popen("mountvol /").read(), + re.MULTILINE, + ) + ) + elif platform in ["linux", "android"]: + self.disks = sorted( + re.findall( + r"on\s(/.*)\stype", + os.popen("mount").read(), + ) + ) + elif platform == "macosx": + self.disks = sorted( + re.findall( + r"on\s(/.*)\s\(", + os.popen("mount").read(), + ) + ) + else: + return + + self.current_path = "" + manager_list = [] + + for disk in self.disks: + access_string = self.get_access_string(disk) + if "r" not in access_string: + icon = "harddisk-remove" + else: + icon = "harddisk" + + manager_list.append( + { + "viewclass": "BodyManager", + "path": disk, + "icon": icon, + "dir_or_file_name": disk, + "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=self.size_hint, 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 show(self, path: str) -> None: + """ + 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=self.size_hint, 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: str) -> str: + 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, + ) -> Union[Tuple[List[str], List[str]], Tuple[None, None]]: + """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) -> None: + """Closes the file manager window.""" + + self._window_manager.dismiss() + self._window_manager_open = False + + def select_dir_or_file( + self, + path: str, + widget: Union[BodyManagerWithPreview, Factory.BodyManager], + ): + """Called by tap on the name of the directory or file.""" + + if os.path.isfile(os.path.join(self.current_path, path)): + if self.selector == "multi": + 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) + elif self.selector == "folder": + return + else: + self.select_path(os.path.join(self.current_path, path)) + + else: + self.current_path = path + self.show(path) + + def back(self) -> None: + """Returning to the branch down in the directory tree.""" + + path, end = os.path.split(self.current_path) + + if self.current_path and path == self.current_path: + self.show_disks() + else: + if not end: + self.close() + self.exit_manager(1) + else: + self.show(path) + + def select_directory_on_press_button(self, *args) -> None: + """Called when a click on a floating button.""" + + if self.selector == "multi": + if len(self.selection) > 0: + self.select_path(self.selection) + else: + if self.selector == "folder" or self.selector == "any": + self.select_path(self.current_path) + + 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 diff --git a/sbapp/kivymd/uix/fitimage/__init__.py b/sbapp/kivymd/uix/fitimage/__init__.py new file mode 100644 index 0000000..04a7ff5 --- /dev/null +++ b/sbapp/kivymd/uix/fitimage/__init__.py @@ -0,0 +1 @@ +from .fitimage import FitImage # NOQA F401 diff --git a/sbapp/kivymd/uix/fitimage/fitimage.py b/sbapp/kivymd/uix/fitimage/fitimage.py new file mode 100644 index 0000000..f3c94ee --- /dev/null +++ b/sbapp/kivymd/uix/fitimage/fitimage.py @@ -0,0 +1,181 @@ +""" +Components/FitImage +================== + +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 + + MDBoxLayout: + 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.properties import BooleanProperty, ObjectProperty +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.image import AsyncImage +from kivy.uix.widget import Widget + +from kivymd.uix.templates import StencilWidget + + +class FitImage(BoxLayout, StencilWidget): + source = ObjectProperty() + """ + Filename/source of your image. + + :attr:`source` is a :class:`~kivy.properties.StringProperty` + and defaults to None. + """ + + mipmap = BooleanProperty(False) + """ + Indicate if you want OpenGL mipmapping to be applied to the texture. + Read :ref:`mipmap` for more information. + + .. versionadded:: 1.0.0 + + :attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + _container = ObjectProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self._late_init) + + def _late_init(self, *args): + self._container = Container(self.source, self.mipmap) + self.bind(source=self._container.setter("source")) + self.add_widget(self._container) + + def reload(self): + self._container.image.reload() + + +class Container(Widget): + source = ObjectProperty() + image = ObjectProperty() + + def __init__(self, source, mipmap, **kwargs): + super().__init__(**kwargs) + self.image = AsyncImage(mipmap=mipmap) + self.loader_clock = Clock.schedule_interval( + self.adjust_size, self.image.anim_delay + ) + self.image.bind( + on_load=lambda inst: ( + self.adjust_size(), + self.loader_clock.cancel(), + ) + ) + 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/sbapp/kivymd/uix/floatlayout.py b/sbapp/kivymd/uix/floatlayout.py new file mode 100644 index 0000000..df67386 --- /dev/null +++ b/sbapp/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/sbapp/kivymd/uix/gridlayout.py b/sbapp/kivymd/uix/gridlayout.py new file mode 100644 index 0000000..ea9df1e --- /dev/null +++ b/sbapp/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 + width: 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/sbapp/kivymd/uix/hero.py b/sbapp/kivymd/uix/hero.py new file mode 100644 index 0000000..34eef37 --- /dev/null +++ b/sbapp/kivymd/uix/hero.py @@ -0,0 +1,507 @@ +""" +Components/Hero +=============== + +.. versionadded:: 1.0.0 + +.. rubric:: Use the :class:`~MDHeroFrom` widget to animate a widget from one + screen to the next. + +- The hero refers to the widget that flies between screens. +- Create a hero animation using KivyMD’s :class:`~MDHeroFrom` widget. +- Fly the hero from one screen to another. +- Animate the transformation of a hero’s shape from circular to rectangular while flying it from one screen to another. +- The :class:`~MDHeroFrom` widget in KivyMD implements a style of animation commonly known as shared element transitions or shared element animations. + +.. raw:: html + +
+ +
+ +The widget that will move from screen A to screen B will be a hero. To move +a widget from one screen to another using hero animation, you need to do the +following: + +- On screen **A**, place the :class:`~MDHeroFrom` container. +- Sets a tag (string) for the :class:`~MDHeroFrom` container. +- Place a hero in the :class:`~MDHeroFrom` container. +- On screen **B**, place the :class:`~MDHeroTo` container - our hero from screen **A **will fly into this container. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hero-base.png + :align: center + +.. warning:: + :class:`~MDHeroFrom` container cannot have more than one child widget. + +Base example +------------ + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreenManager: + + MDScreen: + name: "screen A" + md_bg_color: "lightblue" + + MDHeroFrom: + id: hero_from + tag: "hero" + size_hint: None, None + size: "120dp", "120dp" + pos_hint: {"top": .98} + x: 24 + + FitImage: + source: "https://github.com/kivymd/internal/raw/main/logo/kivymd_logo_blue.png" + size_hint: None, None + size: hero_from.size + + MDRaisedButton: + text: "Move Hero To Screen B" + pos_hint: {"center_x": .5} + y: "36dp" + on_release: + root.current_hero = "hero" + root.current = "screen B" + + MDScreen: + name: "screen B" + hero_to: hero_to + md_bg_color: "cadetblue" + + MDHeroTo: + id: hero_to + size_hint: None, None + size: "220dp", "220dp" + pos_hint: {"center_x": .5, "center_y": .5} + + MDRaisedButton: + text: "Move Hero To Screen A" + pos_hint: {"center_x": .5} + y: "36dp" + on_release: + root.current_hero = "hero" + root.current = "screen A" + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hero-usage.gif + :align: center + + +Note that the child of the :class:`~MDHeroFrom` widget must have the size of the parent: + +.. code-block:: kv + + MDHeroFrom: + id: hero_from + + FitImage: + size_hint: None, None + size: hero_from.size + +To enable hero animation before setting the name of the current screen for the +screen manager, you must specify the name of the tag of the :class:`~MDHeroFrom` +container in which the hero is located: + +.. code-block:: kv + + MDRaisedButton: + text: "Move Hero To Screen B" + on_release: + root.current_hero = "hero" + root.current = "screen 2" + +If you need to switch to a screen that does not contain heroes, set the +`current_hero` attribute for the screen manager as "" (empty string): + +.. code-block:: kv + + MDRaisedButton: + text: "Go To Another Screen" + on_release: + root.current_hero = "" + root.current = "another screen" + +Example +------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreenManager: + + MDScreen: + name: "screen A" + md_bg_color: "lightblue" + + MDHeroFrom: + id: hero_from + tag: "hero" + size_hint: None, None + size: "120dp", "120dp" + pos_hint: {"top": .98} + x: 24 + + FitImage: + source: "https://github.com/kivymd/internal/raw/main/logo/kivymd_logo_blue.png" + size_hint: None, None + size: hero_from.size + + MDRaisedButton: + text: "Move Hero To Screen B" + pos_hint: {"center_x": .5} + y: "36dp" + on_release: + root.current_hero = "hero" + root.current = "screen B" + + MDScreen: + name: "screen B" + hero_to: hero_to + md_bg_color: "cadetblue" + + MDHeroTo: + id: hero_to + size_hint: None, None + size: "220dp", "220dp" + pos_hint: {"center_x": .5, "center_y": .5} + + MDRaisedButton: + text: "Go To Screen C" + pos_hint: {"center_x": .5} + y: "52dp" + on_release: + root.current_hero = "" + root.current = "screen C" + + MDRaisedButton: + text: "Move Hero To Screen A" + pos_hint: {"center_x": .5} + y: "8dp" + on_release: + root.current_hero = "hero" + root.current = "screen A" + + MDScreen: + name: "screen C" + + MDLabel: + text: "Screen C" + halign: "center" + + MDRaisedButton: + text: "Back To Screen B" + pos_hint: {"center_x": .5} + y: "36dp" + on_release: + root.current = "screen B" + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hero-switch-another-screen.gif + :align: center + +Events +------ + +Two events are available for the hero: + +- `on_transform_in` - when the hero flies from screen **A** to screen **B**. +- `on_transform_out` - when the hero back from screen **B** to screen **A**. + +The `on_transform_in`, `on_transform_out` events relate to the +:class:`~MDHeroFrom` container. For example, let's change the radius and +background color of the hero during the flight between the screens: + +.. code-block:: python + + from kivy import utils + from kivy.animation import Animation + from kivy.lang import Builder + from kivy.utils import get_color_from_hex + + from kivymd.app import MDApp + from kivymd.uix.hero import MDHeroFrom + from kivymd.uix.relativelayout import MDRelativeLayout + + KV = ''' + MDScreenManager: + + MDScreen: + name: "screen A" + md_bg_color: "lightblue" + + MyHero: + id: hero_from + tag: "hero" + size_hint: None, None + size: "120dp", "120dp" + pos_hint: {"top": .98} + x: 24 + + MDRelativeLayout: + size_hint: None, None + size: hero_from.size + md_bg_color: "blue" + radius: [24, 12, 24, 12] + + FitImage: + source: "https://github.com/kivymd/internal/raw/main/logo/kivymd_logo_blue.png" + + MDRaisedButton: + text: "Move Hero To Screen B" + pos_hint: {"center_x": .5} + y: "36dp" + on_release: + root.current_hero = "hero" + root.current = "screen B" + + MDScreen: + name: "screen B" + hero_to: hero_to + md_bg_color: "cadetblue" + + MDHeroTo: + id: hero_to + size_hint: None, None + size: "220dp", "220dp" + pos_hint: {"center_x": .5, "center_y": .5} + + MDRaisedButton: + text: "Move Hero To Screen A" + pos_hint: {"center_x": .5} + y: "36dp" + on_release: + root.current_hero = "hero" + root.current = "screen A" + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + class MyHero(MDHeroFrom): + def on_transform_in( + self, instance_hero_widget: MDRelativeLayout, duration: float + ): + ''' + Called when the hero flies from screen **A** to screen **B**. + + :param instance_hero_widget: dhild widget of the `MDHeroFrom` class. + :param duration of the transition animation between screens. + ''' + + Animation( + radius=[12, 24, 12, 24], + duration=duration, + md_bg_color=(0, 1, 1, 1), + ).start(instance_hero_widget) + + def on_transform_out( + self, instance_hero_widget: MDRelativeLayout, duration: float + ): + '''Called when the hero back from screen **B** to screen **A**.''' + + Animation( + radius=[24, 12, 24, 12], + duration=duration, + md_bg_color=get_color_from_hex(utils.hex_colormap["blue"]), + ).start(instance_hero_widget) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hero-events.gif + :align: center + +Usage with ScrollView +--------------------- + +.. code-block:: python + + from kivy.animation import Animation + from kivy.clock import Clock + from kivy.lang import Builder + from kivy.properties import StringProperty, ObjectProperty + + from kivymd.app import MDApp + from kivymd.uix.hero import MDHeroFrom + + KV = ''' + + size_hint_y: None + height: "200dp" + radius: 24 + + MDSmartTile: + id: tile + radius: 24 + box_radius: 0, 0, 24, 24 + box_color: 0, 0, 0, .5 + source: "image.jpg" + size_hint: None, None + size: root.size + mipmap: True + on_release: root.on_release() + + MDLabel: + text: root.tag + bold: True + font_style: "H6" + opposite_colors: True + + + MDScreenManager: + + MDScreen: + name: "screen A" + + ScrollView: + + MDGridLayout: + id: box + cols: 2 + spacing: "12dp" + padding: "12dp" + adaptive_height: True + + MDScreen: + name: "screen B" + hero_to: hero_to + + MDHeroTo: + id: hero_to + size_hint: 1, None + height: "220dp" + pos_hint: {"top": 1} + + MDRaisedButton: + text: "Move Hero To Screen A" + pos_hint: {"center_x": .5} + y: "36dp" + on_release: + root.current_hero = "hero" + root.current = "screen A" + ''' + + + class HeroItem(MDHeroFrom): + text = StringProperty() + manager = ObjectProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.ids.tile.ids.image.ripple_duration_in_fast = 0.05 + + def on_transform_in(self, instance_hero_widget, duration): + Animation( + radius=[0, 0, 0, 0], + box_radius=[0, 0, 0, 0], + duration=duration, + ).start(instance_hero_widget) + + def on_transform_out(self, instance_hero_widget, duration): + Animation( + radius=[24, 24, 24, 24], + box_radius=[0, 0, 24, 24], + duration=duration, + ).start(instance_hero_widget) + + def on_release(self): + def switch_screen(*args): + self.manager.current_hero = self.tag + self.manager.current = "screen B" + + Clock.schedule_once(switch_screen, 0.2) + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for i in range(12): + hero_item = HeroItem( + text=f"Item {i + 1}", tag=f"Tag {i}", manager=self.root + ) + if not i % 2: + hero_item.md_bg_color = "lightgrey" + self.root.ids.box.add_widget(hero_item) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hero-usage-with-scrollview.gif + :align: center +""" + +from kivy.properties import StringProperty + +from kivymd.uix.boxlayout import MDBoxLayout + + +class MDHeroFrom(MDBoxLayout): + """ + The container from which the hero begins his flight. + + :Events: + `on_transform_in` + when the hero flies from screen **A** to screen **B**. + `on_transform_out` + Called when the hero back from screen **B** to screen **A**. + """ + + tag = StringProperty(allownone=True) + """ + Tag ID for heroes. + + :attr:`shift_right` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_transform_in") + self.register_event_type("on_transform_out") + + def on_transform_in(self, *args): + """Called when the hero flies from screen **A** to screen **B**.""" + + def on_transform_out(self, *args): + """Called when the hero back from screen **B** to screen **A**.""" + + +class MDHeroTo(MDBoxLayout): + """The container in which the hero comes.""" diff --git a/sbapp/kivymd/uix/imagelist/__init__.py b/sbapp/kivymd/uix/imagelist/__init__.py new file mode 100644 index 0000000..f8ba305 --- /dev/null +++ b/sbapp/kivymd/uix/imagelist/__init__.py @@ -0,0 +1,2 @@ +# NOQA F401 +from .imagelist import MDSmartTile, SmartTileWithLabel, SmartTileWithStar diff --git a/sbapp/kivymd/uix/imagelist/imagelist.kv b/sbapp/kivymd/uix/imagelist/imagelist.kv new file mode 100644 index 0000000..8cb474c --- /dev/null +++ b/sbapp/kivymd/uix/imagelist/imagelist.kv @@ -0,0 +1,27 @@ + + + SmartTileImage: + id: image + mipmap: root.mipmap + source: root.source + radius: root.radius if root.radius else [0, 0, 0, 0] + size_hint_y: 1 if root.overlap else None + height: root.height if root.overlap else root.height - box.height + pos: + ((0, 0) if root.overlap else (0, box.height)) \ + if root.box_position == "footer" else \ + (0, 0) + on_release: root.dispatch("on_release") + on_press: root.dispatch("on_press") + + SmartTileOverlayBox: + id: box + md_bg_color: root.box_color + size_hint_y: None + padding: "8dp" + radius: root.box_radius + height: "68dp" if root.lines == 2 else "48dp" + pos: + (0, 0) \ + if root.box_position == "footer" else \ + (0, root.height - self.height) diff --git a/sbapp/kivymd/uix/imagelist/imagelist.py b/sbapp/kivymd/uix/imagelist/imagelist.py new file mode 100755 index 0000000..b858404 --- /dev/null +++ b/sbapp/kivymd/uix/imagelist/imagelist.py @@ -0,0 +1,302 @@ +""" +Components/ImageList +==================== + +.. 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: + +Usage +----- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDSmartTile: + radius: 24 + box_radius: [0, 0, 24, 24] + box_color: 1, 1, 1, .2 + source: "cats.jpg" + pos_hint: {"center_x": .5, "center_y": .5} + size_hint: None, None + size: "320dp", "320dp" + + MDIconButton: + icon: "heart-outline" + theme_icon_color: "Custom" + icon_color: 1, 0, 0, 1 + pos_hint: {"center_y": .5} + on_release: self.icon = "heart" if self.icon == "heart-outline" else "heart-outline" + + MDLabel: + text: "Julia and Julie" + bold: True + color: 1, 1, 1, 1 + ''' + + + class MyApp(MDApp): + def build(self): + return Builder.load_string(KV) + + + MyApp().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-smart-tile-usage.gif + :align: center + +Implementation +-------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-smart-tile-usage-sceleton.png + :align: center +""" + +__all__ = ("MDSmartTile", "SmartTileWithLabel", "SmartTileWithStar") + +import os + +from kivy.lang import Builder +from kivy.logger import Logger +from kivy.properties import ( + BooleanProperty, + ColorProperty, + OptionProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.behaviors import ButtonBehavior + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import RectangularRippleBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.fitimage import FitImage +from kivymd.uix.label import MDLabel +from kivymd.uix.relativelayout import MDRelativeLayout + +with open( + os.path.join(uix_path, "imagelist", "imagelist.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class SmartTileImage(RectangularRippleBehavior, ButtonBehavior, FitImage): + """Implements the tile image.""" + + +class SmartTileOverlayBox(MDBoxLayout): + """Implements a container for custom widgets to be added to the tile.""" + + +class MDSmartTile(ThemableBehavior, MDRelativeLayout): + """ + 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. + + :Events: + `on_press` + Called when the button is pressed. + `on_release` + Called when the button is released (i.e. the touch/click that + pressed the button goes away). + """ + + box_radius = VariableListProperty([0], length=4) + """ + Box radius. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDSmartTile: + radius: 24 + box_radius: [0, 0, 24, 24] + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-smart-tile-box-radius.png + :align: center + + :attr:`box_radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + box_color = ColorProperty((0, 0, 0, 0.5)) + """ + Sets the color and opacity for the information box. + + .. code-block:: kv + + MDSmartTile: + radius: 24 + box_radius: [0, 0, 24, 24] + box_color: 0, 1, 0, .5 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-smart-tile-box-color.png + :align: center + + :attr:`box_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `(0, 0, 0, 0.5)`. + """ + + box_position = OptionProperty("footer", options=["footer", "header"]) + """ + Determines weather the information box acts as a header or footer to the + image. Available are options: `'footer'`, `'header'`. + + .. code-block:: kv + + MDSmartTile: + radius: 24 + box_radius: [24, 24, 0, 0] + box_position: "header" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-smart-tile-box-position.png + :align: center + + :attr:`box_position` is a :class:`~kivy.properties.OptionProperty` + and defaults to `'footer'`. + """ + + overlap = BooleanProperty(True) + """ + Determines if the `header/footer` overlaps on top of the image or not. + + .. code-block:: kv + + MDSmartTile: + radius: [24, 24, 0, 0] + box_radius: [0, 0, 24, 24] + overlap: False + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-smart-tile-overlap.png + :align: center + + :attr:`overlap` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + 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`. + This parameter just increases the height of the container for custom + elements. + + .. code-block:: kv + + MDSmartTile: + radius: 24 + box_radius: [0, 0, 24, 24] + lines: 2 + source: "cats.jpg" + pos_hint: {"center_x": .5, "center_y": .5} + size_hint: None, None + size: "320dp", "320dp" + + MDIconButton: + icon: "heart-outline" + theme_icon_color: "Custom" + icon_color: 1, 0, 0, 1 + pos_hint: {"center_y": .5} + on_release: self.icon = "heart" if self.icon == "heart-outline" else "heart-outline" + + TwoLineListItem: + text: "[color=#ffffff][b]My cats[/b][/color]" + secondary_text: "[color=#808080][b]Julia and Julie[/b][/color]" + pos_hint: {"center_y": .5} + _no_ripple_effect: True + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-smart-tile-lines.png + :align: center + + :attr:`lines` is a :class:`~kivy.properties.OptionProperty` + and defaults to `1`. + """ + + source = StringProperty() + """ + Path to tile image. See :attr:`~kivy.uix.image.Image.source`. + + :attr:`source` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + mipmap = BooleanProperty(False) + """ + Indicate if you want OpenGL mipmapping to be applied to the texture. + Read :ref:`mipmap` for more information. + + .. versionadded:: 1.0.0 + + :attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_release") + self.register_event_type("on_press") + + def on_release(self, *args): + """ + Called when the button is released (i.e. the touch/click that + pressed the button goes away). + """ + + def on_press(self, *args): + """Called when the button is pressed.""" + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, (SmartTileImage, SmartTileOverlayBox)): + return super().add_widget(widget, *args, **kwargs) + else: + if isinstance(widget, MDLabel): + widget.shorten = True + widget.shorten_from = "right" + self.ids.box.add_widget(widget) + + +class SmartTileWithLabel(MDSmartTile): + """ + .. deprecated:: 1.0.0 + Use :class:`~MDSmartTile` class instead. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Logger.warning( + "KivyMD: " + "The `SmartTileWithLabel` class has been deprecated. " + "Use the `MDSmartTile` class instead`" + ) + + +class SmartTileWithStar(MDSmartTile): + """ + .. deprecated:: 1.0.0 + Use :class:`~MDSmartTile` class instead. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Logger.warning( + "KivyMD: " + "The `SmartTileWithStar` class has been deprecated. " + "Use the `MDSmartTile` class instead`" + ) diff --git a/sbapp/kivymd/uix/label/__init__.py b/sbapp/kivymd/uix/label/__init__.py new file mode 100644 index 0000000..d13eb3a --- /dev/null +++ b/sbapp/kivymd/uix/label/__init__.py @@ -0,0 +1 @@ +from .label import MDIcon, MDLabel # NOQA F401 diff --git a/sbapp/kivymd/uix/label/label.kv b/sbapp/kivymd/uix/label/label.kv new file mode 100644 index 0000000..b974336 --- /dev/null +++ b/sbapp/kivymd/uix/label/label.kv @@ -0,0 +1,61 @@ +#:import md_icons kivymd.icon_definitions.md_icons + + + + disabled_color: self.theme_cls.disabled_hint_text_color + # FIXME: Overriding the values of this property greatly affects application + # performance. Especially when the application window is resized and a + # custom font is used. Performance is especially slow when you are using + # `PIL` as your text processing provider - os.environ ['KIVY_TEXT'] = 'pil'. + # Priority - CRITICAL. + text_size: self.width, None + + +: + 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 + + font_style: "Icon" + text: u"{}".format(md_icons[root.icon]) if root.icon in md_icons else "blank" + source: None if root.icon in md_icons else root.icon + + # Badge icon. + MDLabel: + font_style: "Icon" + adaptive_size: True + opposite_icon_color: True + color: root.badge_icon_color + text: + u"{}".format(md_icons[root.badge_icon]) \ + if root.badge_icon in md_icons else \ + "" + pos: + root.x + root.width / 2 + self.width / 2 - dp(6), \ + root.y + self.texture_size[1] / 2 + dp(6) + font_size: + ( \ + root.font_size / 1.5 \ + if not root.badge_font_size else \ + root.badge_font_size \ + ) \ + if root.badge_icon and root.badge_icon != "blank" else 0 + + canvas.before: + Color: + rgba: + ( \ + root.badge_bg_color \ + if root.badge_bg_color else \ + app.theme_cls.error_color \ + ) \ + if root.badge_icon else \ + (0, 0, 0, 0) + RoundedRectangle: + radius: [self.width / 2,] + pos: self.pos + size: self.size diff --git a/sbapp/kivymd/uix/label/label.py b/sbapp/kivymd/uix/label/label.py new file mode 100755 index 0000000..da164f7 --- /dev/null +++ b/sbapp/kivymd/uix/label/label.py @@ -0,0 +1,465 @@ +""" +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 = ''' + MDScreen: + + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + 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 = ''' + MDScreen: + + MDBoxLayout: + id: box + orientation: "vertical" + + MDTopAppBar: + 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 = ''' + MDScreen: + + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + 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: + icon: "gmail" + pos_hint: {"center_x": .5, "center_y": .5} + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon.png + :align: center + +MDIcon with badge icon +---------------------- + +.. code-block:: kv + + MDIcon: + icon: "gmail" + badge_icon: "numeric-10" + pos_hint: {"center_x": .5, "center_y": .5} + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-icon-badge.png + :align: center +""" + +__all__ = ("MDLabel", "MDIcon") + +import os +from typing import Union + +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import sp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + ColorProperty, + NumericProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.label import Label + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.theming_dynamic_text import get_contrast_text_color +from kivymd.uix import MDAdaptiveWidget +from kivymd.uix.floatlayout import MDFloatLayout + +__MDLabel_colors__ = { + "Primary": "text_color", + "Secondary": "secondary_text_color", + "Hint": "disabled_hint_text_color", + "Error": "error_color", + "OP": { + "Primary": "opposite_text_color", + "Secondary": "opposite_secondary_text_color", + "Hint": "opposite_disabled_hint_text_color", + }, +} + +with open( + os.path.join(uix_path, "label", "label.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDLabel(ThemableBehavior, Label, MDAdaptiveWidget): + 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 = ColorProperty(None) + """ + Label text color in (r, g, b, a) format. + + :attr:`text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _text_color_str = StringProperty() + + parent_background = ColorProperty(None) + 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(None, "") + self.on_opposite_colors(None, self.opposite_colors) + Clock.schedule_once(self.check_font_styles) + self.theme_cls.bind(theme_style=self._do_update_theme_color) + + def check_font_styles(self, interval: Union[int, float] = 0) -> bool: + 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, instance_label, font_style: str) -> None: + 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_label, theme_text_color: str + ) -> None: + op = self.opposite_colors + if op: + self._text_color_str = __MDLabel_colors__.get("OP", "").get( + theme_text_color, "" + ) + else: + self._text_color_str = __MDLabel_colors__.get(theme_text_color, "") + if self._text_color_str: + self._do_update_theme_color() + else: + # 'Custom' and 'ContrastParentBackground' lead here, as well as the + # generic None value it's not yet been set + self._text_color_str = "" + if theme_text_color == "Custom" and self.text_color: + self.color = self.text_color + elif ( + theme_text_color == "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, instance_label, color: list) -> None: + if self.theme_text_color == "Custom": + self.color = self.text_color + + def on_opposite_colors(self, *args) -> None: + self.on_theme_text_color(self, self.theme_text_color) + + def _do_update_theme_color(self, *args): + if self._text_color_str: + self.color = getattr(self.theme_cls, self._text_color_str) + if not self.disabled: + self.color = getattr(self.theme_cls, self._text_color_str) + else: + self.color = getattr(self.theme_cls, "disabled_hint_text_color") + + +class MDIcon(MDFloatLayout, MDLabel): + icon = StringProperty("android") + """ + Label icon name. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'android'`. + """ + + badge_icon = StringProperty() + """ + Label badge icon name. + + .. versionadded:: 1.0.0 + + :attr:`badge_icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + badge_icon_color = ColorProperty([1, 1, 1, 1]) + """ + Badge icon color in (r, g, b, a) format. + + .. versionadded:: 1.0.0 + + :attr:`badge_icon_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + badge_bg_color = ColorProperty(None) + """ + Badge icon background color in (r, g, b, a) format. + + .. versionadded:: 1.0.0 + + :attr:`badge_bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + badge_font_size = NumericProperty() + """ + Badge font size. + + .. versionadded:: 1.0.0 + + :attr:`badge_font_size` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + source = StringProperty(None, allownone=True) + """ + Path to icon. + + :attr:`source` is an :class:`~kivy.properties.StringProperty` + and defaults to `None`. + """ + + def __init__(self, **kwargs): + from kivymd.uix.selectioncontrol import MDCheckbox + + super().__init__(**kwargs) + if not isinstance(self, MDCheckbox): + self.size_hint = (None, None) + self.size = self.texture_size + self.adaptive_size = True diff --git a/sbapp/kivymd/uix/list/__init__.py b/sbapp/kivymd/uix/list/__init__.py new file mode 100644 index 0000000..b61f9d8 --- /dev/null +++ b/sbapp/kivymd/uix/list/__init__.py @@ -0,0 +1,34 @@ +# NOQA F401 +from .list import ( + BaseListItem, + CheckboxLeftWidget, + ContainerSupport, + IconLeftWidget, + IconLeftWidgetWithoutTouch, + IconRightWidget, + IconRightWidgetWithoutTouch, + ILeftBody, + ILeftBodyTouch, + ImageLeftWidget, + ImageLeftWidgetWithoutTouch, + ImageRightWidget, + ImageRightWidgetWithoutTouch, + IRightBody, + IRightBodyTouch, + MDList, + OneLineAvatarIconListItem, + OneLineAvatarListItem, + OneLineIconListItem, + OneLineListItem, + OneLineRightIconListItem, + ThreeLineAvatarIconListItem, + ThreeLineAvatarListItem, + ThreeLineIconListItem, + ThreeLineListItem, + ThreeLineRightIconListItem, + TwoLineAvatarIconListItem, + TwoLineAvatarListItem, + TwoLineIconListItem, + TwoLineListItem, + TwoLineRightIconListItem, +) diff --git a/sbapp/kivymd/uix/list/list.kv b/sbapp/kivymd/uix/list/list.kv new file mode 100644 index 0000000..467d1c8 --- /dev/null +++ b/sbapp/kivymd/uix/list/list.kv @@ -0,0 +1,173 @@ +#: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) \ + ) \ + if not root.divider_color \ + else \ + root.divider_color + + 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) + RoundedRectangle: + pos: self.pos + size: self.size + radius: root.radius + + 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) diff --git a/sbapp/kivymd/uix/list/list.py b/sbapp/kivymd/uix/list/list.py new file mode 100755 index 0000000..75591e4 --- /dev/null +++ b/sbapp/kivymd/uix/list/list.py @@ -0,0 +1,985 @@ +""" +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 + + OneLineIconListItem: + 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: + + + MDBoxLayout: + + 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" + + YourContainer: + id: container + + MDIconButton: + icon: "minus" + + MDIconButton: + icon: "plus" + ''' + + + class YourContainer(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 + +Behavior +-------- + +When using the `AvatarListItem` and `IconListItem` classes, when an icon is clicked, +the event of this icon is triggered: + +.. code-block:: kv + + OneLineIconListItem: + text: "Single-line item with icon" + + IconLeftWidget: + icon: "language-python" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/list-icon-trigger.gif + :align: center + +You can disable the icon event using the `WithoutTouch` classes: + +.. code-block:: kv + + OneLineIconListItem: + text: "Single-line item with icon" + + IconLeftWidgetWithoutTouch: + icon: "language-python" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/list-icon-without-trigger.gif + :align: center +""" + +__all__ = ( + "BaseListItem", + "MDList", + "ILeftBodyTouch", + "IRightBodyTouch", + "OneLineListItem", + "TwoLineListItem", + "ThreeLineListItem", + "OneLineAvatarListItem", + "TwoLineAvatarListItem", + "ThreeLineAvatarListItem", + "OneLineIconListItem", + "TwoLineIconListItem", + "ThreeLineIconListItem", + "OneLineRightIconListItem", + "TwoLineRightIconListItem", + "ThreeLineRightIconListItem", + "OneLineAvatarIconListItem", + "TwoLineAvatarIconListItem", + "ThreeLineAvatarIconListItem", + "ImageLeftWidget", + "ImageRightWidget", + "IconRightWidget", + "IconLeftWidget", + "CheckboxLeftWidget", + "IconLeftWidgetWithoutTouch", + "IconRightWidgetWithoutTouch", + "ImageRightWidgetWithoutTouch", + "ImageLeftWidgetWithoutTouch", +) + +import os + +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.behaviors import ButtonBehavior + +import kivymd.material_resources as m_res +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + CircularRippleBehavior, + RectangularRippleBehavior, +) +from kivymd.uix.button import MDIconButton +from kivymd.uix.floatlayout import MDFloatLayout +from kivymd.uix.gridlayout import MDGridLayout +from kivymd.uix.selectioncontrol import MDCheckbox +from kivymd.utils.fitimage import FitImage + +with open( + os.path.join(uix_path, "list", "list.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +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, MDFloatLayout +): + """ + 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 = ColorProperty(None) + """ + Text color in ``rgba`` format used if :attr:`~theme_text_color` is set + to `'Custom'`. + + :attr:`text_color` is a :class:`~kivy.properties.ColorProperty` + 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 = ColorProperty(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.ColorProperty` + and defaults to `None`. + """ + + tertiary_text_color = ColorProperty(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.ColorProperty` + 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'`. + """ + + divider_color = ColorProperty(None) + """ + Divider color. + + .. versionadded:: 1.0.0 + + :attr:`divider_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + bg_color = ColorProperty(None) + """ + Background color for menu item. + + :attr:`bg_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _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. + """ + + +class ILeftBodyTouch: + """ + Same as :class:`~ILeftBody`, but allows the widget to receive touch + events instead of triggering the ListItem's ripple effect. + """ + + +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. + """ + + +class IRightBodyTouch: + """ + Same as :class:`~IRightBody`, but allows the widget to receive touch + events instead of triggering the ``ListItem``'s ripple effect + """ + + +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 TouchBehavior: + def on_release(self): + if issubclass(self.parent.parent.__class__, BaseListItem): + self.parent.parent.dispatch("on_release") + + +class ImageLeftWidget( + CircularRippleBehavior, ButtonBehavior, ILeftBodyTouch, FitImage +): + pass + + +class ImageLeftWidgetWithoutTouch( + CircularRippleBehavior, TouchBehavior, ButtonBehavior, ILeftBody, FitImage +): + """ + .. versionadded:: 1.0.0 + """ + + _no_ripple_effect = True + + +class ImageRightWidget( + CircularRippleBehavior, ButtonBehavior, IRightBodyTouch, FitImage +): + pass + + +class ImageRightWidgetWithoutTouch( + CircularRippleBehavior, TouchBehavior, ButtonBehavior, IRightBody, FitImage +): + """ + .. versionadded:: 1.0.0 + """ + + _no_ripple_effect = True + + +class IconRightWidget(IRightBodyTouch, MDIconButton): + pos_hint = {"center_y": 0.5} + + +class IconRightWidgetWithoutTouch(TouchBehavior, IRightBody, MDIconButton): + """ + .. versionadded:: 1.0.0 + """ + + pos_hint = {"center_y": 0.5} + _no_ripple_effect = True + + +class IconLeftWidget(ILeftBodyTouch, MDIconButton): + pos_hint = {"center_y": 0.5} + + +class IconLeftWidgetWithoutTouch(TouchBehavior, ILeftBody, MDIconButton): + """ + .. versionadded:: 1.0.0 + """ + + pos_hint = {"center_y": 0.5} + _no_ripple_effect = True + + +class CheckboxLeftWidget(ILeftBodyTouch, MDCheckbox): + pass diff --git a/sbapp/kivymd/uix/menu/__init__.py b/sbapp/kivymd/uix/menu/__init__.py new file mode 100644 index 0000000..a9c77a9 --- /dev/null +++ b/sbapp/kivymd/uix/menu/__init__.py @@ -0,0 +1 @@ +from .menu import MDDropdownMenu # NOQA F401 diff --git a/sbapp/kivymd/uix/menu/menu.kv b/sbapp/kivymd/uix/menu/menu.kv new file mode 100644 index 0000000..d4ac30e --- /dev/null +++ b/sbapp/kivymd/uix/menu/menu.kv @@ -0,0 +1,59 @@ +#: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 + key_viewclass: "viewclass" + key_size: "height" + + RecycleBoxLayout: + padding: 0, "4dp", 0, "4dp" + default_size: None, dp(48) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: "vertical" + + + + + + + + MenuContainer: + id: card + orientation: "vertical" + elevation: root.elevation + size_hint: None, None + size: md_menu.size[0], md_menu.size[1] + content_header.height + pos: md_menu.pos + opacity: md_menu.opacity + radius: root.radius + md_bg_color: + root.background_color \ + if root.background_color else root.theme_cls.bg_dark + + MDBoxLayout: + id: content_header + adaptive_size: True + + MDMenu: + id: md_menu + drop_cls: root + width_mult: root.width_mult + size_hint: None, None + size: 0, 0 + opacity: 0 diff --git a/sbapp/kivymd/uix/menu/menu.py b/sbapp/kivymd/uix/menu/menu.py new file mode 100755 index 0000000..ebe1461 --- /dev/null +++ b/sbapp/kivymd/uix/menu/menu.py @@ -0,0 +1,1134 @@ +""" +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 = ''' + MDScreen: + + 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}", + "viewclass": "OneLineListItem", + "on_release": lambda x=f"Item {i}": self.menu_callback(x), + } for i in range(5) + ] + self.menu = MDDropdownMenu( + caller=self.screen.ids.button, + items=menu_items, + width_mult=4, + ) + + def menu_callback(self, text_item): + print(text_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() + +Customization of menu item +-------------------------- + +Menu items are created in the same way as items for the +:class:`~kivy.uix.recycleview.RecycleView` class. + +.. code-block:: python + + from kivy.lang import Builder + from kivy.metrics import dp + from kivy.properties import StringProperty + + from kivymd.app import MDApp + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.list import IRightBodyTouch, OneLineAvatarIconListItem + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + + disabled: True + adaptive_size: True + pos_hint: {"center_y": .5} + + MDIconButton: + icon: root.icon + user_font_size: "16sp" + md_bg_color_disabled: 0, 0, 0, 0 + + MDLabel: + text: root.text + font_style: "Caption" + adaptive_size: True + pos_hint: {"center_y": .5} + + + + + IconLeftWidget: + icon: root.left_icon + + RightContentCls: + id: container + icon: root.right_icon + text: root.right_text + + + MDScreen: + + MDRaisedButton: + id: button + text: "PRESS ME" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.menu.open() + ''' + + + class RightContentCls(IRightBodyTouch, MDBoxLayout): + icon = StringProperty() + text = StringProperty() + + + class Item(OneLineAvatarIconListItem): + left_icon = StringProperty() + right_icon = StringProperty() + right_text = StringProperty() + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [ + { + "text": f"Item {i}", + "right_text": f"R+{i}", + "right_icon": "apple-keyboard-command", + "left_icon": "git", + "viewclass": "Item", + "height": dp(54), + "on_release": lambda x=f"Item {i}": self.menu_callback(x), + } for i in range(5) + ] + self.menu = MDDropdownMenu( + caller=self.screen.ids.button, + items=menu_items, + width_mult=4, + ) + + def menu_callback(self, text_item): + print(text_item) + + def build(self): + return self.screen + + + Test().run() + + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-right.gif + :align: center + +.. Header: +Header +------ + +.. code-block:: python + + from kivy.lang import Builder + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + from kivymd.uix.boxlayout import MDBoxLayout + + KV = ''' + + orientation: "vertical" + adaptive_size: True + padding: "4dp" + + MDBoxLayout: + spacing: "12dp" + adaptive_size: True + + MDIconButton: + icon: "gesture-tap-button" + pos_hint: {"center_y": .5} + + MDLabel: + text: "Actions" + adaptive_size: True + pos_hint: {"center_y": .5} + + + MDScreen: + + MDRaisedButton: + id: button + text: "PRESS ME" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.menu.open() + ''' + + + class MenuHeader(MDBoxLayout): + '''An instance of the class that will be added to the menu header.''' + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [ + { + "text": f"Item {i}", + "viewclass": "OneLineListItem", + "height": dp(56), + "on_release": lambda x=f"Item {i}": self.menu_callback(x), + } for i in range(5) + ] + self.menu = MDDropdownMenu( + header_cls=MenuHeader(), + caller=self.screen.ids.button, + items=menu_items, + width_mult=4, + ) + + def menu_callback(self, text_item): + print(text_item) + + def build(self): + return self.screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-header.png + :align: center + +Menu with MDTopAppBar +--------------------- + +The :class:`~MDDropdownMenu` works well with the standard +:class:`~kivymd.uix.toolbar.MDTopAppBar`. Since the buttons on the Toolbar are created +by the MDTopAppBar component, it is necessary to pass the button as an argument to +the callback using `lambda x: app.callback(x)`. + +.. note:: This example uses drop down menus for both the righthand and + lefthand menus (i.e both the 'triple bar' and 'triple dot' menus) to + illustrate that it is possible. A better solution for the 'triple bar' menu + would probably have been :class:`~kivymd.uix.MDNavigationDrawer`. + + +.. code-block:: python + + from kivy.lang import Builder + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + from kivymd.uix.snackbar import Snackbar + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "MDTopAppBar" + left_action_items: [["menu", lambda x: app.callback(x)]] + right_action_items: [["dots-vertical", lambda x: app.callback(x)]] + + MDLabel: + text: "Content" + halign: "center" + ''' + + + class Test(MDApp): + def build(self): + menu_items = [ + { + "viewclass": "OneLineListItem", + "text": f"Item {i}", + "height": dp(56), + "on_release": lambda x=f"Item {i}": self.menu_callback(x), + } for i in range(5) + ] + self.menu = MDDropdownMenu( + items=menu_items, + width_mult=4, + ) + return Builder.load_string(KV) + + def callback(self, button): + self.menu.caller = button + self.menu.open() + + def menu_callback(self, text_item): + self.menu.dismiss() + Snackbar(text=text_item).open() + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-menu.gif + :align: center + +.. Position: +Position +======== + +Bottom position +--------------- + +.. seealso:: + + :attr:`~MDDropdownMenu.position` + +.. code-block:: python + + from kivy.lang import Builder + from kivy.metrics import dp + from kivy.properties import StringProperty + + from kivymd.uix.list import OneLineIconListItem + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + + + IconLeftWidget: + icon: root.icon + + + MDScreen + + MDTextField: + id: field + pos_hint: {'center_x': .5, 'center_y': .6} + size_hint_x: None + width: "200dp" + hint_text: "Password" + on_focus: if self.focus: app.menu.open() + ''' + + + class IconListItem(OneLineIconListItem): + icon = StringProperty() + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [ + { + "viewclass": "IconListItem", + "icon": "git", + "height": dp(56), + "text": f"Item {i}", + "on_release": lambda x=f"Item {i}": self.set_item(x), + } for i in range(5)] + self.menu = MDDropdownMenu( + caller=self.screen.ids.field, + items=menu_items, + position="bottom", + width_mult=4, + ) + + def set_item(self, text__item): + self.screen.ids.field.text = text__item + self.menu.dismiss() + + 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 kivy.metrics import dp + from kivy.properties import StringProperty + + from kivymd.uix.list import OneLineIconListItem + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + + + IconLeftWidget: + icon: root.icon + + + MDScreen + + MDDropDownItem: + id: drop_item + pos_hint: {'center_x': .5, 'center_y': .5} + text: 'Item 0' + on_release: app.menu.open() + ''' + + + class IconListItem(OneLineIconListItem): + icon = StringProperty() + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [ + { + "viewclass": "IconListItem", + "icon": "git", + "text": f"Item {i}", + "height": dp(56), + "on_release": lambda x=f"Item {i}": self.set_item(x), + } 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() + + def set_item(self, text_item): + self.screen.ids.drop_item.set_item(text_item) + 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",) + +import os +from typing import Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.core.window.window_sdl2 import WindowSDL +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.recycleview import RecycleView + +import kivymd.material_resources as m_res +from kivymd import uix_path +from kivymd.theming import ThemableBehavior + +with open( + os.path.join(uix_path, "menu", "menu.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDMenu(RecycleView): + width_mult = NumericProperty(1) + """ + See :attr:`~MDDropdownMenu.width_mult`. + """ + + drop_cls = ObjectProperty() + """ + See :class:`~MDDropdownMenu` class. + """ + + +class MDDropdownMenu(ThemableBehavior, FloatLayout): + """ + :Events: + `on_release` + The method that will be called when you click menu items. + """ + + header_cls = ObjectProperty() + """ + An instance of the class (`Kivy` or `KivyMD` widget) that will be added + to the menu header. + + .. versionadded:: 0.104.2 + + See Header_ for more information. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-header-cls.png + :align: center + + :attr:`header_cls` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + items = ListProperty() + """ + See :attr:`~kivy.uix.recycleview.RecycleView.data`. + + .. code-block:: python + + items = [ + { + "viewclass": "OneLineListItem", + "height": dp(56), + "text": f"Item {i}", + } + for i in range(5) + ] + self.menu = MDDropdownMenu( + items=items, + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-items.png + :align: center + + :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. + + .. code-block:: python + + self.menu = MDDropdownMenu( + width_mult=4, + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-width-mult-4.png + :align: center + + .. code-block:: python + + self.menu = MDDropdownMenu( + width_mult=8, + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-width-mult-8.png + :align: center + + :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. + + .. code-block:: python + + self.menu = MDDropdownMenu( + max_height=dp(112), + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-max-height-112.png + :align: center + + .. code-block:: python + + self.menu = MDDropdownMenu( + max_height=dp(224), + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-max-height-224.png + :align: center + + :attr:`max_height` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + border_margin = NumericProperty("4dp") + """ + Margin between Window border and menu. + + .. code-block:: python + + self.menu = MDDropdownMenu( + border_margin=dp(4), + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-border-margin-4.png + :align: center + + .. code-block:: python + + self.menu = MDDropdownMenu( + border_margin=dp(24), + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-border-margin-24.png + :align: center + + :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'`. + + .. code-block:: python + + self.menu = MDDropdownMenu( + ver_growth="up", + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-up.gif + :align: center + + .. code-block:: python + + self.menu = MDDropdownMenu( + ver_growth="down", + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-down.gif + :align: center + + :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'`. + + .. code-block:: python + + self.menu = MDDropdownMenu( + hor_growth="left", + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-left.gif + :align: center + + .. code-block:: python + + self.menu = MDDropdownMenu( + hor_growth="right", + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-right.gif + :align: center + + :attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty` + and defaults to `None`. + """ + + background_color = ColorProperty(None) + """ + Color of the background of the menu. + + .. code-block:: python + + self.menu = MDDropdownMenu( + background_color=self.theme_cls.primary_light, + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-background-color.png + :align: center + + :attr:`background_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + 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 and you can set it to 0 + if you don't want animation of menu opening. + + :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + caller = ObjectProperty() + """ + The widget object that calls the menu window. + + :attr:`caller` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + position = OptionProperty( + "auto", options=["top", "auto", "center", "bottom"] + ) + """ + Menu window position relative to parent element. + Available options are: `'auto'`, `'center'`, `'bottom'`. + + See Position_ for more information. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.png + :align: center + + :attr:`position` is a :class:`~kivy.properties.OptionProperty` + and defaults to `'auto'`. + """ + + radius = VariableListProperty([dp(7)]) + """ + Menu radius. + + .. code-block:: python + + self.menu = MDDropdownMenu( + radius=[24, 0, 24, 0], + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-radius.png + :align: center + + :attr:`radius` is a :class:`~kivy.properties.VariableListProperty` + and defaults to `'[dp(7)]'`. + """ + + elevation = NumericProperty(10) + """ + Elevation value of menu dialog. + + .. versionadded:: 1.0.0 + + .. code-block:: python + + self.menu = MDDropdownMenu( + elevation=16, + ..., + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-elevation.png + :align: center + + :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` + and defaults to `10`. + """ + + _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) + Clock.schedule_once(self.ajust_radius) + self.register_event_type("on_dismiss") + self.menu = self.ids.md_menu + self.target_height = 0 + + def check_position_caller( + self, instance_window: WindowSDL, width: int, height: int + ) -> None: + """Called when the application root window is resized.""" + + # FIXME: Menu position is not recalculated when changing the size of + # the root application window. + self.set_menu_properties(0) + + def set_menu_properties(self, interval: Union[int, float] = 0) -> None: + """Sets the size and position for the menu window.""" + + if self.caller: + self.ids.md_menu.data = self.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.ids.md_menu.data: + self.target_height += item.get("height", dp(72)) + + # 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 ajust_radius(self, interval: Union[int, float]) -> None: + """ + Adjusts the radius of the first and last items in the menu list + according to the radius that is set for the menu. + """ + + if self.items: + radius_for_firt_item = self.radius[:2] + radius_for_last_item = self.radius[2:] + + firt_data_item = self.items[0] + last_data_item = self.items[-1] + + firt_data_item["radius"] = radius_for_firt_item + [0, 0] + last_data_item["radius"] = [0, 0] + radius_for_last_item + last_data_item["divider"] = None + + self.items[0] = firt_data_item + self.items[-1] = last_data_item + + # For all other elements of the list, except for the first and + # last, we set the value of the radius to `0`. + for i, data_item in enumerate(self.items): + if "radius" not in data_item: + data_item["radius"] = 0 + self.items[i] = data_item + + def adjust_position(self) -> str: + """ + Returns value 'auto' for the menu position if the menu position is out + of screen. + """ + + target_width = self.target_width + target_height = self.target_height + caller = self.caller + position = self.position + + if ( + caller.x < target_width + or caller.x + target_width > Window.width + or caller.y + target_height > Window.height + or (caller.y < target_height and position == "center") + ): + position = "auto" + if self.hor_growth or self.ver_growth: + self.hor_growth = None + self.ver_growth = None + self.set_menu_properties() + return position + + def open(self) -> None: + """Animate the opening of a menu window.""" + + def open(interval): + if not self._calculate_complete: + return + + position = self.adjust_position() + + if position == "auto": + self.menu.pos = self._start_coords + anim = Animation( + x=self.tar_x, + y=self.tar_y + - (self.header_cls.height if self.header_cls else 0), + width=self.target_width, + height=self.target_height, + duration=self.opening_time, + opacity=1, + transition=self.opening_transition, + ) + anim.start(self.menu) + else: + if position == "center": + self.menu.pos = ( + self._start_coords[0] - self.target_width / 2, + self._start_coords[1] - self.target_height / 2, + ) + elif position == "bottom": + self.menu.pos = ( + self._start_coords[0] - self.target_width / 2, + self.caller.pos[1] - self.target_height, + ) + elif position == "top": + self.menu.pos = ( + self._start_coords[0] - self.target_width / 2, + self.caller.pos[1] + self.caller.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_header_cls( + self, instance_dropdown_menu, instance_user_menu_header + ) -> None: + """Called when a value is set to the :attr:`header_cls` parameter.""" + + def add_content_header_cls(interval): + self.ids.content_header.clear_widgets() + self.ids.content_header.add_widget(instance_user_menu_header) + + Clock.schedule_once(add_content_header_cls, 1) + + 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_dismiss(self) -> None: + """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, *args) -> None: + """Closes the menu.""" + + self.on_dismiss() + + +if __name__ == "__main__": + # To test the correct menu position. + from kivy.lang import Builder + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.button import MDRaisedButton + from kivymd.uix.screen import MDScreen + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = MDScreen() + menu_items = [ + { + "viewclass": "OneLineListItem", + "height": dp(56), + "text": f"Item {i}", + } + for i in range(5) + ] + self.menu = MDDropdownMenu(items=menu_items, width_mult=4) + + def open_menu(self, caller): + self.menu.caller = caller + self.menu.open() + + def on_start(self): + pos_hints = [ + {"top": 1, "left": 0.1}, + {"top": 1, "center_x": 0.5}, + {"top": 1, "right": 1}, + {"center_y": 0.5, "left": 1}, + {"bottom": 1, "left": 1}, + {"bottom": 1, "center_x": 0.5}, + {"bottom": 1, "right": 1}, + {"center_y": 0.5, "right": 1}, + {"center_y": 0.5, "center_x": 0.5}, + ] + for pos_hint in pos_hints: + self.screen.add_widget( + MDRaisedButton(pos_hint=pos_hint, on_release=self.open_menu) + ) + + def build(self): + return self.screen + + Test().run() diff --git a/sbapp/kivymd/uix/navigationdrawer/__init__.py b/sbapp/kivymd/uix/navigationdrawer/__init__.py new file mode 100644 index 0000000..1e859f9 --- /dev/null +++ b/sbapp/kivymd/uix/navigationdrawer/__init__.py @@ -0,0 +1,10 @@ +# NOQA F401 +from .navigationdrawer import ( + MDNavigationDrawer, + MDNavigationDrawerDivider, + MDNavigationDrawerHeader, + MDNavigationDrawerItem, + MDNavigationDrawerLabel, + MDNavigationDrawerMenu, + MDNavigationLayout, +) diff --git a/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.kv b/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.kv new file mode 100644 index 0000000..dbb484a --- /dev/null +++ b/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.kv @@ -0,0 +1,134 @@ +#:import Window kivy.core.window.Window +#:import m_res kivymd.material_resources + + +: + size_hint_x: None + width: Window.width - dp(56) if Window.width <= dp(376) else dp(320) + md_bg_color: self.theme_cls.bg_light + padding: + x: + (self.width * (self.open_progress - 1)) \ + if self.anchor == "left" \ + else (Window.width - self.width * self.open_progress) + + canvas: + Clear + Color: + rgba: self.md_bg_color + RoundedRectangle: + size: self.size + pos: self.pos + source: root.background + radius: root.radius + + + + adaptive_height: True + + MDLabel: + text: root.text + adaptive_size: True + markup: True + + + + adaptive_height: True + + MDSeparator: + color: root.color if root.color else app.theme_cls.divider_color + + + + adaptive_height: True + + FitImage: + id: logo + source: root.source + size_hint: None, None + size: label_box.height, label_box.height + + MDBoxLayout: + id: label_box + orientation: "vertical" + adaptive_height: True + + MDLabel: + id: title + adaptive_height: True + halign: root.title_halign + text: root.title + font_style: root.title_font_style + font_size: root.title_font_size + color: + root.title_color \ + if root.title_color else \ + app.theme_cls.text_color + + MDLabel: + id: text + adaptive_height: True + text: root.text + halign: root.text_halign + font_style: root.text_font_style + font_size: root.text_font_size + color: + root.text_color \ + if root.text_color else \ + app.theme_cls.text_color + + + + radius: self.height / 2 if self.radius == [0, 0, 0, 0] else self.radius + divider: None + theme_text_color: "Custom" + text_color: self.text_color if not self.selected else self.selected_color + _txt_left_pad: "56dp" + on_size: + self.ids._left_container.x = "4dp" + self.ids._right_container.width = right_label.texture_size[0] + on_release: + if not self.selected: self._text_color = self.text_color + self._text_right_color = root.text_right_color if root.text_right_color else app.theme_cls.text_color + self._drawer_menu.reset_active_color(self) + + IconLeftWidgetWithoutTouch: + icon: root.icon + theme_icon_color: "Custom" + icon_color: + ( \ + app.theme_cls.text_color \ + if not root.icon_color else \ + root.icon_color \ + ) \ + if not root.selected else \ + root.selected_color + + MDLabel: + id: right_label + text: root.right_text + pos_hint: {"center_y": .5} + adaptive_size: True + markup: True + color: + ( \ + root.text_right_color \ + if root.text_right_color else \ + app.theme_cls.text_color \ + ) \ + if not root.selected else \ + root.selected_color + x: + root.x \ + + root.width \ + - m_res.HORIZ_MARGINS \ + - root.ids._right_container.width - dp(24) \ + - self.texture_size[0] \ + + dp(24) + + + + + MDList: + id: menu + spacing: root.spacing diff --git a/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py b/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py new file mode 100755 index 0000000..5dea20d --- /dev/null +++ b/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py @@ -0,0 +1,1270 @@ +""" +Components/NavigationDrawer +=========================== + +.. seealso:: + + `Material Design 2 spec, Navigation drawer `_ and + `Material Design 3 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: + +Anatomy +------- + +.. code-block:: kv + + Root: + + MDNavigationLayout: + + 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 kivymd.uix.boxlayout import MDBoxLayout + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDNavigationLayout: + + ScreenManager: + + MDScreen: + + MDTopAppBar: + title: "Navigation Drawer" + elevation: 10 + pos_hint: {"top": 1} + md_bg_color: "#e7e4c0" + specific_text_color: "#4a4939" + left_action_items: + [['menu', lambda x: nav_drawer.set_state("open")]] + + + MDNavigationDrawer: + id: nav_drawer + md_bg_color: "#f7f4e7" + + ContentNavigationDrawer: + ''' + + + class ContentNavigationDrawer(MDBoxLayout): + 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. + +Custom content for navigation drawer +------------------------------------ + +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 + +Standard content for the navigation bar +--------------------------------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + + focus_color: "#e7e4c0" + unfocus_color: "#f7f4e7" + text_color: "#4a4939" + icon_color: "#4a4939" + ripple_color: "#c5bdd2" + selected_color: "#0c6c4d" + + + + bg_color: "#f7f4e7" + text_color: "#4a4939" + icon_color: "#4a4939" + _no_ripple_effect: True + + + MDScreen: + + MDNavigationLayout: + + ScreenManager: + + MDScreen: + + MDTopAppBar: + title: "Navigation Drawer" + elevation: 10 + pos_hint: {"top": 1} + md_bg_color: "#e7e4c0" + specific_text_color: "#4a4939" + left_action_items: + [ \ + [ \ + 'menu', lambda x: \ + nav_drawer.set_state("open") \ + if nav_drawer.state == "close" else \ + nav_drawer.set_state("close") \ + ] \ + ] + + MDNavigationDrawer: + id: nav_drawer + radius: (0, 16, 16, 0) if self.anchor == "left" else (16, 0, 0, 16) + md_bg_color: "#f7f4e7" + + MDNavigationDrawerMenu: + + MDNavigationDrawerHeader: + title: "Header title" + title_color: "#4a4939" + text: "Header text" + title_color: "#4a4939" + spacing: "4dp" + padding: "12dp", 0, 0, "56dp" + + MDNavigationDrawerLabel: + text: "Mail" + + DrawerClickableItem: + icon: "gmail" + right_text: "+99" + text_right_color: "#4a4939" + text: "Inbox" + + DrawerClickableItem: + icon: "send" + text: "Outbox" + + MDNavigationDrawerDivider: + + MDNavigationDrawerLabel: + text: "Labels" + + DrawerLabelItem: + icon: "information-outline" + text: "Label" + + DrawerLabelItem: + icon: "information-outline" + text: "Label" + ''' + + + class TestNavigationDrawer(MDApp): + def build(self): + self.theme_cls.primary_palette = "Indigo" + return Builder.load_string(KV) + + + TestNavigationDrawer().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-standatd-content.gif + :align: center + +Switching screens in the ``ScreenManager`` and using the common ``MDTopAppBar`` +----------------------------------------------------------------------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import ObjectProperty + + from kivymd.app import MDApp + from kivymd.uix.boxlayout import MDBoxLayout + + 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" + + + MDScreen: + + MDTopAppBar: + id: toolbar + pos_hint: {"top": 1} + elevation: 10 + title: "MDNavigationDrawer" + left_action_items: [["menu", lambda x: nav_drawer.set_state("open")]] + + MDNavigationLayout: + x: toolbar.height + + ScreenManager: + id: screen_manager + + MDScreen: + name: "scr 1" + + MDLabel: + text: "Screen 1" + halign: "center" + + MDScreen: + name: "scr 2" + + MDLabel: + text: "Screen 2" + halign: "center" + + MDNavigationDrawer: + id: nav_drawer + + ContentNavigationDrawer: + screen_manager: screen_manager + nav_drawer: nav_drawer + ''' + + + class ContentNavigationDrawer(MDBoxLayout): + screen_manager = ObjectProperty() + nav_drawer = ObjectProperty() + + + class TestNavigationDrawer(MDApp): + def build(self): + return Builder.load_string(KV) + + + TestNavigationDrawer().run() +""" + +__all__ = ( + "MDNavigationLayout", + "MDNavigationDrawer", + "MDNavigationDrawerItem", + "MDNavigationDrawerMenu", + "MDNavigationDrawerHeader", + "MDNavigationDrawerLabel", + "MDNavigationDrawerDivider", +) + +import os +from typing import Union + +from kivy.animation import Animation, AnimationTransition +from kivy.clock import Clock +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.properties import ( + AliasProperty, + BooleanProperty, + ColorProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.screenmanager import ScreenManager +from kivy.uix.scrollview import ScrollView + +from kivymd import uix_path +from kivymd.uix.behaviors import FakeRectangularElevationBehavior, FocusBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.card import MDCard +from kivymd.uix.list import MDList, OneLineAvatarIconListItem +from kivymd.uix.toolbar import MDTopAppBar + +with open( + os.path.join(uix_path, "navigationdrawer", "navigationdrawer.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class NavigationDrawerContentError(Exception): + pass + + +class MDNavigationLayout(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, instance_navigation_drawer, pos_x: float) -> None: + 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, instance_manager: ScreenManager) -> None: + with instance_manager.canvas.after: + self._scrim_color = Color(rgba=[0, 0, 0, 0]) + self._scrim_rectangle = Rectangle( + pos=instance_manager.pos, size=instance_manager.size + ) + instance_manager.bind( + pos=self.update_scrim_rectangle, + size=self.update_scrim_rectangle, + ) + + def update_scrim_rectangle( + self, instance_manager: ScreenManager, size: list + ) -> None: + 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, MDTopAppBar) + ): + raise NavigationDrawerContentError( + "The MDNavigationLayout 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 MDNavigationLayout must contain " + "only `MDNavigationDrawer` and `ScreenManager`" + ) + return super().add_widget(widget) + + +class MDNavigationDrawerLabel(MDBoxLayout): + """ + Implements a label for a menu for :class:`~MDNavigationDrawer` class. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDNavigationDrawer: + + MDNavigationDrawerMenu: + + MDNavigationDrawerLabel: + text: "Mail" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-label.png + :align: center + """ + + text = StringProperty() + """ + Text label. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + padding = VariableListProperty(["20dp", 0, 0, "8dp"]) + """ + Padding between layout box and 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]. + + :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` + and defaults to `['20dp', 0, 0, '8dp']`. + """ + + +class MDNavigationDrawerDivider(MDBoxLayout): + """ + Implements a divider for a menu for :class:`~MDNavigationDrawer` class. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDNavigationDrawer: + + MDNavigationDrawerMenu: + + MDNavigationDrawerLabel: + text: "Mail" + + MDNavigationDrawerDivider: + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-divider.png + :align: center + """ + + padding = VariableListProperty(["20dp", "12dp", 0, "12dp"]) + """ + Padding between layout box and 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]. + + :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` + and defaults to `['20dp', '12dp', 0, '12dp']`. + """ + + color = ColorProperty(None) + """ + Divider color in ``rgba`` format. + + :attr:`color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + +class MDNavigationDrawerHeader(MDBoxLayout): + """ + Implements a header for a menu for :class:`~MDNavigationDrawer` class. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDNavigationDrawer: + + MDNavigationDrawerMenu: + + MDNavigationDrawerHeader: + title: "Header title" + text: "Header text" + spacing: "4dp" + padding: "12dp", 0, 0, "56dp" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-header.png + :align: center + """ + + source = StringProperty() + """ + Image logo path. + + .. code-block:: kv + + MDNavigationDrawer: + + MDNavigationDrawerMenu: + + MDNavigationDrawerHeader: + title: "Header title" + text: "Header text" + source: "logo.png" + spacing: "4dp" + padding: "12dp", 0, 0, "56dp" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-header-source.png + :align: center + + :attr:`source` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + title = StringProperty() + """ + Title shown in the first line. + + :attr:`title` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + title_halign = StringProperty("left") + """ + Title halign first line. + + :attr:`title_halign` is a :class:`~kivy.properties.StringProperty` + and defaults to `'left'`. + """ + + title_color = ColorProperty(None) + """ + Title text color. + + :attr:`title_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + title_font_style = StringProperty("H4") + """ + Title shown in the first line. + + :attr:`title_font_style` is a :class:`~kivy.properties.StringProperty` + and defaults to `'H4'`. + """ + + title_font_size = StringProperty("34sp") + """ + Title shown in the first line. + + :attr:`title_font_size` is a :class:`~kivy.properties.StringProperty` + and defaults to `'34sp'`. + """ + + text = StringProperty() + """ + Text shown in the second line. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + text_halign = StringProperty("left") + """ + Text halign first line. + + :attr:`text_halign` is a :class:`~kivy.properties.StringProperty` + and defaults to `'left'`. + """ + + text_color = ColorProperty(None) + """ + Title text color. + + :attr:`text_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text_font_style = StringProperty("H6") + """ + Title shown in the first line. + + :attr:`text_font_style` is a :class:`~kivy.properties.StringProperty` + and defaults to `'H6'`. + """ + + text_font_size = StringProperty("20sp") + """ + Title shown in the first line. + + :attr:`text_font_size` is a :class:`~kivy.properties.StringProperty` + and defaults to `'20sp'`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self.check_content) + + def check_content(self, interval: Union[int, float]) -> None: + """Removes widgets that the user has not added to the container.""" + + if not self.title: + self.ids.label_box.remove_widget(self.ids.title) + if not self.text: + self.ids.label_box.remove_widget(self.ids.text) + if not self.source: + self.remove_widget(self.ids.logo) + + +class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior): + """ + Implements an item for the :class:`~MDNavigationDrawer` menu list. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDNavigationDrawer: + + MDNavigationDrawerMenu: + + MDNavigationDrawerHeader: + title: "Header title" + text: "Header text" + spacing: "4dp" + padding: "12dp", 0, 0, "56dp" + + MDNavigationDrawerItem + icon: "gmail" + right_text: "+99" + text: "Inbox" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-item.png + :align: center + """ + + selected = BooleanProperty(False) + """ + Is the item selected. + + :attr:`selected` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + icon = StringProperty() + """ + Icon item. + + :attr:`icon` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon_color = ColorProperty(None) + """ + Icon color item. + + :attr:`icon_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + selected_color = ColorProperty([0, 0, 0, 1]) + """ + The color of the icon and text of the selected item. + + :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 1]`. + """ + + right_text = StringProperty() + """ + Right text item. + + :attr:`right_text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + text_right_color = ColorProperty(None) + """ + Right text color item. + + :attr:`text_right_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _text_color = None + _text_right_color = None + # kivymd.uix.navigationdrawer.navigationdrawer.MDNavigationDrawerMenu + _drawer_menu = ObjectProperty() + + +class MDNavigationDrawerMenu(ScrollView): + """ + Implements a scrollable list for menu items of the + :class:`~MDNavigationDrawer` class. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDNavigationDrawer: + + MDNavigationDrawerMenu: + + # Your menu items. + ... + """ + + spacing = NumericProperty(0) + """ + Spacing between children, in pixels. + + :attr:`spacing` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDList): + return super().add_widget(widget, *args, **kwargs) + else: + if isinstance(widget, MDNavigationDrawerItem): + widget._drawer_menu = self + self.ids.menu.add_widget(widget) + + def reset_active_color(self, item: MDNavigationDrawerItem) -> None: + for widget in self.ids.menu.children: + if issubclass(widget.__class__, MDNavigationDrawerItem): + if widget != item: + widget.selected = False + else: + widget.selected = True + + if ( + issubclass(widget.__class__, MDNavigationDrawerItem) + and widget != item + ): + if widget._text_color: + widget.text_color = widget._text_color + + +class MDNavigationDrawer(MDCard, FakeRectangularElevationBehavior): + 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. + + Standard + -------- + + .. code-block:: kv + + MDNavigationDrawer: + type: "standard" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-standard.gif + :align: center + + Model + ----- + + .. code-block:: kv + + MDNavigationDrawer: + type: "modal" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-modal.gif + :align: center + + :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'`. + + Left + ---- + + .. code-block:: kv + + MDNavigationDrawer: + anchor: "left" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-type-left.png + :align: center + + Right + ----- + + .. code-block:: kv + + MDNavigationDrawer: + anchor: "right" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-type-right.png + :align: center + + :attr:`anchor` is a :class:`~kivy.properties.OptionProperty` + and defaults to `'left'`. + """ + + # FIXME: Doesn't work in Kivy v2.1.0. + scrim_color = ColorProperty([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. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-scrim-color.png + :align: center + + .. code-block:: kv + + MDNavigationDrawer: + scrim_color: 0, 0, 0, .8 + # scrim_color: 0, 0, 0, .2 + + :attr:`scrim_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0.5]`. + """ + + padding = VariableListProperty([16, 16, 12, 16]) + """ + Padding between layout box and 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]. + + .. versionchanged:: 1.0.0 + + .. code-block:: kv + + MDNavigationDrawer: + padding: 56, 56, 12, 16 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-padding.png + :align: center + + :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and + defaults to '[16, 16, 12, 16]'. + """ + + 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`. + """ + + 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, MDNavigationLayout) + 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) -> None: + """ + 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 update_status(self, *_) -> None: + 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.opacity = 0 + else: + self.opacity = 1 + + def get_dist_from_side(self, x: float) -> float: + 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": + if abs(touch.x - touch.ox) > self.swipe_distance: + 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_navigation_drawer, radius_value: list) -> None: + self._radius = radius_value + + def on_type(self, instance_navigation_drawer, drawer_type: str) -> None: + 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/sbapp/kivymd/uix/navigationrail/__init__.py b/sbapp/kivymd/uix/navigationrail/__init__.py new file mode 100644 index 0000000..0ec0480 --- /dev/null +++ b/sbapp/kivymd/uix/navigationrail/__init__.py @@ -0,0 +1,6 @@ +# NOQA F401 +from .navigationrail import ( + MDNavigationRail, + MDNavigationRailFabButton, + MDNavigationRailMenuButton, +) diff --git a/sbapp/kivymd/uix/navigationrail/navigationrail.kv b/sbapp/kivymd/uix/navigationrail/navigationrail.kv new file mode 100644 index 0000000..9baf444 --- /dev/null +++ b/sbapp/kivymd/uix/navigationrail/navigationrail.kv @@ -0,0 +1,145 @@ + + pos_hint: {"center_x": .5, "top": 1} + + + + type: "standard" + pos_hint: {"center_x": .5} + + + + size_hint: None, 1 + width: "80dp" + + PanelRoot: + id: box_buttons + + PanelItems: + id: box_items + orientation: "vertical" + spacing: "12dp" + adaptive_size: True + pos_hint: {"center_x": .5} + + + + orientation: "vertical" + size_hint: None, None + size: self.navigation_rail.width, "56dp" + + RelativeLayout: + id: container + size_hint: None, None + size: root.size + + RippleWidget: + id: ripple_widget + size_hint: None, None + size: (container.width, container.width) + radius: container.width / 2 + scale_value_x: 0 + scale_value_y: 0 + scale_value_z: 0 + opacity: 0 + md_bg_color: + root.navigation_rail.ripple_color_item \ + if root.navigation_rail.ripple_color_item else \ + app.theme_cls.primary_color + + MDIcon: + id: icon + icon: root.icon + opposite_colors: root.opposite_colors + font_size: "24sp" + pos_hint: {"center_x": .5} + badge_icon: root.badge_icon + badge_font_size: root.badge_font_size + badge_icon_color: + root.badge_icon_color \ + if root.badge_icon_color else \ + (1, 1, 1, 1) + badge_bg_color: + root.badge_bg_color \ + if root.badge_bg_color else \ + app.theme_cls.error_color + theme_text_color: "Custom" + text_color: + ( \ + root.navigation_rail.icon_color_item_normal \ + if root.navigation_rail.icon_color_item_normal else \ + app.theme_cls.text_color \ + ) \ + if not root.active else \ + ( \ + root.navigation_rail.icon_color_item_active \ + if root.navigation_rail.icon_color_item_active else \ + app.theme_cls.text_color \ + ) + y: + container.height - \ + ( \ + (self.height + dp(4)) \ + if root.navigation_rail.type == "unselected" else \ + (self.height - dp(8)) \ + ) + + canvas.before: + Color: + rgba: + ( \ + ( \ + ( \ + app.theme_cls.primary_color \ + if not root.navigation_rail.selected_color_background else \ + root.navigation_rail.selected_color_background \ + ) \ + if root._release else \ + (0, 0, 0, 0) \ + ) \ + ) \ + if root.active else \ + (0, 0, 0, 0) + RoundedRectangle: + radius: + [root._selected_region_width / 2,] \ + if root.navigation_rail.type == "unselected" else \ + [root._selected_region_width / 4,] + size: + root._selected_region_width, \ + root._selected_region_width \ + if root.navigation_rail.type == "unselected" else \ + root._selected_region_width / 2 + pos: + self.center_x - self.width - dp(4), \ + self.center_y - root._selected_region_width / 2 \ + if root.navigation_rail.type == "unselected" else \ + self.center_y - root._selected_region_width / 4 + + MDLabel: + id: label + text: root.text + size_hint_x: None + text_size: None, root.height + adaptive_height: True + opposite_colors: root.opposite_colors + pos_hint: {"center_x": .5} + y: "16" + font_style: "Body2" + font_name: root.navigation_rail.font_name + theme_text_color: "Custom" + text_color: + ( \ + root.navigation_rail.text_color_item_normal \ + if root.navigation_rail.text_color_item_normal else \ + app.theme_cls.text_color \ + ) \ + if not root.active else \ + ( \ + root.navigation_rail.text_color_item_active \ + if root.navigation_rail.text_color_item_active else \ + app.theme_cls.text_color \ + ) + opacity: + (0 if root.navigation_rail.type == "unselected" else 1) \ + if root.navigation_rail.type != "selected" else \ + (0 if not root.active else 1) diff --git a/sbapp/kivymd/uix/navigationrail/navigationrail.py b/sbapp/kivymd/uix/navigationrail/navigationrail.py new file mode 100644 index 0000000..881d6cd --- /dev/null +++ b/sbapp/kivymd/uix/navigationrail/navigationrail.py @@ -0,0 +1,990 @@ +""" +Components/NavigationRail +========================= + +.. versionadded:: 1.0.0 + +.. seealso:: + + `Material Design spec, Navigation rail `_ + +.. rubric:: + + Navigation rails provide access to primary destinations in apps when using + tablet and desktop screens. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail.png + :align: center + +Usage +===== + +.. code-block:: kv + + MDNavigationRail: + + MDNavigationRailItem: + + MDNavigationRailItem: + + MDNavigationRailItem: + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + + + MDBoxLayout: + + MDNavigationRail: + + MDNavigationRailItem: + text: "Python" + icon: "language-python" + + MDNavigationRailItem: + text: "JavaScript" + icon: "language-javascript" + + MDNavigationRailItem: + text: "CPP" + icon: "language-cpp" + + MDNavigationRailItem: + text: "Git" + icon: "git" + + MDScreen: + ''' + + + class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-usage.png + :align: center + +Example +======= + +.. code-block:: python + + from kivy.clock import Clock + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.behaviors import RoundedRectangularElevationBehavior + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.button import MDFillRoundFlatIconButton + from kivymd.uix.label import MDLabel + from kivymd.uix.screen import MDScreen + + KV = ''' + #:import FadeTransition kivy.uix.screenmanager.FadeTransition + + + + elevation: 3 + -height: "56dp" + + + + focus_color: "#e7e4c0" + unfocus_color: "#fffcf4" + + + MDScreen: + + MDNavigationLayout: + + ScreenManager: + + MDScreen: + + MDBoxLayout: + orientation: "vertical" + + MDBoxLayout: + adaptive_height: True + md_bg_color: "#fffcf4" + padding: "12dp" + + MDLabel: + text: "12:00" + adaptive_height: True + pos_hint: {"center_y": .5} + + MDBoxLayout: + + MDNavigationRail: + id: navigation_rail + md_bg_color: "#fffcf4" + selected_color_background: "#e7e4c0" + ripple_color_item: "#e7e4c0" + on_item_release: app.switch_screen(*args) + + MDNavigationRailMenuButton: + on_release: nav_drawer.set_state("open") + + MDNavigationRailFabButton: + md_bg_color: "#b0f0d6" + + MDNavigationRailItem: + text: "Python" + icon: "language-python" + + MDNavigationRailItem: + text: "JavaScript" + icon: "language-javascript" + + MDNavigationRailItem: + text: "CPP" + icon: "language-cpp" + + MDNavigationRailItem: + text: "Swift" + icon: "language-swift" + + ScreenManager: + id: screen_manager + transition: + FadeTransition(duration=.2, clearcolor=app.theme_cls.bg_dark) + + MDNavigationDrawer: + id: nav_drawer + radius: (0, 16, 16, 0) + md_bg_color: "#fffcf4" + elevation: 12 + width: "240dp" + + MDNavigationDrawerMenu: + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "12dp" + padding: 0, 0, 0, "12dp" + + MDIconButton: + icon: "menu" + + ExtendedButton: + text: "Compose" + icon: "pencil" + + DrawerClickableItem: + text: "Python" + icon: "language-python" + + DrawerClickableItem: + text: "JavaScript" + icon: "language-javascript" + + DrawerClickableItem: + text: "CPP" + icon: "language-cpp" + + DrawerClickableItem: + text: "Swift" + icon: "language-swift" + ''' + + + class ExtendedButton( + RoundedRectangularElevationBehavior, MDFillRoundFlatIconButton + ): + ''' + Implements a button of type + `Extended FAB `_. + + .. rubric:: + Extended FABs help people take primary actions. + They're wider than FABs to accommodate a text label and larger target + area. + + This type of buttons is not yet implemented in the standard widget set + of the KivyMD library, so we will implement it ourselves in this class. + ''' + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.padding = "16dp" + Clock.schedule_once(self.set_spacing) + + def set_spacing(self, interval): + self.ids.box.spacing = "12dp" + + def set_radius(self, *args): + if self.rounded_button: + self._radius = self.radius = self.height / 4 + + + class Example(MDApp): + def build(self): + self.theme_cls.material_style = "M3" + self.theme_cls.primary_palette = "Orange" + return Builder.load_string(KV) + + def switch_screen( + self, instance_navigation_rail, instance_navigation_rail_item + ): + ''' + Called when tapping on rail menu items. Switches application screens. + ''' + + self.root.ids.screen_manager.current = ( + instance_navigation_rail_item.icon.split("-")[1].lower() + ) + + def on_start(self): + '''Creates application screens.''' + + navigation_rail_items = self.root.ids.navigation_rail.get_items()[:] + navigation_rail_items.reverse() + + for widget in navigation_rail_items: + name_screen = widget.icon.split("-")[1].lower() + screen = MDScreen( + name=name_screen, + md_bg_color="#edd769", + radius=[18, 0, 0, 0], + ) + box = MDBoxLayout(padding="12dp") + label = MDLabel( + text=name_screen.capitalize(), + font_style="H1", + halign="right", + adaptive_height=True, + shorten=True, + ) + box.add_widget(label) + screen.add_widget(box) + self.root.ids.screen_manager.add_widget(screen) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-example.gif + :align: center + +""" + +__all__ = ( + "MDNavigationRail", + "MDNavigationRailFabButton", + "MDNavigationRailMenuButton", +) + +import os +from typing import Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.logger import Logger +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.behaviors import ButtonBehavior + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import FakeRectangularElevationBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.button import MDFloatingActionButton, MDIconButton +from kivymd.uix.card import MDCard +from kivymd.uix.floatlayout import MDFloatLayout +from kivymd.uix.templates import ScaleWidget +from kivymd.uix.widget import MDWidget + +with open( + os.path.join(uix_path, "navigationrail", "navigationrail.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class PanelRoot(MDFloatLayout): + """ + Contains + :class:`~MDNavigationRailFabButton`, :class:`~MDNavigationRailMenuButton` + buttons and a :class:`~Paneltems` container with menu items. + """ + + +class PanelItems(MDBoxLayout): + """Box for menu items.""" + + +class RippleWidget(MDWidget, ScaleWidget): + """ + Implements a background color for a menu item - + (:class:`~MDNavigationRailItem`). + """ + + +class MDNavigationRailFabButton(MDFloatingActionButton): + """Implements an optional floating action button (FAB).""" + + icon = StringProperty("pencil") + """ + Button icon name. + + .. code-block:: kv + + MDNavigationRail: + + MDNavigationRailFabButton: + icon: "home" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-fab-button-icon.png + :align: center + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'pencil'`. + """ + + +class MDNavigationRailMenuButton(MDIconButton): + """Implements a menu button.""" + + icon = StringProperty("menu") + """ + Button icon name. + + .. code-block:: kv + + MDNavigationRail: + + MDNavigationRailMenuButton: + icon: "home" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-menu-button-icon.png + :align: center + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'menu'`. + """ + + +class MDNavigationRailItem(ThemableBehavior, ButtonBehavior, MDBoxLayout): + """Implements a menu item with an icon and text.""" + + navigation_rail = ObjectProperty() + """ + :class:`~MDNavigationRail` object. + + :attr:`navigation_rail` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + icon = StringProperty("checkbox-blank-circle") + """ + Icon item. + + .. code-block:: kv + + MDNavigationRail: + + MDNavigationRailItem: + icon: "language-python" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-icon.png + :align: center + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'checkbox-blank'`. + """ + + text = StringProperty() + """ + Text item. + + .. code-block:: kv + + MDNavigationRail: + + MDNavigationRailItem: + text: "Python" + icon: "language-python" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-text.png + :align: center + + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + badge_icon = StringProperty() + """ + Badge icon name. + + .. code-block:: kv + + MDNavigationRail: + + MDNavigationRailItem: + text: "Python" + icon: "language-python" + badge_icon: "plus" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-badge-icon.png + :align: center + + :attr:`badge_icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + badge_icon_color = ColorProperty(None) + """ + Badge icon color in (r, g, b, a) format. + + .. code-block:: kv + + MDNavigationRail: + + MDNavigationRailItem: + text: "Python" + icon: "language-python" + badge_icon: "plus" + badge_icon_color: 0, 0, 1, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-badge-icon-color.png + :align: center + + :attr:`badge_icon_color` is an :class:`~kivy.properties.StringProperty` + and defaults to `None`. + """ + + badge_bg_color = ColorProperty(None) + """ + Badge icon background color in (r, g, b, a) format. + + .. code-block:: kv + + MDNavigationRail: + + MDNavigationRailItem: + text: "Python" + icon: "language-python" + badge_icon: "plus" + badge_bg_color: "#b0f0d6" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-badge-bg-color.png + :align: center + + :attr:`badge_bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + badge_font_size = NumericProperty(0) + """ + Badge icon font size. + + .. code-block:: kv + + MDNavigationRail: + + MDNavigationRailItem: + text: "Python" + icon: "language-python" + badge_icon: "plus" + badge_font_size: "24sp" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-item-badge-font-size.png + :align: center + + :attr:`badge_font_size` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + active = BooleanProperty(False) + """ + Is the element active. + + :attr:`active` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + _selected_region_width = NumericProperty("56dp") + _ripple_size = ListProperty([0, 0]) + _release = BooleanProperty(False) + + def on_active( + self, instance_navigation_rail_item, value_active: bool + ) -> None: + """Called when the value of `active` changes.""" + + self.animation_size_ripple_area(1 if value_active else 0) + + def animation_size_ripple_area(self, value: int) -> None: + """Animates the size/fade of the ripple area.""" + + Animation( + scale_value_x=value, + scale_value_y=value, + scale_value_z=value, + opacity=value, + d=0.25, + t=self.navigation_rail.ripple_transition, + ).start(self.ids.ripple_widget) + + def on_press(self) -> None: + """Called when pressed on a panel element.""" + + self._release = False + self.active = True + self.navigation_rail.deselect_item(self) + self.navigation_rail.dispatch("on_item_press", self) + + def on_release(self) -> None: + """Called when released on a panel element.""" + + self._release = True + self.animation_size_ripple_area(0) + self.navigation_rail.dispatch("on_item_release", self) + + +class MDNavigationRail(MDCard, FakeRectangularElevationBehavior): + """ + :Events: + :attr:`on_item_press` + Called on the `on_press` event of menu item - + :class:`~MDNavigationRailItem`. + + :attr:`on_item_release` + Called on the `on_release` event of menu item - + :class:`~MDNavigationRailItem`. + """ + + radius = VariableListProperty(0, length=4) + """ + Rail radius. + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + padding = VariableListProperty([0, "36dp", 0, "36dp"], length=4) + """ + Padding between layout box and children: + [padding_left, padding_top, padding_right, padding_bottom]. + + :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` + and defaults to `[0, '36dp', 0, '36dp']`. + """ + + anchor = OptionProperty("top", options=["top", "bottom", "center"]) + """ + The position of the panel with menu items. + Available options are: `'top'`, `'bottom'`, `'center'`. + + .. rubric:: Top + + .. code-block:: kv + + MDNavigationRail: + anchor: "top" + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anchor-top.png + :align: center + + .. rubric:: Center + + .. code-block:: kv + + MDNavigationRail: + anchor: "center" + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-center.png + :align: center + + .. rubric:: Bottom + + .. code-block:: kv + + MDNavigationRail: + anchor: "bottom" + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-bottom.png + :align: center + + :attr:`anchor` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'top'`. + """ + + type = OptionProperty( + "labeled", options=["labeled", "selected", "unselected"] + ) + """ + Type of switching menu items. + Available options are: `'labeled'`, `'selected'`, `'unselected'`. + + .. rubric:: Labeled + + .. code-block:: kv + + MDNavigationRail: + type: "labeled" + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-labeled.png + :align: center + + .. rubric:: Selected + + .. code-block:: kv + + MDNavigationRail: + type: "selected" + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-selected.gif + :align: center + + .. rubric:: Unselected + + .. code-block:: kv + + MDNavigationRail: + type: "unselected" + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-type-unselected.gif + :align: center + + :attr:`type` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'labeled'`. + """ + + text_color_item_normal = ColorProperty(None) + """ + The text color of the normal menu item (:class:`~MDNavigationRailItem`). + + .. code-block:: kv + + MDNavigationRail: + text_color_item_normal: app.theme_cls.primary_color + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-text-color-item-normal.png + :align: center + + :attr:`text_color_item_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text_color_item_active = ColorProperty(None) + """ + The text color of the active menu item (:class:`~MDNavigationRailItem`). + + .. code-block:: kv + + MDNavigationRail: + text_color_item_active: app.theme_cls.primary_color + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-text-color-item-active.png + :align: center + + :attr:`text_color_item_active` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + icon_color_item_normal = ColorProperty(None) + """ + The icon color of the normal menu item (:class:`~MDNavigationRailItem`). + + .. code-block:: kv + + MDNavigationRail: + icon_color_item_normal: app.theme_cls.primary_color + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-icon-color-item-normal.png + :align: center + + :attr:`icon_color_item_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + icon_color_item_active = ColorProperty(None) + """ + The icon color of the active menu item (:class:`~MDNavigationRailItem`). + + .. code-block:: kv + + MDNavigationRail: + icon_color_item_active: app.theme_cls.primary_color + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-icon-color-item-active.png + :align: center + + :attr:`icon_color_item_active` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + selected_color_background = ColorProperty(None) + """ + Background color which will highlight the icon of the active menu item - + :class:`~MDNavigationRailItem` - in (r, g, b, a) format. + + .. code-block:: kv + + MDNavigationRail: + selected_color_background: "#e7e4c0" + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-selected-color-background.png + :align: center + + :attr:`selected_color_background` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + ripple_color_item = ColorProperty(None) + """ + Ripple effect color of menu items (:class:`~MDNavigationRailItem`) + in (r, g, b, a) format. + + .. code-block:: kv + + MDNavigationRail: + ripple_color_item: "#e7e4c0" + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-ripple-color-item.png + :align: center + + :attr:`ripple_color_item` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + ripple_transition = StringProperty("out_cubic") + """ + Type of animation of the ripple effect when a menu item is selected. + + :attr:`ripple_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'ripple_transition'`. + """ + + current_selected_item = NumericProperty(0) + """ + Index of the menu list item (:class:`~MDNavigationRailItem`) that will be + active by default + + .. code-block:: kv + + MDNavigationRail: + current_selected_item: 1 + + MDNavigationRailItem: + ... + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-current-selected-item.png + :align: center + + :attr:`current_selected_item` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + font_name = StringProperty("Roboto") + """ + Font path for menu item (:class:`~MDNavigationRailItem`) text. + + .. code-block:: kv + + MDNavigationRail: + + MDNavigationRailItem: + text: "Python" + icon: "language-python" + font_name: "nasalization-rg.ttf" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-font-name.png + :align: center + + :attr:`font_name` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Roboto'`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self.set_pos_menu_fab_buttons) + Clock.schedule_once(self.set_current_selected_item) + self.register_event_type("on_item_press") + self.register_event_type("on_item_release") + + def on_item_press(self, *args) -> None: + """ + Called on the `on_press` event of menu item - + :class:`~MDNavigationRailItem`. + """ + + def on_item_release(self, *args) -> None: + """ + Called on the `on_release` event of menu item - + :class:`~MDNavigationRailItem`. + """ + + def deselect_item( + self, selected_navigation_rail_item: MDNavigationRailItem + ) -> None: + """ + Sets the `active` value to `False` for all menu items + (:class:`~MDNavigationRailItem`) except the selected item. + Called when a menu item is touched. + """ + + for navigation_rail_item in self.ids.box_items.children: + if selected_navigation_rail_item is not navigation_rail_item: + navigation_rail_item.active = False + + def get_items(self) -> list: + """Returns a list of :class:`~MDNavigationRailItem` objects""" + + return self.ids.box_items.children + + def set_pos_panel_items( + self, + instance_fab_button: Union[None, MDNavigationRailFabButton], + instance_menu_button: Union[None, MDNavigationRailFabButton], + ) -> None: + """Set :class:`~Paneltems` panel position with menu items.""" + + if self.anchor == "top": + if instance_fab_button: + self.ids.box_items.y = instance_fab_button.y - ( + len(self.ids.box_items.children) * dp(56) + + self.padding[1] * 2 + + dp(24) + ) + else: + if not instance_menu_button: + self.ids.box_items.pos_hint = {"top": 1} + else: + self.ids.box_items.y = instance_menu_button.y - ( + len(self.ids.box_items.children) * dp(56) + + self.padding[1] * 2 + ) + elif self.anchor == "center": + self.ids.box_items.pos_hint = {"center_y": 0.5} + elif self.anchor == "bottom": + self.ids.box_items.y = dp(12) + + def set_current_selected_item(self, interval: Union[int, float]) -> None: + """Sets the active menu list item (:class:`~MDNavigationRailItem`).""" + + if self.ids.box_items.children: + items = self.ids.box_items.children[:] + items.reverse() + + if len(items) <= self.current_selected_item: + Logger.error( + f"MDNavigationRail:You have " + f"{len(self.ids.box_items.children)} menu items, but you " + f"set {self.current_selected_item} as the active item. " + f"The very first menu item will be set active." + ) + index = 0 + else: + index = self.current_selected_item + + items[index].dispatch("on_press") + items[index].dispatch("on_release") + + def set_pos_menu_fab_buttons(self, interval: Union[int, float]) -> None: + """ + Sets the position of the :class:`~MDNavigationRailFabButton` and + :class:`~MDNavigationRailMenuButton` buttons on the panel. + """ + + fab_button = None # MDNavigationRailFabButton + menu_button = None # MDNavigationRailMenuButton + + for widget in self.ids.box_buttons.children: + if isinstance(widget, MDNavigationRailFabButton): + fab_button = widget + if isinstance(widget, MDNavigationRailMenuButton): + menu_button = widget + + if fab_button and menu_button: + + def set_fab_button_y(interval): + fab_button.y = self.parent.height - ( + menu_button.height + + fab_button.height + + self.padding[1] + + dp(18) + ) + self.set_pos_panel_items(fab_button, menu_button) + + Clock.schedule_once(set_fab_button_y) + elif fab_button and not menu_button: + + def set_fab_button_y(interval): + fab_button.y = self.parent.height - ( + self.padding[1] + fab_button.height + ) + self.set_pos_panel_items(fab_button, menu_button) + + Clock.schedule_once(set_fab_button_y) + else: + Clock.schedule_once( + lambda x: self.set_pos_panel_items(fab_button, menu_button) + ) + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDNavigationRailFabButton): + self.ids.box_buttons.add_widget(widget) + elif isinstance(widget, MDNavigationRailMenuButton): + self.ids.box_buttons.add_widget(widget) + elif isinstance(widget, MDNavigationRailItem): + widget.navigation_rail = self + self.ids.box_items.add_widget(widget) + elif isinstance(widget, (PanelRoot, PanelItems)): + return super().add_widget(widget) diff --git a/sbapp/kivymd/uix/pickers/__init__.py b/sbapp/kivymd/uix/pickers/__init__.py new file mode 100644 index 0000000..d8039aa --- /dev/null +++ b/sbapp/kivymd/uix/pickers/__init__.py @@ -0,0 +1,3 @@ +from .colorpicker import MDColorPicker # NOQA F401 +from .datepicker import MDDatePicker # NOQA F401 +from .timepicker import MDTimePicker # NOQA F401 diff --git a/sbapp/kivymd/uix/pickers/colorpicker/__init__.py b/sbapp/kivymd/uix/pickers/colorpicker/__init__.py new file mode 100644 index 0000000..5d5f5dd --- /dev/null +++ b/sbapp/kivymd/uix/pickers/colorpicker/__init__.py @@ -0,0 +1 @@ +from .colorpicker import MDColorPicker # NOQA F401 diff --git a/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.kv b/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.kv new file mode 100644 index 0000000..dde7c0f --- /dev/null +++ b/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.kv @@ -0,0 +1,299 @@ +#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT +#:import images_path kivymd.images_path +#:import colors kivymd.color_definitions.colors +#:import Window kivy.core.window.Window + + + + orientation: "vertical" + adaptive_height: True + spacing: "12dp" + padding: 0, 0, 0, "8dp" + + FitImage: + size_hint_y: None + height: "36dp" + source: f"{images_path}/alpha_layer.png" + radius: [8,] + + canvas.after: + Color: + rgba: + root._rgb[:-1] + [root._opacity_value_selected_color] + RoundedRectangle: + pos: self.pos + size: self.size + radius: [8,] + + MDSlider: + id: slider + size_hint_y: None + height: "12dp" + hint: False + max: 1 + value: root._opacity_value_selected_color + on_value: + root._opacity_value_selected_color = self.value + if root.color_picker: \ + root.color_picker._opacity_value_selected_color = self.value + + + + spacing: "12dp" + color_slider: "Red" + max: 255 + adaptive_height: True + + MDSlider: + id: slider + size_hint_y: None + height: "36dp" + color: colors[root.color_slider]["500"] + max: root.max + value: 1 if root.max == 1 else 0 + on_value: + root.parent.dispatch("on_slide_value", root.parent.get_color()) + + MDLabel: + adaptive_size: True + -text_size: None, None + pos_hint: {"center_y": .5} + text: + str(int(slider.value)) \ + if root.max != 1 \ + else str(round(slider.value, 1)) + + + + orientation: "vertical" + padding: "12dp", "24dp", "12dp", 0 + spacing: "24dp" + + SliderItem: + id: slider_red + color_slider: "Red" + + SliderItem: + id: slider_green + color_slider: "Green" + + SliderItem: + id: slider_blue + color_slider: "Blue" + + Widget: + + SelectAlphaChannelWidget: + id: select_alpha_channel_widget + color_picker: root.color_picker + + + + orientation: "vertical" + padding: "12dp", "12dp", "12dp", 0 + spacing: "8dp" + + MDBoxLayout: + id: color_selection_box + spacing: "12dp" + + Widget: + id: gradient_widget + + MDBoxLayout: + orientation: "vertical" + size_hint_x: None + width: "24dp" + + canvas.before: + StencilPush + RoundedRectangle: + size: self.size + pos: self.pos + radius: root.color_picker.radius_color_scale + StencilUse + + canvas.after: + StencilUnUse + RoundedRectangle: + size: self.size + pos: self.pos + radius: root.color_picker.radius_color_scale + StencilPop + + Image: + source: f"{images_path}/blue.png" + allow_stretch: True + keep_ratio: False + on_touch_down: + if self.collide_point(*args[1].pos): \ + root.updated_canvas(self, args[1]) + + Image: + source: f"{images_path}/green.png" + allow_stretch: True + keep_ratio: False + on_touch_down: + if self.collide_point(*args[1].pos): \ + root.updated_canvas(self, args[1]) + + Image: + source: f"{images_path}/yellow.png" + allow_stretch: True + keep_ratio: False + on_touch_down: + if self.collide_point(*args[1].pos): \ + root.updated_canvas(self, args[1]) + + Image: + source: f"{images_path}/red.png" + allow_stretch: True + keep_ratio: False + on_touch_down: + if self.collide_point(*args[1].pos): \ + root.updated_canvas(self, args[1]) + + Image: + source: f"{images_path}/black.png" + allow_stretch: True + keep_ratio: False + on_touch_down: + if self.collide_point(*args[1].pos): \ + root.updated_canvas(self, args[1]) + + SelectAlphaChannelWidget: + id: select_alpha_channel_widget + color_picker: root.color_picker + + + + rv: rv + + RecycleView: + id: rv + key_viewclass: "viewclass" + key_size: "height" + + RecycleBoxLayout: + orientation: "vertical" + size_hint_y: None + height: self.minimum_height + padding: "8dp" + spacing: "8dp" + default_size_hint: 1, None + default_size: None, dp(48) + + + + size_hint_y: None + padding: "12dp" + md_bg_color: root.color + radius: [8,] + + MDLabel: + text: root.hue_code + theme_text_color: "Custom" + text_color: root.text_color + halign: "center" + + + + # These are the sums of the widths of the `TypeColorButton` buttons in the + # `type_color_button_box` box. + size_hint_min_x: dp(264) + + MDBoxLayout: + orientation: "vertical" + + MDBoxLayout: + id: header + orientation: "vertical" + padding: 0, "8dp", 0, 0 + spacing: "8dp" + radius: root.radius[:2] + [0, 0] + size_hint_y: None + height: STANDARD_INCREMENT + md_bg_color: + app.theme_cls.primary_color \ + if not root.default_color \ + else root.default_color + + MDLabel: + id: lbl_color_value + halign: "center" + shorten: True + bold: True + markup: True + + MDBoxLayout: + id: type_color_button_box + adaptive_height: True + + TypeColorButton: + text: "HEX" + group: "x" + size_hint_x: 1 + on_release: root.type_color = self.text + + TypeColorButton: + text: "RGB" + group: "x" + size_hint_x: 1 + on_release: root.type_color = self.text + + TypeColorButton: + text: "RGBA" + group: "x" + size_hint_x: 1 + on_release: root.type_color = self.text + + MDBottomNavigation: + id: bottom_navigation + use_text: False + on_switch_tabs: root.dispatch("on_switch_tabs", *args) + + MDBottomNavigationItem: + id: bottom_navigation_gradient + name: "bottom navigation gradient" + icon: "gradient-vertical" + + MDBottomNavigationItem: + id: view_headline + name: "view headline" + icon: "view-headline" + + ColorListTab: + id: color_list_tabs + text_color_normal: 0, 0, 0, 1 + on_tab_switch: self.generates_list_colors(*args) + color_picker: root + + MDBottomNavigationItem: + id: tune + name: "tune" + icon: "tune" + + SliderTab: + color_picker: root + on_slide_value: + root.dispatch("on_select_color", args[1]) + + MDBoxLayout: + size_hint_y: None + height: "48dp" + md_bg_color: app.theme_cls.bg_dark + radius: [0, 0] + root.radius[2:] + + MDFlatButton: + text: root.text_button_ok + size_hint: 1, 1 + on_release: + root.dispatch( \ + "on_release", \ + root.type_color, \ + root._get_selected_color(root.selected_color)) + + MDFlatButton: + text: root.text_button_cancel + size_hint: 1, 1 + on_release: root.dismiss() diff --git a/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py b/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py new file mode 100644 index 0000000..37a0e52 --- /dev/null +++ b/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py @@ -0,0 +1,653 @@ +""" +Components/ColorPicker +====================== + +.. versionadded:: 1.0.0 + +.. rubric:: Create, share, and apply color palettes to your UI, as well as measure the accessibility level of any color combination.. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/color-picker-preview.png + :align: center + +Usage +----- + +.. code-block:: python + + from typing import Union + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.pickers import MDColorPicker + + KV = ''' + MDScreen: + + MDTopAppBar: + id: toolbar + title: "MDTopAppBar" + pos_hint: {"top": 1} + + MDRaisedButton: + text: "OPEN PICKER" + pos_hint: {"center_x": .5, "center_y": .5} + md_bg_color: toolbar.md_bg_color + on_release: app.open_color_picker() + ''' + + + class MyApp(MDApp): + def build(self): + return Builder.load_string(KV) + + def open_color_picker(self): + color_picker = MDColorPicker(size_hint=(0.45, 0.85)) + color_picker.open() + color_picker.bind( + on_select_color=self.on_select_color, + on_release=self.get_selected_color, + ) + + def update_color(self, color: list) -> None: + self.root.ids.toolbar.md_bg_color = color + + def get_selected_color( + self, + instance_color_picker: MDColorPicker, + type_color: str, + selected_color: Union[list, str], + ): + '''Return selected color.''' + + print(f"Selected color is {selected_color}") + self.update_color(selected_color[:-1] + [1]) + + def on_select_color(self, instance_gradient_tab, color: list) -> None: + '''Called when a gradient image is clicked.''' + + + MyApp().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/color-picker-usage.png + :align: center +""" + +import os +import struct +from io import BytesIO +from typing import List, Union + +from kivy.clock import Clock +from kivy.core.image import Image as CoreImage +from kivy.graphics import RoundedRectangle +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.behaviors import ButtonBehavior +from kivy.utils import get_color_from_hex, get_hex_from_color +from PIL import Image as PilImage +from PIL import ImageDraw + +from kivymd import uix_path +from kivymd.color_definitions import colors as _colors +from kivymd.color_definitions import text_colors +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import RectangularRippleBehavior +from kivymd.uix.behaviors.toggle_behavior import MDToggleButton +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.button import MDRaisedButton +from kivymd.uix.dialog import BaseDialog +from kivymd.uix.tab import MDTabs, MDTabsBase, MDTabsLabel + +__all__ = ("MDColorPicker",) + +with open( + os.path.join(uix_path, "pickers", "colorpicker", "colorpicker.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class TypeColorButton(MDRaisedButton, MDToggleButton): + """ + The class implements the button to switch the color type - + 'RGBA', 'HEX', 'RGB'. + """ + + theme_text_color = "Custom" + text_color = (0, 0, 0, 1) + elevation = 0 + + +class SelectAlphaChannelWidget(MDBoxLayout): + """ + The class implements the widget with the current color and slider to set + the value of the transparency of the selected color. + """ + + # :class:`~kivymd.uix.colorpicker.MDColorPicker` class. + color_picker = ObjectProperty() + + # The `RGB` value for the transparency preview widget of the selected + # color. + _rgb = ColorProperty([0, 0, 0, 0]) + # The opacity value for the transparency preview widget of the selected + # color. + _opacity_value_selected_color = NumericProperty(1) + + def on_color_picker( + self, instance_select_alpha_channel_widget, instance_color_picker + ) -> None: + instance_color_picker.bind(_rgb=self.set_scale_rgb) + + def set_scale_rgb( + self, + instance_color_picker, + color: Union[List[int], List[float]], + ) -> None: + if color[0] > 1: + self._rgb = [x / 255.0 for x in color] + else: + self._rgb = color + + +class SliderTab(MDBoxLayout): + """ + The class has implemented `RGB` value sliders and a scale for setting the + transparency value of the selected color. This is the third tab on the + bottom navigation panel. + """ + + # :class:`~kivymd.uix.colorpicker.MDColorPicker` class. + color_picker = ObjectProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_slide_value") + + def get_color(self) -> List[float]: + return [ + self.ids.slider_red.ids.slider.value / 255, + self.ids.slider_green.ids.slider.value / 255, + self.ids.slider_blue.ids.slider.value / 255, + self.color_picker._opacity_value_selected_color, + ] + + def on_slide_value(self, *args) -> None: + """Basic event handler for changing the slider value.""" + + +class GradientTab(ThemableBehavior, MDBoxLayout): + """ + The class implements a tab with a gradient, a color selection scale and + a scale for setting the transparency value of the selected color. + This is the first tab on the bottom navigation panel. + """ + + # :class:`~kivymd.uix.colorpicker.MDColorPicker` class. + color_picker = ObjectProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.rectangle = None + self.texture = None + Clock.schedule_once(lambda x: self.create_gradient_texture()) + Clock.schedule_once(self.create_canvas_with_gradient_texture) + + def create_gradient_texture( + self, r_g_b=None, interval: Union[int, float] = 0 + ) -> None: + """ + Creates a gradient value buffer and texture object. + Called when clicking on the gradient bar to the right. + """ + + # TODO: Perhaps there is a better way to create a gradient. + # The implementation using the PIL package is most likely not the most + # better. In any case, performance tests should be carried out. + gradient_widget_width = int(self.ids.gradient_widget.width) + gradient_widget_height = int(self.ids.gradient_widget.height - dp(100)) + img = PilImage.new( + "RGBA", (gradient_widget_width, gradient_widget_height), "#FFFFFF" + ) + draw = ImageDraw.Draw(img) + + if not self.color_picker.default_color: + r, g, b = ( + r_g_b + if r_g_b + else self.color_picker.get_rgb(self.theme_cls.primary_color) + ) + else: + r, g, b = [ + int(value * 255) + for value in self.color_picker.default_color[:-1] + ] + self.color_picker._rgb = [r, g, b] + ( + r_adjacent_color_constant, + g_adjacent_color_constant, + b_adjacent_color_constant, + ) = ( + self.color_picker.adjacent_color_constants + if r_g_b != (0, 0, 0) + else (0.40, 0.40, 0.40) # if the selected color is black + ) + + for i in range(gradient_widget_width): + r, g, b = ( + r + r_adjacent_color_constant, + g + g_adjacent_color_constant, + b + b_adjacent_color_constant, + ) + draw.line( + (i, 0, i, gradient_widget_width), fill=(int(r), int(g), int(b)) + ) + + data = BytesIO() + img.save(data, format="png") + data.seek(0) + self.texture = CoreImage(BytesIO(data.read()), ext="png").texture + + def create_canvas_with_gradient_texture( + self, interval: Union[int, float] + ) -> None: + """Creates a canvas with a gradient texture.""" + + with self.ids.color_selection_box.canvas: + self.rectangle = RoundedRectangle( + texture=self.texture, + pos=self.ids.gradient_widget.pos, + size=self.ids.gradient_widget.size, + radius=self.color_picker.radius, + group="gradient", + ) + self.bind( + size=lambda instance, size: Clock.schedule_once( + lambda dt: self._update_canvas(instance, size) + ) + ) + + def get_rgba_color_from_touch_region(self, widget, touch) -> List[int]: + """ + Returns the color of the pixel in the gradient that was clicked. + """ + + pixel = widget.texture.get_region(*touch.pos, 1, 1) + rgba = struct.unpack("4B", pixel.pixels) + return rgba + + def updated_canvas(self, widget, touch, color=None) -> None: + """ + Called when clicking on the gradient bar to the right. + Updates the color of the gradient texture. + """ + + if self.color_picker.default_color: + self.color_picker.default_color = None + + self.ids.color_selection_box.canvas.remove_group("gradient") + if not color: + # (0-255, 0-255, 0-255, 0-255) + color = self.get_rgba_color_from_touch_region(widget, touch) + self.create_gradient_texture(color[:-1]) + self.color_picker.dispatch( + "on_select_color", [x / 255.0 for x in color] + ) + else: + self.create_gradient_texture(color) + self.create_canvas_with_gradient_texture(0) + + def on_touch_down(self, touch): + """Handles the ``self.ids.gradient_widget`` touch event.""" + + if self.ids.gradient_widget.collide_point(*touch.pos): + color = self.get_rgba_color_from_touch_region(self, touch) + self.color_picker.dispatch( + "on_select_color", [x / 255.0 for x in color] + ) + return super().on_touch_down(touch) + + def _update_canvas(self, instance_gradient_widget, size: list) -> None: + self.rectangle.size = self.ids.gradient_widget.size + self.rectangle.pos = self.ids.gradient_widget.pos + + +class TabColorList(MDBoxLayout, MDTabsBase): + """Implements a tab for :class:`~ColorListTab` class.""" + + +class ColorListTab(MDTabs): + """ + The class implements a tab with tabs with a list of colors. + This is the second tab on the bottom navigation panel. + """ + + # :class:`~kivymd.uix.colorpicker.MDColorPicker` class. + color_picker = ObjectProperty() + + def generates_list_colors( + self, + instance_color_list_tab, + instance_tab_color_list: TabColorList, + instance_tabs_label: MDTabsLabel, + tab_label_text: str, + ) -> None: + """ + Generates list of colors. + Called when you click the tab of :class:`~TabColorList` class. + """ + + if not tab_label_text: + tab_label_text = "Red" + if not instance_tab_color_list.rv.data: + for hue in _colors[tab_label_text]: + color = get_color_from_hex(_colors[tab_label_text][hue]) + if tab_label_text == "Light": + text_color = (0, 0, 0, 1) + elif tab_label_text == "Dark": + text_color = (1, 1, 1, 1) + else: + text_color = text_colors[tab_label_text][hue] + instance_tab_color_list.rv.data.append( + { + "viewclass": "ColorListItem", + "color": color, + "hue_code": hue, + "text_color": text_color, + "on_press": lambda x=color: self.on_press_color_item(x), + } + ) + + def on_press_color_item(self, color: list) -> None: + """Called when you click on the color item from the list of colors.""" + + rgb = [int(value * 255) for value in color[:-1]] + self.color_picker._rgb = rgb + self.background_color = color + self.color_picker.dispatch("on_select_color", color) + + +class ColorListItem(RectangularRippleBehavior, ButtonBehavior, MDBoxLayout): + """Implements the item for the list of :class:`~TabColorList` class.""" + + color = ColorProperty() + text_color = ColorProperty() + hue_code = StringProperty() + + +class MDColorPicker(BaseDialog): + adjacent_color_constants = ListProperty([0.299, 0.887, 0.411]) + """ + A list of values that are used to create the gradient. These values are + selected empirically. Each of these values will be added to the selected + ``RGB`` value, thus creating colors that are close in value. + + :attr:`adjacent_color_constants` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0.299, 0.887, 0.411]`. + """ + + default_color = ColorProperty(None, allownone=True) + """ + Default color value The set color value will be used when you open the + dialog. + + :attr:`default_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + type_color = OptionProperty("RGB", options=["RGBA", "HEX", "RGB"]) + """ + Type of color. + Available options are: `'RGBA'`, `'HEX'`, `'RGB'`. + + :attr:`type_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'RGB'`. + """ + + background_down_button_selected_type_color = ColorProperty([1, 1, 1, 0.3]) + """ + Button background for choosing a color type ('RGBA', 'HEX', 'HSL', 'RGB'). + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/color-picker-background-down-button-selected-type-color.png + :align: center + + :attr:`background_down_button_selected_type_color` is an + :class:`~kivy.properties.ColorProperty` and defaults to `[1, 1, 1, 0.3]`. + """ + + radius_color_scale = VariableListProperty([8]) + """ + The radius value for the color scale. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/color-picker-gradient-scale-radius.png + :align: center + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[8, 8, 8, 8]`. + """ + + text_button_ok = StringProperty("SELECT") + """ + Color selection button text. + + :attr:`text_button_ok` is an :class:`~kivy.properties.StringProperty` + and defaults to `'SELECT'`. + """ + + text_button_cancel = StringProperty("CANCEL") + """ + Cancel button text. + + :attr:`text_button_cancel` is an :class:`~kivy.properties.StringProperty` + and defaults to `'CANCEL'`. + """ + + selected_color = None + # One of the objects of classes: + # :class:`~GradientTab`, :class:`~ColorListTab`, :class:`~SliderTab`. + _current_tab = ObjectProperty() + # The `RGB` value for the transparency preview widget of the selected + # color. + _rgb = ListProperty() + # The opacity value for the transparency preview widget of the selected + # color. + _opacity_value_selected_color = NumericProperty(1) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.gradient_tab = None + self.register_event_type("on_select_color") + self.register_event_type("on_switch_tabs") + self.register_event_type("on_release") + self.on_background_down_button_selected_type_color( + None, self.background_down_button_selected_type_color + ) + + self.on_background_down_button_selected_type_color( + None, self.background_down_button_selected_type_color + ) + Clock.schedule_once(lambda x: self.on_type_color(self), 1) + + def update_color_slider_item_bottom_navigation(self, color: list) -> None: + """ + Updates the color of the slider that sets the transparency value of the + selected color and the color of bottom navigation items. + """ + + if "select_alpha_channel_widget" in self._current_tab.ids: + self._current_tab.ids.select_alpha_channel_widget.ids.slider.color = ( + color + ) + self.ids.bottom_navigation.text_color_active = color + + def update_color_type_buttons(self, color: list) -> None: + """ + Updating button colors (display buttons of type of color) to match the + selected color. + """ + + for instance_toggle_button in self.ids.type_color_button_box.children: + if instance_toggle_button.state != "down": + instance_toggle_button.md_bg_color = color + instance_toggle_button.background_normal = color + + def get_rgb(self, color: list) -> list: + """Returns an ``RGB`` list of values from 0 to 255.""" + + return [ + int(value * 255) + for value in (color[:-1] if len(color) == 4 else color) + ] + + def on_background_down_button_selected_type_color( + self, instance_color_picker, color: list + ) -> None: + def set_background_down(interval: Union[float, int]) -> None: + for ( + instance_toggle_button + ) in self.ids.type_color_button_box.children: + instance_toggle_button.background_down = color + if self.type_color == instance_toggle_button.text: + instance_toggle_button.state = "down" + + Clock.schedule_once(set_background_down) + + def on_type_color( + self, + instance_color_picker, + type_color: str = "", + interval: Union[float, int] = 0, + ) -> None: + """Called when buttons are clicked to set the color type.""" + + if not type_color: + type_color = self.type_color + + if self._rgb: + rgb = self._rgb if self._rgb[0] > 1 else self.get_rgb(self._rgb) + opacity = self._opacity_value_selected_color + color = "" + + if type_color == "RGB": + self.selected_color = [value for value in rgb] + color = f"RGB({', '.join([str(value) for value in self.selected_color])})" + elif type_color == "RGBA": + self.selected_color = [x / 255.0 for x in rgb] + [opacity] + color = f"RGBA({', '.join([str(x / 255.0) for x in rgb])}, {opacity})" + elif type_color == "HEX": + self.selected_color = get_hex_from_color( + [x / 255.0 for x in rgb] + [opacity] + ) + color = f"HEX({self.selected_color})" + + self.ids.lbl_color_value.text = color + + def on_open(self) -> None: + """Default open event handler.""" + + if not self.ids.bottom_navigation_gradient.children: + self.gradient_tab = GradientTab(color_picker=self) + self._current_tab = self.gradient_tab + self.ids.bottom_navigation_gradient.add_widget(self.gradient_tab) + + def on_select_color(self, color: list) -> None: + """Called when a gradient image is clicked.""" + + if len(color) == 3: + color += [self._opacity_value_selected_color] + + self.ids.header.md_bg_color = color + self._rgb = color[:-1] + self.on_type_color(self, self.type_color) + self.update_color_type_buttons(color) + self.update_color_slider_item_bottom_navigation(color) + + def on_switch_tabs( + self, + bottom_navigation_instance, + bottom_navigation_item_instance, + name_tab, + ) -> None: + """Called when switching tabs of bottom navigation.""" + + if name_tab == "bottom navigation gradient": + self._current_tab = self.gradient_tab + bottom_navigation_item_instance.children[0].updated_canvas( + None, + None, + self._rgb if self._rgb[0] > 1 else self.get_rgb(self._rgb), + ) + instance_slider_tab = ( + bottom_navigation_instance.ids.tab_manager.get_screen( + "tune" + ).children[0] + ) + select_alpha_channel_widget = ( + self.gradient_tab.ids.select_alpha_channel_widget + ) + select_alpha_channel_widget.ids.slider.value = ( + instance_slider_tab.ids.select_alpha_channel_widget.ids.slider.value + ) + select_alpha_channel_widget.ids.slider.color = [ + x / 255.0 for x in self._rgb + ] + [1] + elif name_tab == "tune": + if self._rgb[0] <= 1: + color = self.get_rgb(self._rgb) + else: + color = self._rgb + instance_slider_tab = self.ids.tune.children[0] + self._current_tab = instance_slider_tab + instance_slider_tab.ids.slider_red.ids.slider.value = color[0] + instance_slider_tab.ids.slider_green.ids.slider.value = color[1] + instance_slider_tab.ids.slider_blue.ids.slider.value = color[2] + instance_slider_tab.ids.select_alpha_channel_widget.ids.slider.value = ( + self._opacity_value_selected_color + ) + elif name_tab == "view headline": + color = self._rgb + [1] + color_list_tabs = self.ids.view_headline.children[0] + self._current_tab = color_list_tabs + try: + color_list_tabs.background_color = color + except ValueError: + color_list_tabs.background_color = [x / 255.0 for x in color][ + :-1 + ] + [1] + if not color_list_tabs.get_tab_list(): + for color in _colors.keys(): + tab_widget = TabColorList(title=str(color)) + color_list_tabs.add_widget(tab_widget) + + def on_release(self, *args): + """Called when the `SELECT` button is pressed""" + + def _get_selected_color(self, selected_color: Union[list, str]) -> list: + """ + Convert [0-255, 0-255, 0-255] and '#rrggbb' to kivy color format. + Return kivy color format. + """ + + rgba = [0, 0, 0, 0] + if isinstance(selected_color, list): + if selected_color[0] > 1: + rgba = [x / 255.0 for x in selected_color] + [ + self._opacity_value_selected_color + ] + else: + rgba = selected_color + elif isinstance(selected_color, str): + rgba = get_color_from_hex(selected_color)[:-1] + [ + self._opacity_value_selected_color + ] + return rgba diff --git a/sbapp/kivymd/uix/pickers/datepicker/__init__.py b/sbapp/kivymd/uix/pickers/datepicker/__init__.py new file mode 100644 index 0000000..994878c --- /dev/null +++ b/sbapp/kivymd/uix/pickers/datepicker/__init__.py @@ -0,0 +1,5 @@ +from .datepicker import ( # NOQA F401 + BaseDialogPicker, + DatePickerInputField, + MDDatePicker, +) diff --git a/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv b/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv new file mode 100644 index 0000000..8f95e58 --- /dev/null +++ b/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv @@ -0,0 +1,517 @@ +#:import os os +#:import date datetime.date +#:import calendar calendar +#:import platform platform +#:import Clock kivy.clock.Clock +#:import images_path kivymd.images_path + + + + on_enter: + self.tooltip_text = "" if self.owner \ + and self.owner._input_date_dialog_open \ + or self.owner._select_year_dialog_open \ + else self.hint_text + + + + + + + _calendar_layout: _calendar_layout + size_hint: None, None + size: + (dp(328), dp(512) - root._shift_dialog_height) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(528), dp(328) - root._shift_dialog_height) + + MDRelativeLayout: + id: container + background: os.path.join(images_path, "transparent.png") + + canvas: + Color: + rgb: + app.theme_cls.primary_color \ + if not root.primary_color else root.primary_color + RoundedRectangle: + size: + (dp(328), dp(120)) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(168), dp(328) - root._shift_dialog_height) + pos: + (0, root.height - dp(120)) \ + if root.theme_cls.device_orientation == "portrait" \ + else (0, 0) + radius: + (root.radius[0], root.radius[1], dp(0), dp(0)) \ + if root.theme_cls.device_orientation == "portrait" \ + else (root.radius[0], dp(0), dp(0), root.radius[3]) + Color: + rgba: + app.theme_cls.bg_normal \ + if not root.accent_color else root.accent_color + RoundedRectangle: + size: + (dp(328), dp(512) - dp(120) - root._shift_dialog_height) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(360), dp(328) - root._shift_dialog_height) + pos: + (0, 0) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(168), 0) + radius: + (dp(0), dp(0), root.radius[2], root.radius[3]) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(0), root.radius[1], root.radius[2], dp(0)) + + MDLabel: + id: label_title + font_style: "Body2" + bold: True + theme_text_color: "Custom" + size_hint_x: None + width: root.width + adaptive_height: True + text: root.title + font_name: root.font_name + pos: + (dp(24), root.height - self.height - dp(18)) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(24), root.height - self.height - dp(24)) + text_color: + root.specific_text_color \ + if not root.text_toolbar_color else root.text_toolbar_color + + MDLabel: + id: label_full_date + font_style: "H4" + theme_text_color: "Custom" + size_hint_x: None + width: root.width + adaptive_height: True + font_name: root.font_name + markup: True + pos: + (dp(24), root.height - dp(120) + dp(18)) \ + if root.theme_cls.device_orientation == "portrait" \ + else \ + ( \ + dp(24) if not root._input_date_dialog_open else dp(168) + dp(24), \ + root.height - self.height - dp(96) \ + ) + text: + root.set_text_full_date(root.sel_year, root.sel_month, root.sel_day, \ + root.theme_cls.device_orientation) + text_color: + ( \ + root.specific_text_color \ + if not root.text_toolbar_color else root.text_toolbar_color \ + ) \ + if root.theme_cls.device_orientation == "portrait" \ + else \ + ( \ + ( \ + self.theme_cls.primary_color \ + if not root.primary_color else root.primary_color \ + ) \ + if root._input_date_dialog_open \ + else \ + ( \ + root.specific_text_color \ + if not root.text_toolbar_color else root.text_toolbar_color \ + ) \ + ) + + RecycleView: + id: _year_layout + key_viewclass: "viewclass" + size_hint: None, None + size: _calendar_layout.size + pos: _calendar_layout.pos + disabled: True + + canvas.before: + PushMatrix + Scale: + x: root._scale_year_layout + y: root._scale_year_layout + origin: self.center + canvas.after: + PopMatrix + + SelectYearList: + cols: 3 + default_size: dp(170), dp(36) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + + MDIconButton: + id: edit_icon + icon: "pencil" + icon_size: "24sp" + theme_icon_color: "Custom" + on_release: + root.transformation_to_dialog_input_date() \ + if not root._input_date_dialog_open else \ + Clock.schedule_once(root.transformation_from_dialog_input_date, .15) + x: + (root.width - self.width - dp(12)) \ + if root.theme_cls.device_orientation == "portrait" \ + else dp(12) + y: + (root.height - dp(120) + dp(12)) \ + if root.theme_cls.device_orientation == "portrait" \ + else dp(12) + text_color: + root.specific_text_color \ + if not root.text_toolbar_color else root.text_toolbar_color + + MDLabel: + id: label_month_selector + font_style: "Body2" + -text_size: None, None + theme_text_color: "Custom" + adaptive_size: True + text: calendar.month_name[root.month].capitalize() + " " + str(root.year) + font_name: root.font_name + pos: + (dp(24), root.height - dp(120) - self.height - dp(20)) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(168) + dp(24), label_title.y) + text_color: + app.theme_cls.text_color \ + if not root.text_color else root.text_color + + DatePickerIconTooltipButton: + id: triangle + owner: root + icon: "menu-down" + ripple_scale: .5 + theme_icon_color: "Custom" + hint_text: "Choose year" + on_release: + root.transformation_to_dialog_select_year() \ + if not root._select_year_dialog_open else \ + root.transformation_from_dialog_select_year() + pos: + (label_month_selector.width + dp(14), root.height - dp(123) - self.height) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(180) + label_month_selector.width, label_title.y - dp(14)) + text_color: + app.theme_cls.text_color \ + if not root.text_color else root.text_color + md_bg_color_disabled: 0, 0, 0, 0 + + DatePickerIconTooltipButton: + id: chevron_left + owner: root + icon: "chevron-left" + on_release: root.change_month("prev") + theme_icon_color: "Custom" + hint_text: "Previous month" + x: + dp(228) if root.theme_cls.device_orientation == "portrait" \ + else dp(418) + y: + root.height - dp(120) - self.height / 2 - dp(30) \ + if root.theme_cls.device_orientation == "portrait" \ + else dp(272) + text_color: + app.theme_cls.text_color \ + if not root.text_color else root.text_color + + DatePickerIconTooltipButton: + id: chevron_right + owner: root + icon: "chevron-right" + on_release: root.change_month("next") + theme_icon_color: "Custom" + hint_text: "Next month" + x: + dp(272) if root.theme_cls.device_orientation == "portrait" \ + else dp(464) + y: + root.height - dp(120) - self.height / 2 - dp(30) \ + if root.theme_cls.device_orientation == "portrait" \ + else dp(272) + text_color: + app.theme_cls.text_color \ + if not root.text_color else root.text_color + + # TODO: Replace the GridLayout with a RecycleView + # if it improves performance. + GridLayout: + id: _calendar_layout + cols: 7 + size_hint: None, None + 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) + 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: + (dp(10), dp(56)) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(168) + dp(20), dp(44)) + + canvas.before: + PushMatrix + Scale: + x: root._scale_calendar_layout + y: root._scale_calendar_layout + origin: self.center + canvas.after: + PopMatrix + + MDFlatButton: + id: ok_button + width: dp(32) + pos: root.width - self.width, dp(10) + text: "OK" + theme_text_color: "Custom" + font_name: root.font_name + text_color: + root.theme_cls.primary_color \ + if not root.text_button_color else \ + root.text_button_color + on_release: root.on_ok_button_pressed() + + MDFlatButton: + id: cancel_button + text: "CANCEL" + on_release: root.dispatch("on_cancel", None) + theme_text_color: "Custom" + pos: root.width - self.width - ok_button.width - dp(10), dp(10) + font_name: root.font_name + text_color: + root.theme_cls.primary_color \ + if not root.text_button_color else \ + root.text_button_color + + + + size_hint: None, None + size: + (dp(42), dp(42)) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(32), dp(32)) + disabled: True + + canvas: + Color: + rgba: + ( \ + ( \ + self.theme_cls.primary_color if not root.owner.selector_color \ + else root.owner.selector_color \ + ) \ + if root.is_selected and not self.disabled \ + else (0, 0, 0, 0) \ + ) \ + if self.owner.mode != "range" else \ + ( \ + ( \ + self.theme_cls.primary_color if not root.owner.selector_color \ + else root.owner.selector_color \ + ) \ + if root.is_selected and not self.disabled \ + and (self.owner.mode == "range" and self.owner._start_range_date) \ + else (0, 0, 0, 0) \ + ) + Ellipse: + size: + (dp(42), dp(42)) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(32), dp(32)) + pos: self.pos + + # Fill marking the available dates of the range, if using the `range` mode + # or use `min_date/max_date`. + canvas.before: + Color: + rgba: + (\ + self.owner.selector_color[:-1] + [.3] \ + if self.owner.selector_color \ + else self.theme_cls.primary_color[:-1] + [.3] \ + ) \ + if not self.disabled \ + and self.text \ + and self.check_date(self.owner.year, self.owner.month, int(self.text)) \ + else (0, 0, 0, 0) + RoundedRectangle: + size: + (dp(44), dp(32)) \ + if root.theme_cls.device_orientation == "portrait" \ + else \ + (dp(32), dp(28)) \ + if self.index in [6, 13, 20, 27, 30] or self.owner._date_range \ + and self.text and self.owner._date_range[-1] == date( \ + self.current_year, \ + self.current_month, \ + int(self.text) \ + ) \ + else (dp(46), dp(28)) + pos: + (self.x - dp(1.5), self.y + dp(5)) \ + if root.theme_cls.device_orientation == "portrait" else \ + (self.x, self.y + 1) + radius: + [0, 0, 0, 0] if not self.owner._date_range else \ + ( \ + [self.width / 2, 0, 0, self.width / 2] \ + if self.text and self.owner._date_range[0] == date( \ + self.current_year, \ + self.current_month, \ + int(self.text) \ + ) \ + or (self.index in [0, 7, 14, 21, 28] and root.is_selected) \ + else \ + ( \ + [0, 0, 0, 0] if self.text \ + and self.owner._date_range[-1] != date( \ + self.current_year, \ + self.current_month, \ + int(self.text) \ + ) \ + and self.index not in [6, 13, 20, 27, 30] \ + else [0, self.width / 2, self.width, 0] \ + if root.is_selected or self.text \ + and self.owner._date_range[-1] == date( \ + self.current_year, \ + self.current_month, \ + int(self.text) \ + ) \ + else [0, 0, 0, 0]) \ + ) + + # Circle marking the beginning and end of the date range if the "range" + # mode is used. + Color: + rgba: + [0, 0, 0, 0] if not self.owner._date_range else \ + ( + ( \ + self.theme_cls.primary_color if not root.owner.selector_color \ + else root.owner.selector_color \ + ) \ + if self.text and self.owner._date_range[0] == date( \ + self.current_year, \ + self.current_month, \ + int(self.text) \ + ) \ + or \ + self.text and self.owner._date_range[-1] == date( \ + self.current_year, \ + self.current_month, \ + int(self.text) \ + ) \ + else (0, 0, 0, 0) \ + ) + Ellipse: + size: + (dp(42), dp(42)) \ + if root.theme_cls.device_orientation == "portrait" \ + else (dp(32), dp(32)) + pos: self.pos + + MDLabel: + font_style: "Caption" + size_hint_x: None + halign: "center" + text: root.text + font_name: root.owner.font_name + theme_text_color: "Custom" + text_color: + ( \ + root.theme_cls.primary_color \ + if not root.owner.text_current_color \ + else root.owner.text_current_color \ + ) \ + if root.is_today and not root.is_selected \ + else ( \ + ( \ + root.theme_cls.text_color \ + if not root.is_selected or root.owner.mode == "range" \ + else (1, 1, 1, 1) \ + ) \ + if not root.owner.text_color \ + else \ + ( \ + root.owner.text_color \ + if not root.is_selected else (1, 1, 1, 1)) \ + ) + + + + font_style: "Caption" + theme_text_color: "Custom" + size_hint: None, None + text_size: self.size + halign: "center" + valign: + "middle" if root.theme_cls.device_orientation == "portrait" \ + else "center" + size: + (dp(40), dp(40)) if root.theme_cls.device_orientation == "portrait" \ + else (dp(32), dp(32)) + text_color: + app.theme_cls.disabled_hint_text_color \ + if not root.owner.text_weekday_color else root.owner.text_weekday_color + + + + font_style: "Caption" + size_hint_x: None + valign: "middle" + halign: "center" + text: root.text + on_text: root.font_name = root.owner.font_name + + canvas.before: + Color: + rgba: + root.selected_color if root.selected_color \ + else self.theme_cls.primary_color + RoundedRectangle: + pos: self.x + dp(12), self.y + size: self.width - dp(24), self.height + radius: [root.height / 2, ] + + + + adaptive_height: True + size_hint_x: None + spacing: dp(8) + width: + self.owner.width - dp(48) \ + if root.owner.theme_cls.device_orientation == "portrait" \ + else self.owner.width - dp(168) - dp(48) + y: + self.owner.height - dp(123) - self.height - dp(20) \ + if root.owner.theme_cls.device_orientation == "portrait" \ + else self.owner.height - self.height - dp(24) + x: + dp(24) if root.owner.theme_cls.device_orientation == "portrait" \ + else dp(168) + dp(24) + + + + mode: "fill" + opacity: 0 + hint_text: "dd/mm/yyyy" + input_filter: root.input_filter + fill_color: + (0, 0, 0, .15) \ + if not self.owner.input_field_background_color \ + else root.owner.input_field_background_color diff --git a/sbapp/kivymd/uix/pickers/datepicker/datepicker.py b/sbapp/kivymd/uix/pickers/datepicker/datepicker.py new file mode 100644 index 0000000..50e80de --- /dev/null +++ b/sbapp/kivymd/uix/pickers/datepicker/datepicker.py @@ -0,0 +1,1441 @@ +""" +Components/DatePicker +===================== + +.. seealso:: + + `Material Design spec, Date picker `_ + +.. rubric:: Includes date picker. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/picker-previous.png + :align: center + +.. warning:: The widget is under testing. Therefore, we would be grateful if + you would let us know about the bugs found. + +.. rubric:: Usage + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.pickers import MDDatePicker + + KV = ''' + MDFloatLayout: + + MDTopAppBar: + title: "MDDatePicker" + pos_hint: {"top": 1} + elevation: 10 + + MDRaisedButton: + text: "Open date picker" + pos_hint: {'center_x': .5, 'center_y': .5} + on_release: app.show_date_picker() + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def on_save(self, instance, value, date_range): + ''' + Events called when the "OK" dialog box button is clicked. + + :type instance: ; + + :param value: selected date; + :type value: ; + + :param date_range: list of 'datetime.date' objects in the selected range; + :type date_range: ; + ''' + + print(instance, value, date_range) + + def on_cancel(self, instance, value): + '''Events called when the "CANCEL" dialog box button is clicked.''' + + def show_date_picker(self): + date_dialog = MDDatePicker() + date_dialog.bind(on_save=self.on_save, on_cancel=self.on_cancel) + date_dialog.open() + + + Test().run() + + +.. 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(year=1983, month=4, day=12) + date_dialog.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/previous-date.png + :align: center + +Interval date +------------- + +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): + date_dialog = MDDatePicker( + min_date=datetime.date(2021, 2, 15), + max_date=datetime.date(2021, 3, 27), + ) + date_dialog.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/range-date.gif + :align: center + +The range of available dates can be changed in the picker dialog: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/change-range-date.gif + :align: center + +Select year +----------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/select-year-date.gif + :align: center + +.. warning:: The list of years when opening is not automatically set + to the current year. + +You can set the range of years using the :attr:`~kivymd.uix.picker.MDDatePicker.min_year` and +:attr:`~kivymd.uix.picker.MDDatePicker.max_year` attributes: + +.. code-block:: python + + def show_date_picker(self): + date_dialog = MDDatePicker(min_year=2021, max_year=2030) + date_dialog.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/min-max-year-date.png + :align: center + +Set and select a date range +--------------------------- + +.. code-block:: python + + def show_date_picker(self): + date_dialog = MDDatePicker(mode="range") + date_dialog.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/set-select-range-date.gif + :align: center +""" + +__all__ = ("MDDatePicker", "BaseDialogPicker", "DatePickerInputField") + +import calendar +import datetime +import os +import time +from datetime import date +from typing import Union + +from kivy import Logger +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, + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.behaviors import ButtonBehavior, FocusBehavior +from kivy.uix.recyclegridlayout import RecycleGridLayout +from kivy.uix.recycleview.layout import LayoutSelectionBehavior +from kivy.uix.recycleview.views import RecycleDataViewBehavior + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior, ThemeManager +from kivymd.toast import toast +from kivymd.uix.behaviors import ( + CircularRippleBehavior, + FakeRectangularElevationBehavior, + SpecificBackgroundColorBehavior, +) +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.button import MDIconButton +from kivymd.uix.dialog import BaseDialog +from kivymd.uix.label import MDLabel +from kivymd.uix.textfield import MDTextField +from kivymd.uix.tooltip import MDTooltip + +with open( + os.path.join(uix_path, "pickers", "datepicker", "datepicker.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class BaseDialogPicker( + BaseDialog, + FakeRectangularElevationBehavior, + SpecificBackgroundColorBehavior, +): + """ + Base class for :class:`~kivymd.uix.picker.MDDatePicker` and + :class:`~kivymd.uix.picker.MDTimePicker` classes. + + :Events: + `on_save` + Events called when the "OK" dialog box button is clicked. + `on_cancel` + Events called when the "CANCEL" dialog box button is clicked. + """ + + title_input = StringProperty("INPUT DATE") + """ + Dialog title fot input date. + + .. code-block:: python + + MDDatePicker(title_input="INPUT DATE") + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-input-date.png + :align: center + + :attr:`title_input` is an :class:`~kivy.properties.StringProperty` + and defaults to `INPUT DATE`. + """ + + title = StringProperty("SELECT DATE") + """ + Dialog title fot select date. + + .. code-block:: python + + MDDatePicker(title="SELECT DATE") + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-select-date.png + :align: center + + :attr:`title` is an :class:`~kivy.properties.StringProperty` + and defaults to `SELECT DATE`. + """ + + radius = ListProperty([7, 7, 7, 7]) + """ + Radius list for the four corners of the dialog. + + .. code-block:: python + + MDDatePicker(radius=[7, 7, 7, 26]) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-radius.png + :align: center + + :attr:`radius` is an :class:`~kivy.properties.ListProperty` + and defaults to `[7, 7, 7, 7]`. + """ + + primary_color = ColorProperty(None) + """ + Background color of toolbar in (r, g, b, a) format. + + .. code-block:: python + + MDDatePicker(primary_color=get_color_from_hex("#72225b")) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/primary-color-date.png + :align: center + + :attr:`primary_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + accent_color = ColorProperty(None) + """ + Background color of calendar/clock face in (r, g, b, a) format. + + .. code-block:: python + + MDDatePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/accent-color-date.png + :align: center + + :attr:`accent_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + selector_color = ColorProperty(None) + """ + Background color of the selected day of the month or hour in (r, g, b, a) format. + + .. code-block:: python + + MDDatePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + selector_color=get_color_from_hex("#e93f39"), + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selector-color-date.png + :align: center + + :attr:`selector_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text_toolbar_color = ColorProperty(None) + """ + Color of labels for text on a toolbar in (r, g, b, a) format. + + .. code-block:: python + + MDDatePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + selector_color=get_color_from_hex("#e93f39"), + text_toolbar_color=get_color_from_hex("#cccccc"), + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-toolbar-color-date.png + :align: center + + :attr:`text_toolbar_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text_color = ColorProperty(None) + """ + Color of text labels in calendar/clock face in (r, g, b, a) format. + + .. code-block:: python + + MDDatePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + selector_color=get_color_from_hex("#e93f39"), + text_toolbar_color=get_color_from_hex("#cccccc"), + text_color=("#ffffff"), + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-color-date.png + :align: center + + :attr:`text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text_current_color = ColorProperty(None) + """ + Color of the text of the current day of the month/hour in (r, g, b, a) format. + + .. code-block:: python + + MDDatePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + selector_color=get_color_from_hex("#e93f39"), + text_toolbar_color=get_color_from_hex("#cccccc"), + text_color=("#ffffff"), + text_current_color=get_color_from_hex("#e93f39"), + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-current-color-date.png + :align: center + + :attr:`text_current_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text_button_color = ColorProperty(None) + """ + Text button color in (r, g, b, a) format. + + .. code-block:: python + + MDDatePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + selector_color=get_color_from_hex("#e93f39"), + text_toolbar_color=get_color_from_hex("#cccccc"), + text_color=("#ffffff"), + text_current_color=get_color_from_hex("#e93f39"), + text_button_color=(1, 1, 1, .5), + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-button-color-date.png + :align: center + + :attr:`text_button_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + input_field_background_color = ColorProperty(None) + """ + Background color of input fields in (r, g, b, a) format. + + .. code-block:: python + + MDDatePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + selector_color=get_color_from_hex("#e93f39"), + text_toolbar_color=get_color_from_hex("#cccccc"), + text_color=("#ffffff"), + text_current_color=get_color_from_hex("#e93f39"), + input_field_background_color=(1, 1, 1, 0.2), + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-field-background-color-date.png + :align: center + + :attr:`input_field_background_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + input_field_text_color = ColorProperty(None) + """ + Text color of input fields in (r, g, b, a) format. + + Background color of input fields. + + .. code-block:: python + + MDDatePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + selector_color=get_color_from_hex("#e93f39"), + text_toolbar_color=get_color_from_hex("#cccccc"), + text_color=("#ffffff"), + text_current_color=get_color_from_hex("#e93f39"), + input_field_background_color=(1, 1, 1, 0.2), + input_field_text_color=(1, 1, 1, 1), + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-field-background-color-date.png + :align: center + + :attr:`input_field_text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + font_name = StringProperty("Roboto") + """ + Font name for dialog window text. + + .. code-block:: python + + MDDatePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + selector_color=get_color_from_hex("#e93f39"), + text_toolbar_color=get_color_from_hex("#cccccc"), + text_color=("#ffffff"), + text_current_color=get_color_from_hex("#e93f39"), + input_field_background_color=(1, 1, 1, 0.2), + input_field_text_color=(1, 1, 1, 1), + font_name="Weather.ttf", + + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-name-date.png + :align: center + + :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_save") + self.register_event_type("on_cancel") + + def on_save(self, *args) -> None: + """Events called when the "OK" dialog box button is clicked.""" + + self.dismiss() + + def on_cancel(self, *args) -> None: + """Events called when the "CANCEL" dialog box button is clicked.""" + + self.dismiss() + + +class DatePickerBaseTooltip(MDTooltip): + """Implements tooltips for members of the :class:`~MDDatePicker` class.""" + + owner = ObjectProperty() # MDDatePicker object + hint_text = StringProperty() + + +class DatePickerIconTooltipButton(MDIconButton, DatePickerBaseTooltip): + pass + + +class DatePickerWeekdayLabel(MDLabel, DatePickerBaseTooltip): + pass + + +class DatePickerTypeDateError(Exception): + pass + + +class DatePickerInputField(MDTextField): + """Implements date input in dd/mm/yyyy format.""" + + helper_text_mode = StringProperty("on_error") + owner = ObjectProperty() # MDDatePicker object + + def set_error(self): + """Sets a text field to an error state.""" + + self.error = True + + def input_filter(self, value: str, boolean: bool) -> Union[str, None]: + """Filters the input according to the specified mode.""" + + if self.is_numeric(value): + return value + + def is_numeric(self, value: str) -> bool: + """ + Returns true if the value of the `value` argument can be converted + to an integer, or if the value of the `value` argument is '/'. + """ + + try: + if value == "/": + return True + int(value) + return True + except ValueError: + return False + + def get_list_date(self) -> list: + """ + Returns a list as `[dd, mm, yyyy]` from a text fied for entering a date. + """ + + return [d for d in self.text.split("/") if d] + + +class DatePickerInputFieldContainer(MDBoxLayout): + owner = ObjectProperty() # MDDatePicker object + + +class SelectYearList(FocusBehavior, LayoutSelectionBehavior, RecycleGridLayout): + """A class that implements a list for choosing a year.""" + + +class DatePickerDaySelectableItem( + ThemableBehavior, CircularRippleBehavior, ButtonBehavior, AnchorLayout +): + """A class that implements a list for choosing a day.""" + + text = StringProperty() + owner = ObjectProperty() + is_today = BooleanProperty(False) + is_selected = BooleanProperty(False) + current_month = NumericProperty() + current_year = NumericProperty() + index = NumericProperty(0) + + def check_date(self, year: int, month: int, day: int): + try: + return date(year, month, day) in self.owner._date_range + except ValueError as error: + if str(error) == "day is out of range for month": + return False + + def on_release(self): + if ( + self.owner.mode == "range" + and self.owner._end_range_date + and self.owner._start_range_date + ): + return + if ( + not self.owner._input_date_dialog_open + and not self.owner._select_year_dialog_open + ): + if self.owner.mode == "range" and not self.owner._start_range_date: + self.owner._start_range_date = date( + self.current_year, self.current_month, int(self.text) + ) + self.owner.min_date = self.owner._start_range_date + elif ( + self.owner.mode == "range" + and not self.owner._end_range_date + and self.owner._start_range_date + ): + self.owner._end_range_date = date( + self.current_year, self.current_month, int(self.text) + ) + if self.owner._end_range_date <= self.owner.min_date: + toast(self.owner.date_range_text_error) + Logger.error( + "`Data Picker: max_date` value cannot be less than " + "or equal to 'min_date' value." + ) + self.owner._start_range_date = 0 + self.owner._end_range_date = 0 + return + self.owner.max_date = self.owner._end_range_date + self.owner.update_calendar_for_date_range() + + self.owner.set_selected_widget(self) + + +class DatePickerYearSelectableItem(RecycleDataViewBehavior, MDLabel): + """Implements an item for a pick list of the year.""" + + index = None + selected = BooleanProperty(False) + selectable = BooleanProperty(True) + selected_color = ColorProperty([0, 0, 0, 0]) + owner = ObjectProperty() + + def refresh_view_attrs(self, rv, index, data): + self.index = index + return super().refresh_view_attrs(rv, index, data) + + def on_touch_down(self, touch): + if super().on_touch_down(touch): + return True + if self.collide_point(*touch.pos) and self.selectable: + self.owner.year = int(self.text) + # self.owner.sel_year = self.owner.year + self.owner.ids.label_full_date.text = self.owner.set_text_full_date( + self.owner.sel_year, + self.owner.sel_month, + self.owner.sel_day, + self.owner.theme_cls.device_orientation, + ) + return self.parent.select_with_touch(self.index, touch) + + def apply_selection(self, table_data, index, is_selected): + self.selected = is_selected + if is_selected: + self.selected_color = ( + self.owner.selector_color + if self.owner.selector_color + else self.theme_cls.primary_color + ) + self.text_color = (1, 1, 1, 1) + else: + if int(self.text) == self.owner.sel_year: + self.text_color = ( + self.theme_cls.primary_color + if not self.owner.text_current_color + else self.owner.text_current_color + ) + self.selected_color = [0, 0, 0, 0] + self.text_color = (0, 0, 0, 1) + + +# TODO: Add the feature to embed the `MDDatePicker` class in other layouts +# and not use it as a modal dialog. +# Add a date input mask. Currently, the date is entered in the format +# 'dd/mm/yy'. In some countries, the date is formatted as 'mm/dd/yy'. +class MDDatePicker(BaseDialogPicker): + text_weekday_color = ColorProperty(None) + """ + Text color of weekday names in (r, g, b, a) format. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-text-weekday-color.png + :align: center + + :attr:`text_weekday_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + helper_text = StringProperty("Wrong date") + """ + Helper text when entering an invalid date. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-date-picker-helper-text.png + :align: center + + :attr:`helper_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Wrong date'`. + """ + + day = NumericProperty() + """ + The day of the month to be opened by default. If not specified, + the current number will be used. + + See `Open date dialog with the specified date `_ for more information. + + :attr:`day` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + month = NumericProperty() + """ + The number of month to be opened by default. If not specified, + the current number will be used. + + See `Open date dialog with the specified date `_ for more information. + + :attr:`month` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + year = NumericProperty() + """ + The year of month to be opened by default. If not specified, + the current number will be used. + + See `Open date dialog with the specified date `_ for more information. + + :attr:`year` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + min_year = NumericProperty(1914) + """ + The year of month to be opened by default. If not specified, + the current number will be used. + + :attr:`min_year` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1914`. + """ + + max_year = NumericProperty(2121) + """ + The year of month to be opened by default. If not specified, + the current number will be used. + + :attr:`max_year` is an :class:`~kivy.properties.NumericProperty` + and defaults to `2121`. + """ + + mode = OptionProperty("picker", options=["picker", "range"]) + """ + Dialog type:`'picker'` type allows you to select one date; + `'range'` type allows to set a range of dates from which the + user can select a date. + Available options are: [`'picker'`, `'range'`]. + + :attr:`mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `picker`. + """ + + min_date = ObjectProperty() + """ + The minimum value of the date range for the `'mode`' parameter. + Must be an object . + + See `Open date dialog with the specified date `_ for more information. + + :attr:`min_date` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + max_date = ObjectProperty() + """ + The minimum value of the date range for the `'mode`' parameter. + Must be an object . + + See `Open date dialog with the specified date `_ for more information. + + :attr:`max_date` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + date_range_text_error = StringProperty("Error date range") + """ + Error text that will be shown on the screen in the form of a toast if the + minimum date range exceeds the maximum. + + :attr:`date_range_text_error` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Error date range'`. + """ + + input_field_cls = ObjectProperty(DatePickerInputField) + """ + A class that will implement date input in the format dd/mm/yyyy. + See :class:`~DatePickerInputField` class for more information. + + .. code-block:: python + + class CustomInputField(MDTextField): + owner = ObjectProperty() # required attribute + + # Required method. + def set_error(self): + [...] + + # Required method. + def get_list_date(self): + [...] + + # Required method. + def input_filter(self): + [...] + + def show_date_picker(self): + date_dialog = MDDatePicker(input_field_cls=CustomInputField) + + :attr:`input_field_cls` is an :class:`~kivy.properties.ObjectProperty` + and defaults to :class:`~DatePickerInputField`. + """ + + sel_year = NumericProperty() + sel_month = NumericProperty() + sel_day = NumericProperty() + + _calendar_layout = ObjectProperty() + _calendar_list = None + _enter_data_field = None + _enter_data_field_two = None + _enter_data_field_container = None + _date_range = [] + _sel_day_widget = ObjectProperty() + _scale_calendar_layout = NumericProperty(1) + _scale_year_layout = NumericProperty(0) + _shift_dialog_height = NumericProperty(0) + _input_date_dialog_open = BooleanProperty(False) + _select_year_dialog_open = False + _start_range_date = 0 + _end_range_date = 0 + + def __init__( + self, + year=None, + month=None, + day=None, + firstweekday=0, + **kwargs, + ): + self.today = date.today() + self.calendar = 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._current_selected_date = ( + self.sel_day, + self.sel_month, + self.sel_year, + ) + super().__init__(**kwargs) + self.theme_cls.bind(device_orientation=self.on_device_orientation) + + if self.max_date and self.min_date: + if self.min_date and not isinstance(self.min_date, date): + raise DatePickerTypeDateError( + "'min_date' must be of class " + ) + if self.max_date and not isinstance(self.max_date, date): + raise DatePickerTypeDateError( + "'max_date' must be of class " + ) + self.compare_date_range() + self._date_range = self.get_date_range() + + self.generate_list_widgets_days() + self.update_calendar(self.sel_year, self.sel_month) + + if ( + not self.max_date + and not self.min_date + and not self._date_range + and self.mode != "range" + ): + # Mark the current day. + self.set_month_day(self.sel_day) + self._sel_day_widget.dispatch("on_release") + + def on_device_orientation( + self, instance_theme_manager: ThemeManager, orientation_value: str + ) -> None: + """Called when the device's screen orientation changes.""" + + if self._input_date_dialog_open: + if orientation_value == "portrait": + self._shift_dialog_height = dp(250) + if orientation_value == "landscape": + self._shift_dialog_height = dp(138) + + def on_ok_button_pressed(self) -> None: + """ + Called when the 'OK' button is pressed to confirm the date entered. + """ + + if self._enter_data_field and not self.is_date_valaid( + self._enter_data_field.text + ): + self._enter_data_field.set_error() + return + if self._enter_data_field_two and not self.is_date_valaid( + self._enter_data_field_two.text + ): + self._enter_data_field_two.set_error() + return + + self.dispatch( + "on_save", + date(self.sel_year, self.sel_month, self.sel_day), + self._date_range, + ) + + def is_date_valaid(self, date: str) -> bool: + """Checks the valid of the currently entered date.""" + + try: + time.strptime(date, "%d/%m/%Y") + return True + except ValueError: + return False + + def transformation_from_dialog_select_year(self) -> None: + self.ids.chevron_left.disabled = False + self.ids.chevron_right.disabled = False + self.ids._year_layout.disabled = True + self.ids.triangle.disabled = False + self._select_year_dialog_open = False + self.ids.triangle.icon = "menu-down" + + Animation(opacity=1, d=0.15).start(self.ids.chevron_left) + Animation(opacity=1, d=0.15).start(self.ids.chevron_right) + Animation(_scale_year_layout=0, d=0.15).start(self) + Animation( + _shift_dialog_height=dp(0), _scale_calendar_layout=1, d=0.15 + ).start(self) + + self._calendar_layout.clear_widgets() + self.generate_list_widgets_days() + self.update_calendar(self.year, self.month) + + if self.mode != "range": + self.set_month_day(self.day) + self._sel_day_widget.dispatch("on_release") + + def transformation_to_dialog_select_year(self) -> None: + def disabled_chevron_buttons(*args): + self.ids.chevron_left.disabled = True + self.ids.chevron_right.disabled = True + + self._select_year_dialog_open = True + self.ids._year_layout.disabled = False + self._scale_calendar_layout = 0 + Animation(opacity=0, d=0.15).start(self.ids.chevron_left) + Animation(opacity=0, d=0.15).start(self.ids.chevron_right) + anim = Animation(_scale_year_layout=1, d=0.15) + anim.bind(on_complete=disabled_chevron_buttons) + anim.start(self) + self.ids.triangle.icon = "menu-up" + self.generate_list_widgets_years() + self.set_position_to_current_year() + + def transformation_to_dialog_input_date(self) -> None: + def set_date_to_input_field(): + if not self._enter_data_field_two: + # Date of current day. + self._enter_data_field.text = ( + f"{'' if self.sel_day >= 10 else '0'}" + f"{self.sel_day}/" + f"{'' if self.sel_month >= 10 else '0'}" + f"{self.sel_month}/{self.sel_year}" + ) + else: + # Range start date. + self._enter_data_field.text = ( + f"{'' if self.min_date.day >= 10 else '0'}" + f"{self.min_date.day}/" + f"{'' if self.min_date.month >= 10 else '0'}" + f"{self.min_date.month}/{self.min_date.year}" + ) + + def set_date_to_input_field_two() -> None: + # Range end date. + self._enter_data_field_two.text = ( + f"{'' if self.max_date.day >= 10 else '0'}" + f"{self.max_date.day}/" + f"{'' if self.max_date.month >= 10 else '0'}" + f"{self.max_date.month}/{self.max_date.year}" + ) + + self.ids.triangle.disabled = True + if self._select_year_dialog_open: + self.transformation_from_dialog_select_year() + self._input_date_dialog_open = True + + self._enter_data_field_container = DatePickerInputFieldContainer( + owner=self + ) + self._enter_data_field = self.get_field() + if self.min_date and self.max_date: + self._enter_data_field_two = self.get_field() + set_date_to_input_field_two() + set_date_to_input_field() + self._enter_data_field_container.add_widget(self._enter_data_field) + if self._enter_data_field_two: + self._enter_data_field_container.add_widget( + self._enter_data_field_two + ) + + self.ids.container.add_widget(self._enter_data_field_container) + self.ids.edit_icon.icon = "calendar" + self.ids.label_title.text = self.title_input + + Animation( + _shift_dialog_height=dp(250) + if self.theme_cls.device_orientation == "portrait" + else dp(138), + _scale_calendar_layout=0, + d=0.15, + ).start(self) + Animation( + opacity=0, + d=0.15 if self.theme_cls.device_orientation == "portrait" else 0, + ).start(self.ids.chevron_left) + Animation( + opacity=0, + d=0.15 if self.theme_cls.device_orientation == "portrait" else 0, + ).start(self.ids.chevron_right) + Animation(opacity=0, d=0.15).start(self.ids.label_month_selector) + Animation(opacity=0, d=0.15).start(self.ids.triangle) + Animation(opacity=1, d=0.15).start(self._enter_data_field) + if self._enter_data_field_two: + Animation(opacity=1, d=0.15).start(self._enter_data_field_two) + self.ids.label_full_date.text = self.set_text_full_date( + self.sel_year, + self.sel_month, + self.sel_day, + self.theme_cls.device_orientation, + ) + + def transformation_from_dialog_input_date( + self, interval: Union[int, float] + ) -> None: + self._input_date_dialog_open = False + self.ids.label_full_date.text = self.set_text_full_date( + self.sel_year, + self.sel_month, + self.sel_day, + self.theme_cls.device_orientation, + ) + self.ids.triangle.disabled = False + self.ids.container.remove_widget(self._enter_data_field_container) + Animation( + _shift_dialog_height=dp(0), _scale_calendar_layout=1, d=0.15 + ).start(self) + Animation( + opacity=1, + d=0.15 if self.theme_cls.device_orientation == "portrait" else 0.65, + ).start(self.ids.chevron_left) + Animation( + opacity=1, + d=0.15 if self.theme_cls.device_orientation == "portrait" else 0.65, + ).start(self.ids.chevron_right) + Animation(opacity=1, d=0.15).start(self.ids.label_month_selector) + Animation(opacity=1, d=0.15).start(self.ids.triangle) + Animation(opacity=0, d=0.15).start(self._enter_data_field) + self.ids.edit_icon.icon = "pencil" + self.ids.label_title.text = self.title + + if not self.min_date and not self.max_date: + list_date = self._enter_data_field.get_list_date() + if len(list_date) == 3 and len(list_date[2]) == 4: + # self._sel_day_widget.is_selected = False + self.update_calendar(int(list_date[2]), int(list_date[1])) + self.set_month_day(int(list_date[0])) + # self._sel_day_widget.dispatch("on_release") + if self.mode != "range": + self._sel_day_widget.is_selected = False + self._sel_day_widget.dispatch("on_release") + elif self.min_date and self.max_date: + list_min_date = self._enter_data_field.get_list_date() + list_max_date = self._enter_data_field_two.get_list_date() + + if len(list_min_date) == 3 and len(list_min_date[2]) == 4: + self.min_date = date( + int(list_min_date[2]), + int(list_min_date[1]), + int(list_min_date[0]), + ) + if len(list_max_date) == 3 and len(list_max_date[2]) == 4: + self.max_date = date( + int(list_max_date[2]), + int(list_max_date[1]), + int(list_max_date[0]), + ) + + self.update_calendar_for_date_range() + self.ids.label_full_date.text = self.set_text_full_date( + int(list_max_date[2]), + int(list_max_date[1]), + int(list_max_date[0]), + self.theme_cls.device_orientation, + ) + + def compare_date_range(self) -> None: + # TODO: Add behavior if the minimum date range exceeds the maximum + # date range. Use toast? + if self.max_date <= self.min_date: + raise DatePickerTypeDateError( + "`max_date` value cannot be less than or equal " + "to 'min_date' value" + ) + + def update_calendar_for_date_range(self) -> None: + # self.compare_date_range() + self._date_range = self.get_date_range() + self._calendar_layout.clear_widgets() + self.generate_list_widgets_days() + self.update_calendar(self.year, self.month) + + def update_text_full_date(self, list_date) -> None: + """ + Updates the title of the week, month and number day name + in an open date input dialog. + """ + + if len(list_date) == 1 and len(list_date[0]) == 2: + self.ids.label_full_date.text = self.set_text_full_date( + self.sel_year, + self.sel_month, + list_date[0], + self.theme_cls.device_orientation, + ) + if len(list_date) == 2 and len(list_date[1]) == 2: + self.ids.label_full_date.text = self.set_text_full_date( + self.sel_year, + int(list_date[1]), + int(list_date[0]), + self.theme_cls.device_orientation, + ) + if len(list_date) == 3 and len(list_date[2]) == 4: + self.ids.label_full_date.text = self.set_text_full_date( + int(list_date[2]), + int(list_date[1]), + int(list_date[0]), + self.theme_cls.device_orientation, + ) + + def update_calendar(self, year, month) -> None: + try: + dates = [x for x in self.calendar.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._calendar_list)): + self._calendar_list[idx].current_month = int(self.month) + self._calendar_list[idx].current_year = int(self.year) + + # Dates of the month not in the range 1-31. + if idx >= len(dates) or dates[idx].month != month: + # self._calendar_list[idx].disabled = True + self._calendar_list[idx].text = "" + # Dates of the month in the range 1-31. + else: + self._calendar_list[idx].disabled = False + self._calendar_list[idx].text = str(dates[idx].day) + self._calendar_list[idx].is_today = dates[idx] == self.today + # The marked date widget has a True value in the `is_selected` + # attribute. In the KV file it is checked if the date widget + # (DatePickerDaySelectableItem) has the `is_selected = False` + # attribute value, then the date widget is not highlighted. + if ( + 0 + if not self._calendar_list[idx].text + else int(self._calendar_list[idx].text), + self._calendar_list[idx].current_month, + self._calendar_list[idx].current_year, + ) == self._current_selected_date: + self._calendar_list[idx].is_selected = True + else: + self._calendar_list[idx].is_selected = False + # Dates outside the set range - disabled. + if ( + self.mode == "picker" + and self._date_range + and self._calendar_list[idx].text + ) or ( + self.mode == "range" + and self._start_range_date + and self._end_range_date + and self._calendar_list[idx].text + ): + if ( + date( + self._calendar_list[idx].current_year, + self._calendar_list[idx].current_month, + int(self._calendar_list[idx].text), + ) + not in self._date_range + ): + self._calendar_list[idx].disabled = True + + def get_field(self) -> MDTextField: + """Creates and returns a text field object used to enter dates.""" + + if issubclass(self.input_field_cls, MDTextField): + field = self.input_field_cls( + owner=self, + helper_text=self.helper_text, + line_color_normal=self.theme_cls.divider_color, + ) + field.color_mode = "custom" + field.line_color_focus = ( + self.theme_cls.primary_color + if not self.input_field_text_color + else self.input_field_text_color + ) + field.current_hint_text_color = field.line_color_focus + field._current_hint_text_color = field.line_color_focus + return field + else: + raise TypeError( + "The `input_field_cls` parameter must be an object of the " + "`kivymd.uix.textfield.MDTextField class`" + ) + + def get_date_range(self) -> list: + date_range = [ + self.min_date + datetime.timedelta(days=x) + for x in range((self.max_date - self.min_date).days + 1) + ] + return date_range + + def set_text_full_date(self, year, month, day, orientation): + """ + Returns a string of type "Tue, Feb 2" or "Tue,\nFeb 2" for a date + choose and a string like "Feb 15 - Mar 23" or "Feb 15,\nMar 23" for + a date range. + """ + + if 12 < int(month) < 0: + raise ValueError( + "set_text_full_date:\n\t" f"Month [{month}] out of range." + ) + if int(day) > calendar.monthrange(int(year), (month))[1]: + raise ValueError( + "set_text_full_date:\n\t" + f"Day [{day}] out of range for the month {month}" + ) + date = datetime.date(int(year), int(month), int(day)) + separator = ( + "\n" + if (orientation == "landscape" and not self._input_date_dialog_open) + else " " + ) + + if self.mode == "picker": + if not self.min_date and not self.max_date: + return ( + date.strftime("%a,").capitalize() + + separator + + date.strftime("%b ").capitalize() + + str(day).lstrip("0") + ) + else: + return ( + self.min_date.strftime("%b ").capitalize() + + str(self.min_date.day).lstrip("0") + + ( + " - " + if orientation == "portrait" + else ( + ",\n" if not self._input_date_dialog_open else ", " + ) + ) + + self.max_date.strftime("%b ").capitalize() + + str(self.max_date.day).lstrip("0") + ) + elif self.mode == "range": + if self._start_range_date and self._end_range_date: + if ( + orientation == "landscape" + and "-" in self.ids.label_full_date.text + ): + return ( + self.ids.label_full_date.text.split("-")[0].strip() + + (",\n" if not self._input_date_dialog_open else " - ") + + date.strftime("%b ").capitalize() + + str(day).lstrip("0") + ) + else: + if ( + orientation == "landscape" + and "," in self.ids.label_full_date.text + ): + return ( + self.ids.label_full_date.text.split(",")[0].strip() + + ( + ",\n" + if not self._input_date_dialog_open + else "-" + ) + + date.strftime("%b ").capitalize() + + str(day).lstrip("0") + ) + if ( + orientation == "portrait" + and "," in self.ids.label_full_date.text + ): + return ( + self.ids.label_full_date.text.split(",")[0].strip() + + "-" + + date.strftime("%b ").capitalize() + + str(day).lstrip("0") + ) + if ( + orientation == "portrait" + and "-" in self.ids.label_full_date.text + ): + return ( + self.ids.label_full_date.text.split("-")[0].strip() + + " - " + + date.strftime("%b ").capitalize() + + str(day).lstrip("0") + ) + elif self._start_range_date and not self._end_range_date: + return ( + ( + date.strftime("%b ").capitalize() + + str(day).lstrip("0") + + " - End" + ) + if orientation != "landscape" + else ( + date.strftime("%b ").capitalize() + + str(day).lstrip("0") + + "{}End".format( + ",\n" if not self._input_date_dialog_open else " - " + ) + ) + ) + elif not self._start_range_date and not self._end_range_date: + return ( + "Start - End" + if orientation != "landscape" + else "Start{}End".format( + ",\n" if not self._input_date_dialog_open else " - " + ) + ) + + def set_selected_widget(self, widget) -> None: + 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._current_selected_date = ( + self.sel_day, + self.sel_month, + self.sel_year, + ) + self._sel_day_widget = widget + + def set_month_day(self, day) -> None: + for idx in range(len(self._calendar_list)): + if str(day) == str(self._calendar_list[idx].text): + self._sel_day_widget = self._calendar_list[idx] + self.sel_day = int(self._calendar_list[idx].text) + if self._sel_day_widget: + self._sel_day_widget.is_selected = False + self._sel_day_widget = self._calendar_list[idx] + + def set_position_to_current_year(self) -> None: + # TODO: Add the feature to set the position of the list of years + # for the current year. This is not currently possible because the + # ``RecycleView`` class does not support this functionality. + # There is a solution to this problem + # - https://github.com/Bakterija/log_fruit/blob/dev/src/app_modules/widgets/app_recycleview/recycleview.py. + # But I have not been able to get it to work. + pass + + def generate_list_widgets_years(self) -> None: + for i, number_year in enumerate(range(self.min_year, self.max_year)): + self.ids._year_layout.data.append( + { + "owner": self, + "text": str(number_year), + "index": i, + "selectable": True, + "viewclass": "DatePickerYearSelectableItem", + } + ) + + def generate_list_widgets_days(self) -> None: + calendar_list = [] + + for day in self.calendar.iterweekdays(): + weekday_label = DatePickerWeekdayLabel( + text=calendar.day_name[day][0].upper(), + owner=self, + hint_text=calendar.day_name[day], + ) + weekday_label.font_name = self.font_name + self._calendar_layout.add_widget(weekday_label) + for i, j in enumerate(range(6 * 7)): # 6 weeks, 7 days a week + day_selectable_item = DatePickerDaySelectableItem( + index=i, + owner=self, + current_month=int(self.month), + current_year=int(self.year), + ) + calendar_list.append(day_selectable_item) + self._calendar_layout.add_widget(day_selectable_item) + self._calendar_list = calendar_list + + def change_month(self, operation: str) -> None: + """ + Called when "chevron-left" and "chevron-right" buttons are pressed. + Switches the calendar to the previous/next month. + """ + + operation = 1 if operation == "next" else -1 + month = ( + 12 + if self.month + operation == 0 + else 1 + if self.month + operation == 13 + else self.month + operation + ) + year = ( + self.year - 1 + if self.month + operation == 0 + else self.year + 1 + if self.month + operation == 13 + else self.year + ) + self.update_calendar(year, month) + if self.sel_day: + x = calendar.monthrange(year, month)[1] + if x < self.sel_day: + self.sel_day = ( + x if year <= self.sel_year and month <= self.sel_year else 1 + ) diff --git a/sbapp/kivymd/uix/pickers/timepicker/__init__.py b/sbapp/kivymd/uix/pickers/timepicker/__init__.py new file mode 100644 index 0000000..8e776c3 --- /dev/null +++ b/sbapp/kivymd/uix/pickers/timepicker/__init__.py @@ -0,0 +1 @@ +from .timepicker import MDTimePicker # NOQA F401 diff --git a/sbapp/kivymd/uix/pickers/timepicker/timepicker.kv b/sbapp/kivymd/uix/pickers/timepicker/timepicker.kv new file mode 100644 index 0000000..7844326 --- /dev/null +++ b/sbapp/kivymd/uix/pickers/timepicker/timepicker.kv @@ -0,0 +1,340 @@ +: + theme_text_color: "Custom" + font_size: dp(10) + halign: "left" + valign: "bottom" + adaptive_size: True + + + + halign: "center" + valign: "center" + theme_text_color: "Custom" + + + + size_hint: None, None + + canvas.before: + Color: + rgba: root.border_color + RoundedRectangle: + pos: self.pos + size: self.size + radius: [root.border_radius, ] + + #AM + Color: + rgba: root._am_bg_color + RoundedRectangle: + pos: + [ \ + self.pos[0] + root.border_width, \ + self.pos[1] + self.height/2 + self.border_width * 0.5 \ + ] if self.orientation == "vertical" else \ + [ \ + self.pos[0] + root.border_width, \ + self.pos[1] + root.border_width \ + ] + size: + [ \ + self.size[0] - root.border_width * 2, \ + self.size[1] / 2 - self.border_width * 1.5 \ + ] if self.orientation == "vertical" else \ + [ \ + self.size[0] / 2 - root.border_width * 1.5, \ + self.size[1] - root.border_width * 2 \ + ] + radius: + [root.border_radius, root.border_radius, 0, 0] \ + if self.orientation == "vertical" else \ + [root.border_radius, 0, 0, root.border_radius] + + #PM + Color: + rgba: root._pm_bg_color + RoundedRectangle: + pos: + [ \ + self.pos[0] + root.border_width, \ + self.pos[1] + self.border_width \ + ] if self.orientation == "vertical" else \ + [ \ + self.pos[0] + root.size[0] / 2 + root.border_width / 2, \ + self.pos[1] + root.border_width \ + ] + size: + [ \ + self.size[0] - root.border_width * 2, \ + self.size[1] / 2 - self.border_width * 1.5 \ + ] if self.orientation == "vertical" else \ + [ \ + self.size[0] / 2 - root.border_width * 1.5, \ + self.size[1] - root.border_width * 2 \ + ] + radius: + [0, 0, root.border_radius, root.border_radius] \ + if self.orientation == "vertical" else \ + [0 ,root.border_radius, root.border_radius, 0] + + # AM + AmPmSelectorLabel: + text: "AM" + on_release: root.selected = "am" + text_color: root.text_color + + AmPmSelectorLabel: + text: "PM" + on_release: root.selected = "pm" + text_color: root.text_color + + + + size_hint: None, 1 + width: dp(96) + mode: "fill" + active_line: False + font_size: dp(56) + radius: [dp(10), ] + fill_color_normal: + root.parent.parent.parent.accent_color \ + if root.parent.parent.parent.accent_color else \ + ( \ + [*root.parent.bg_color_active[:3], 0.5] \ + if root.parent.state in ["hour", "minute"] else \ + [*root.bg_color[:3], 0.5] \ + ) + fill_color_focus: + (1, 1, 1, 0.5) \ + if root.parent.parent.parent.primary_color else \ + self.theme_cls.bg_dark + text_color_focus: + root.parent.parent.parent.accent_color \ + if root.parent.parent.parent.accent_color else \ + self.theme_cls.primary_color + + + + size_hint: None, None + _hour: hour + _minute: minute + + TimeInputTextField: + id: hour + num_type: "hour" + pos: 0, 0 + text_color: root.text_color + disabled: root.disabled + on_text: root.dispatch("on_time_input") + radius: root.hour_radius + on_select: + root.dispatch("on_hour_select") + root.state = "hour" + + MDLabel: + text: ":" + size_hint: None, None + size: dp(24), dp(80) + halign: "center" + valign: "center" + font_size: dp(50) + pos: dp(96), 0 + theme_text_color: "Custom" + text_color: root.text_color + + TimeInputTextField: + id: minute + num_type: "minute" + pos: dp(120), 0 + text_color: root.text_color + disabled: root.disabled + on_text: root.dispatch("on_time_input") + radius: root.minute_radius + on_select: + root.dispatch("on_minute_select") + root.state = "minute" + + + + circular_padding: dp(28) + size_hint: None, None + size: [dp(256), dp(256)] + row_spacing: dp(40) + + canvas.before: + PushMatrix + Scale: + origin: self.scale_origin + x: root.scale + y: root.scale + Color: + rgba: root.bg_color + Ellipse: + size: self.size + pos: self.pos + PushMatrix + Scale: + origin: self.center + x: root.content_scale + y: root.content_scale + Color: + rgb: root.selector_color + a: 0 if self.selector_pos == [0, 0] else 1 + Ellipse: + size: self.selector_size, self.selector_size + pos: + [self.selector_pos[0] - self.selector_size / 2, \ + self.selector_pos[1] - self.selector_size / 2] + Ellipse: + size: dp(10), dp(10) + pos: [self.center[0] - dp(5), self.center[1] - dp(5)] + Line: + points: [self.center, self.selector_pos] + width: dp(1) + canvas.after: + PopMatrix + PopMatrix + + + + halign: "center" + valign: "center" + adaptive_size: True + theme_text_color: "Custom" + + + + auto_dismiss: True + size_hint: None, None + _time_input: _time_input + _selector: _selector + _am_pm_selector: _am_pm_selector + _minute_label: _minute_label + _hour_label: _hour_label + + MDRelativeLayout: + canvas.before: + Color: + rgba: + root.primary_color \ + if root.primary_color \ + else root.theme_cls.bg_normal + + RoundedRectangle: + size: self.size + radius: root.radius + + MDLabel: + id: label_title + font_style: "Body2" + bold: True + theme_text_color: "Custom" + size_hint_x: None + width: root.width + adaptive_height: True + text: root.title + font_name: root.font_name + pos: (dp(24), root.height - self.height - dp(18)) + text_color: + root.text_toolbar_color if root.text_toolbar_color \ + else root.theme_cls.text_color + + TimeInput: + id: _time_input + bg_color: + root.accent_color if root.accent_color else \ + root.theme_cls.primary_light + bg_color_active: + root.selector_color if root.selector_color \ + else root.theme_cls.primary_color + text_color: + root.input_field_text_color if root.input_field_text_color else \ + root.theme_cls.text_color + on_time_input: root._get_time_input(*self.get_time()) + on_hour_select: _selector.switch_mode("hour") + on_minute_select: _selector.switch_mode("minute") + minute_radius: root.minute_radius + hour_radius: root.hour_radius + + TimeInputLabel: + id: _hour_label + text: "Hour" + opacity: 0 + text_color: + root.text_toolbar_color if root.text_toolbar_color else \ + root.theme_cls.secondary_text_color + + TimeInputLabel: + id: _minute_label + text: "Minute" + opacity: 0 + text_color: + root.text_toolbar_color if root.text_toolbar_color else \ + root.theme_cls.secondary_text_color + + AmPmSelector: + id: _am_pm_selector + owner: root + border_color: + root.accent_color if root.accent_color else \ + root.theme_cls.primary_color + border_radius: root.am_pm_radius + bg_color: + root.primary_color if root.primary_color else \ + root.theme_cls.bg_normal + border_width: root.am_pm_border_width + bg_color_active: + root.selector_color if root.selector_color else \ + root.theme_cls.primary_light + text_color: + root.input_field_text_color if root.input_field_text_color else \ + root.theme_cls.text_color + on_selected: root._get_am_pm(self.selected) + + CircularSelector: + id: _selector + text_color: + root.text_color if root.text_color else \ + root.theme_cls.text_color + bg_color: + root.accent_color if root.accent_color else \ + root.theme_cls.primary_light + selector_color: + root.primary_color if root.primary_color else \ + root.theme_cls.primary_color + font_name: root.font_name + on_selector_change: root._get_dial_time(_selector) + + MDIconButton: + id: input_clock_switch + icon: "keyboard" + pos: dp(12), dp(8) + theme_icon_color: "Custom" + icon_size: "24dp" + on_release: root._switch_input() + icon_color: + root.text_toolbar_color if root.text_toolbar_color else \ + root.theme_cls.secondary_text_color + + MDFlatButton: + id: cancel_button + text: "CANCEL" + on_release: root.dispatch("on_cancel", None) + theme_text_color: "Custom" + pos: root.width - self.width - ok_button.width - dp(10), dp(10) + font_name: root.font_name + text_color: + root.theme_cls.primary_color \ + if not root.text_button_color else root.text_button_color + + MDFlatButton: + id: ok_button + width: dp(32) + pos: root.width - self.width, dp(10) + text: "OK" + theme_text_color: "Custom" + font_name: root.font_name + text_color: + root.theme_cls.primary_color \ + if not root.text_button_color else root.text_button_color + on_release: root.dispatch("on_save", root._get_data()) diff --git a/sbapp/kivymd/uix/pickers/timepicker/timepicker.py b/sbapp/kivymd/uix/pickers/timepicker/timepicker.py new file mode 100644 index 0000000..748637e --- /dev/null +++ b/sbapp/kivymd/uix/pickers/timepicker/timepicker.py @@ -0,0 +1,792 @@ +""" +Components/TimePicker +===================== + +.. seealso:: + + `Material Design spec, Time picker `_ + +.. rubric:: Includes time picker. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/picker-previous.png + :align: center + +.. warning:: The widget is under testing. Therefore, we would be grateful if + you would let us know about the bugs found. + +.. rubric:: Usage + +.. code-block:: + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.pickers import MDTimePicker + + KV = ''' + MDFloatLayout: + + 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.png + :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() + +.. note:: For customization of the :class:`~MDTimePicker` class, see the + documentation in the :class:`~kivymd.uix.pickers.datepicker.datepicker.BaseDialogPicker` class. + +.. code-block:: python + + time_dialog = MDTimePicker( + primary_color=get_color_from_hex("#72225b"), + accent_color=get_color_from_hex("#5d1a4a"), + text_button_color=(1, 1, 1, 1), + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-customization.png + :align: center +""" + +__all__ = ("MDTimePicker",) + +import datetime +import os +import re +import time +from typing import List, Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.event import EventDispatcher +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.behaviors import ButtonBehavior +from kivy.vector import Vector + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.circularlayout import MDCircularLayout +from kivymd.uix.label import MDLabel +from kivymd.uix.pickers.datepicker import BaseDialogPicker +from kivymd.uix.relativelayout import MDRelativeLayout +from kivymd.uix.textfield import MDTextField + +with open( + os.path.join(uix_path, "pickers", "timepicker", "timepicker.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class AmPmSelectorLabel(ButtonBehavior, MDLabel): + pass + + +class AmPmSelector(ThemableBehavior, MDBoxLayout): + border_radius = NumericProperty() + border_color = ColorProperty() + bg_color = ColorProperty() + bg_color_active = ColorProperty() + border_width = NumericProperty() + am = ObjectProperty() + am = ObjectProperty() + owner = ObjectProperty() + text_color = ColorProperty() + selected = StringProperty() + + _am_bg_color = ColorProperty() + _pm_bg_color = ColorProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.bind(selected=self._upadte_color) + Clock.schedule_once(self._upadte_color) + + def _upadte_color(self, *args): + bg_color = ( + self.owner.accent_color + if self.owner.accent_color + else self.bg_color_active + ) + if self.selected == "am": + self._am_bg_color = bg_color + self._pm_bg_color = ( + self.owner.primary_color + if self.owner.accent_color + else self.bg_color + ) + elif self.selected == "pm": + self._am_bg_color = ( + self.owner.primary_color + if self.owner.accent_color + else self.bg_color + ) + self._pm_bg_color = bg_color + + +class TimeInputTextField(MDTextField): + num_type = OptionProperty("hour", options=["hour", "minute"]) + hour_regx = "^[0-9]$|^0[1-9]$|^1[0-2]$" + minute_regx = "^[0-9]$|^0[0-9]$|^[1-5][0-9]$" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self.set_text) + self.register_event_type("on_select") + self.bind(text_color=self.setter("hint_text_color_normal")) + + def validate_time(self, text) -> Union[None, re.Match]: + reg = self.hour_regx if self.num_type == "hour" else self.minute_regx + return re.match(reg, text) + + def insert_text(self, text, from_undo=False): + strip_text = self.text.strip() + current_string = "".join([strip_text, text]) + if not self.validate_time(current_string): + text = "" + return super().insert_text(text, from_undo=from_undo) + + def set_text(self, *args) -> None: + """ + Texts should be center aligned. Now we are setting the padding of text + to somehow make them aligned. + """ + + if not self.text: + self.text = " " + + self._refresh_text(self.text) + max_size = max(self._lines_rects, key=lambda r: r.size[0]).size + dx = (self.width - max_size[0]) / 2.0 + dy = (self.height - max_size[1]) / 2.0 + self.padding = [dx, dy, dx, dy] + + if len(self.text) > 1: + self.text = self.text.replace(" ", "") + + def on_focus(self, *args) -> None: + super().on_focus(*args) + if self.text.strip(): + if ( + not self.focus + and int(self.text) == 0 + and self.num_type == "hour" + ): + self.text = "12" + else: + self.text = " 12" if self.num_type == "hour" else " 00" + + def on_select(self, *args) -> None: + pass + + def on_touch_down(self, touch): + if self.collide_point(*touch.pos): + self.dispatch("on_select") + super().on_touch_down(touch) + + +class TimeInput(MDRelativeLayout): + """Implements two text fields for displaying and entering a time value.""" + + bg_color = ColorProperty() + bg_color_active = ColorProperty() + text_color = ColorProperty() + disabled = BooleanProperty(True) + minute_radius = ListProperty([0, 0, 0, 0]) + hour_radius = ListProperty([0, 0, 0, 0]) + state = StringProperty("hour") + + _hour = ObjectProperty() + _minute = ObjectProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_time_input") + self.register_event_type("on_hour_select") + self.register_event_type("on_minute_select") + + def set_time(self, time_list) -> None: + hour, minute = time_list + self._hour.text = hour + self._minute.text = minute + + def get_time(self) -> List[str]: + hour = self._hour.text.strip() + minute = self._minute.text.strip() + return [hour, minute] + + def on_time_input(self, *args) -> None: + pass + + def on_minute_select(self, *args) -> None: + pass + + def on_hour_select(self, *args) -> None: + pass + + def _update_padding(self, *args): + self._hour.set_text() + self._minute.set_text() + + +class SelectorLabel(MDLabel): + pass + + +class CircularSelector(MDCircularLayout, EventDispatcher): + """Implements clock face display.""" + + mode = OptionProperty("hour", options=["hour", "minute"]) # and military + text_color = ColorProperty() + selected_hour = StringProperty("12") + selected_minute = StringProperty("0") + selector_size = NumericProperty("48dp") + selector_pos = ListProperty([0, 0]) + selector_color = ColorProperty() + bg_color = ColorProperty() + font_name = StringProperty() + scale = NumericProperty(1) + content_scale = NumericProperty(1) + t = StringProperty("out_quad") + d = NumericProperty(0.2) + scale_origin = ListProperty([100, 100]) + + _centers_pos = ListProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.bind( + mode=self._update_labels, + selected_hour=self.update_time, + selected_minute=self.update_time, + ) + Clock.schedule_once(lambda x: self._update_labels(animate=False)) + self.register_event_type("on_selector_change") + + def do_layout(self, *largs, **kwargs): + self.update_time() + return super().do_layout(*largs, **kwargs) + + def set_selector(self, selected) -> bool: + """Sets the selector's position towards the given text.""" + + widget = None + for wid in self.children: + wid.text_color = self.text_color + if wid.text == selected: + widget = wid + if not widget: + return False + self.selector_pos = widget.center + widget.text_color = [1, 1, 1, 1] + self.dispatch("on_selector_change") + return True + + def set_time(self, selected) -> None: + if self.mode == "hour": + self.selected_hour = selected + elif self.mode == "minute": + self.selected_minute = selected + + def update_time(self, *args) -> None: + if self.mode == "hour": + self.set_selector(self.selected_hour) + elif self.mode == "minute": + self.set_selector(self.selected_minute) + + def get_selected(self) -> str: + return self.selected + + def switch_mode(self, mode) -> None: + if mode != self.mode: + self.mode = mode + + def on_touch_down(self, touch): + if self.collide_point(*touch.pos): + touch.grab(self) + closest_wid = self._get_closest_widget(touch.pos) + self.set_time(closest_wid.text) + return True + + def on_touch_move(self, touch): + if touch.grab_current == self: + closest_wid = self._get_closest_widget(touch.pos) + self.set_time(closest_wid.text) + + def on_touch_up(self, touch): + if touch.grab_current is self: + touch.ungrab(self) + return True + + def on_selector_change(self, *args): + pass + + def _update_labels(self, animate=True, *args): + """ + This method builds the selector based on current mode which currently + can be hour or minute. + """ + + if self.mode == "hour": + param = (1, 12) + self.degree_spacing = 30 + self.start_from = 60 + elif self.mode == "minute": + param = (0, 59, 5) + self.degree_spacing = 6 + self.start_from = 90 + elif self.mode == "military": + param = (1, 24) + self.degree_spacing = 30 + self.start_from = 90 + if animate: + anim = Animation(content_scale=0, t=self.t, d=self.d) + anim.bind(on_complete=lambda *args: self._add_items(*param)) + anim.start(self) + else: + self._add_items(*param) + + def _add_items(self, start, end, step=1): + """ + Adds all number in range `[start, end + 1]` to the circular layout with + the specified step. Step means that all widgets will be added to layout + but sets the opacity for skipped widgets to `0` because we are using + the label's text as a reference to the selected number so we have to + add these to layout. + """ + + self.clear_widgets() + i = 0 + for x in range(start, end + 1): + label = SelectorLabel( + text=f"{x}", + ) + if i % step != 0: + label.opacity = 0 + self.bind( + text_color=label.setter("text_color"), + font_name=label.setter("font_name"), + ) + self.add_widget(label) + i += 1 + Clock.schedule_once(self.update_time) + Clock.schedule_once(self._get_centers, 0.1) + anim = Animation(content_scale=1, t=self.t, d=self.d) + anim.start(self) + + def _get_centers(self, *args): + """ + Returns a list of all center. we use this for positioning the selector + indicator. + """ + + self._centers_pos = [] + for child in self.children: + self._centers_pos.append(child.center) + + def _get_closest_widget(self, pos): + """ + Returns the nearest widget to the given position. we use this to create + the magnetic effect. + """ + + distance = [Vector(pos).distance(point) for point in self._centers_pos] + if not distance: + return False + index = distance.index(min(distance)) + return self.children[index] + + +class MDTimePicker(BaseDialogPicker): + hour = StringProperty("12") + """ + Current hour. + + :attr:`hour` is an :class:`~kivy.properties.StringProperty` + and defaults to `'12'`. + """ + + minute = StringProperty("0") + """ + Current minute. + + :attr:`minute` is an :class:`~kivy.properties.StringProperty` + and defaults to `0`. + """ + + minute_radius = VariableListProperty(dp(5), length=4) + """ + Radius of the minute input field. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-minute-radius.png + :align: center + + :attr:`minute_radius` is an :class:`~kivy.properties.ListProperty` + and defaults to `[dp(5), dp(5), dp(5), dp(5)]`. + """ + + hour_radius = VariableListProperty(dp(5), length=4) + """ + Radius of the hour input field. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-hour-radius.png + :align: center + + :attr:`hour_radius` is an :class:`~kivy.properties.ListProperty` + and defaults to `[dp(5), dp(5), dp(5), dp(5)]`. + """ + + am_pm_radius = NumericProperty("5dp") + """ + Radius of the AM/PM selector. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-am-pm-radius.png + :align: center + + :attr:`am_pm_radius` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(5)`. + """ + + am_pm_border_width = NumericProperty("1dp") + """ + Width of the AM/PM selector's borders. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/time-picker-am-pm-border-width.png + :align: center + + :attr:`am_pm_border_width` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(1)`. + """ + + am_pm = OptionProperty("am", options=["am", "pm"]) + """ + Current AM/PM mode. + + :attr:`am_pm` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'am'`. + """ + + animation_duration = NumericProperty(0.3) + """ + Duration of the animations. + + :attr:`animation_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + animation_transition = StringProperty("out_quad") + """ + Transition type of the animations. + + :attr:`animation_transition` is an :class:`~kivy.properties.StringProperty` + and defaults to `'out_quad'`. + """ + + time = ObjectProperty(allownone=True) + """ + Returns the current time object. + + :attr:`time` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + _state = StringProperty() + _selector = ObjectProperty() + _time_input = ObjectProperty() + _am_pm_selector = ObjectProperty() + _hour_label = ObjectProperty() + _minute_label = ObjectProperty() + _anim_playing = BooleanProperty(False) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.bind( + hour=self._set_current_time, + minute=self._set_current_time, + am_pm=self._set_current_time, + ) + self.theme_cls.bind(device_orientation=self._check_orienation) + if self.title == "SELECT DATE": + self.title = "SELECT TIME" + self.set_time(datetime.time(hour=12, minute=0)) # default time + self._check_orienation() + + def set_time(self, time_obj) -> None: + """Manually set time dialog with the specified time.""" + + hour = time_obj.hour + minute = time_obj.minute + if hour > 12: + hour -= 12 + mode = "pm" + else: + mode = "am" + hour = str(hour) + minute = str(minute) + self._set_time_input(hour, minute) + self._set_dial_time(hour, minute) + self._set_am_pm(mode) + + def get_state(self) -> str: + """ + Returns the current state of TimePicker. + Can be one of `portrait`, `landscape` or `input`. + """ + + return self._state + + def _get_dial_time(self, instance): + mode = instance.mode + if mode == "hour": + self.hour = instance.selected_hour + elif mode == "minute": + self.minute = instance.selected_minute + else: + raise Exception("invalid mode for MDTimePicker: " % mode) + self._set_time_input(self.hour, self.minute) + + def _set_dial_time(self, hour, minute): + self._selector.selected_minute = minute + self._selector.selected_hour = hour + + def _get_time_input(self, hour, minute): + if hour: + self.hour = f"{int(hour):01d}" + if minute: + self.minute = f"{int(minute):01d}" + self._set_dial_time(self.hour, self.minute) + + def _set_time_input(self, hour, minute): + hour = f"{int(hour):02d}" + minute = f"{int(minute):02d}" + if self._state != "input": + self._time_input.set_time([hour, minute]) + + def _get_am_pm(self, selected): + self.am_pm = selected + + def _set_am_pm(self, selected: str) -> None: + """Used by set_time() to manually set the mode to "am" or "pm".""" + self.am_pm = selected + self._am_pm_selector.mode = self.am_pm + self._am_pm_selector.selected = self.am_pm + + def _get_data(self): + try: + if time.strftime("%p"): + result = datetime.datetime.strptime( + f"{int(self.hour):02d}:{int(self.minute):02d} {self.am_pm}", + "%I:%M %p", + ).time() + else: + result = datetime.datetime.strptime( + f"{int(self.hour):02d}:{int(self.minute):02d}", + "%I:%M", + ).time() + return result + except ValueError: + return None # hour is zero + + def _check_orienation(self, *args, do_anim=False): + orientation = self.theme_cls.device_orientation + if self._state != "input" and orientation != self._state: + self._update_pos_size(orientation, anim=do_anim) + + def _update_pos_size(self, orientation, anim=False): + d = self.animation_duration + # time input + time_input_pos = ( + [dp(24), dp(368)] + if orientation == "portrait" + else ( + [dp(24), dp(178)] + if orientation == "landscape" + else [dp(24), dp(96)] + ) + ) + if anim: + _time_input = Animation( + pos=time_input_pos, + d=d, + t=self.animation_transition, # 80 - 8, + ) + _time_input.start(self._time_input) + else: + self._time_input.pos = time_input_pos + + self._time_input.disabled = False if orientation == "input" else True + self._time_input.size = ( + [dp(216), dp(62)] if orientation == "input" else [dp(216), dp(72)] + ) + Clock.schedule_once(self._time_input._update_padding) + + # Circular selector. + if orientation == "input": + if self.theme_cls.device_orientation == "portrait": + selector_pos = [dp(34), dp(-256)] + self._selector.scale_origin = [dp(162), dp(200)] + else: + selector_pos = [dp(324), dp(-19)] + self._selector.scale_origin = [dp(292), dp(109)] + elif orientation == "portrait": + self._selector.pos = selector_pos = [dp(36), dp(76)] + else: + self._selector.pos = selector_pos = [dp(304), dp(76)] + + Animation( + pos=selector_pos, + scale=0 if orientation == "input" else 1, + opacity=0 if orientation == "input" else 1, + d=d, + t=self.animation_transition, + ).start(self._selector) + + # AM/PM selector. + am_pm_pos = ( + [dp(252), dp(368)] + if orientation == "portrait" + else ( + [dp(24), dp(126)] + if orientation == "landscape" + else [dp(252), dp(96)] + ) + ) + am_pm_size = ( + [dp(52), dp(80)] + if orientation == "portrait" + else ( + [dp(216), dp(40)] + if orientation == "landscape" + else [dp(48), dp(70)] + ) + ) + if anim: + Animation( + pos=am_pm_pos, + size=am_pm_size, + d=d, + t=self.animation_transition, + ).start(self._am_pm_selector) + else: + self._am_pm_selector.pos = am_pm_pos + self._am_pm_selector.size = am_pm_size + + self._am_pm_selector.orientation = ( + "horizontal" if orientation == "landscape" else "vertical" + ) + + # MDTimePicker. + time_picker_size = ( + [dp(328), dp(500)] + if orientation == "portrait" + else ( + [dp(584), dp(368)] + if orientation == "landscape" + else [dp(324), dp(218)] + ) + ) + if anim: + Animation( + size=time_picker_size, + d=d, + t=self.animation_transition, + ).start(self) + else: + self.size = time_picker_size + + # Minute label. + Animation( + pos=[dp(144), dp(76)], + opacity=1 if orientation == "input" else 0, + d=d, + t=self.animation_transition, + ).start(self._minute_label) + + # Hour label. + Animation( + pos=[dp(24), dp(76)], + opacity=1 if orientation == "input" else 0, + d=d, + t=self.animation_transition, + ).start(self._hour_label) + + self._state = orientation + self.ids.input_clock_switch.icon = ( + "clock-time-four-outline" if orientation == "input" else "keyboard" + ) + + def _set_current_time(self, *args): + self.time = self._get_data() + + def _switch_input(self): + self._update_pos_size( + self.theme_cls.device_orientation + if self._state == "input" + else "input", + anim=True, + ) diff --git a/sbapp/kivymd/uix/progressbar/__init__.py b/sbapp/kivymd/uix/progressbar/__init__.py new file mode 100644 index 0000000..34c14ce --- /dev/null +++ b/sbapp/kivymd/uix/progressbar/__init__.py @@ -0,0 +1 @@ +from .progressbar import MDProgressBar # NOQA F401 diff --git a/sbapp/kivymd/uix/progressbar/progressbar.kv b/sbapp/kivymd/uix/progressbar/progressbar.kv new file mode 100644 index 0000000..00028cf --- /dev/null +++ b/sbapp/kivymd/uix/progressbar/progressbar.kv @@ -0,0 +1,32 @@ + + canvas: + Clear + Color: + rgba: + self.theme_cls.divider_color \ + if not self.back_color else \ + self.back_color + Rectangle: + size: + (self.width, self.height) \ + if self.orientation == "horizontal" else \ + (self.width, self.height) + pos: + (self.x, self.center_y - self.height / 2) \ + if self.orientation == "horizontal" else \ + (self.center_x - self.width / 2, self.y) + Color: + rgba: + self.theme_cls.primary_color if not self.color else self.color + Rectangle: + size: + (self.width * self.value_normalized, self.height if self.height else dp(4)) \ + if self.orientation == "horizontal" else \ + (self.width, 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 - self.height / 2) \ + if self.orientation == "horizontal" \ + else (self.center_x - self.width / 2, self.height \ + * (1 - self.value_normalized) + self.y if self.reversed \ + else self.y) diff --git a/sbapp/kivymd/uix/progressbar/progressbar.py b/sbapp/kivymd/uix/progressbar/progressbar.py new file mode 100644 index 0000000..e7baaf7 --- /dev/null +++ b/sbapp/kivymd/uix/progressbar/progressbar.py @@ -0,0 +1,316 @@ +""" +Components/ProgressBar +====================== + +.. seealso:: + + `Material Design spec, 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 = ''' + MDBoxLayout: + 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 = ''' + MDScreen: + + MDProgressBar: + id: progress + pos_hint: {"center_y": .6} + type: "indeterminate" + + MDRaisedButton: + text: "STOP" if app.state == "start" else "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",) + +import os +from typing import Union + +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, + ColorProperty, + NumericProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.progressbar import ProgressBar + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior + +with open( + os.path.join(uix_path, "progressbar", "progressbar.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +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 = ColorProperty(None) + """ + Progress bar color in ``rgba`` format. + + :attr:`color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + back_color = ColorProperty(None) + """ + Progress bar back color in ``rgba`` format. + + .. versionadded:: 1.0.0 + + :attr:`back_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + 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) + Clock.schedule_once(self.check_size) + + def check_size(self, interval: Union[int, float]) -> None: + if self.size == [100, 100]: + if self.orientation == "horizontal": + self.size_hint_y = None + self.height = dp(4) + elif self.orientation == "vertical": + self.size_hint_x = None + self.width = dp(4) + + def start(self) -> None: + """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) -> None: + """Stop animation.""" + + Animation.cancel_all(self) + self._set_default_value(0) + + def running_away(self, *args) -> None: + self._set_default_value(0) + self.running_anim.start(self) + + def catching_up(self, *args) -> None: + 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/sbapp/kivymd/uix/recyclegridlayout.py b/sbapp/kivymd/uix/recyclegridlayout.py new file mode 100644 index 0000000..8a45768 --- /dev/null +++ b/sbapp/kivymd/uix/recyclegridlayout.py @@ -0,0 +1,92 @@ +""" +Components/RecycleGridLayout +===================== + +:class:`~kivy.uix.recyclegridlayout.RecycleGridLayout` class equivalent. Simplifies working +with some widget properties. For example: + +GridLayout +---------- + +.. code-block:: + + RecycleGridLayout: + size_hint_y: None + height: self.minimum_height + + canvas: + Color: + rgba: app.theme_cls.primary_color + Rectangle: + pos: self.pos + size: self.size + +MDRecycleGridLayout +------------ + +.. code-block:: + + MDRecycleGridLayout: + 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 + width: 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.recyclegridlayout import RecycleGridLayout + +from kivymd.uix import MDAdaptiveWidget + + +class MDRecycleGridLayout(RecycleGridLayout, MDAdaptiveWidget): + pass diff --git a/sbapp/kivymd/uix/refreshlayout/__init__.py b/sbapp/kivymd/uix/refreshlayout/__init__.py new file mode 100644 index 0000000..829d3b9 --- /dev/null +++ b/sbapp/kivymd/uix/refreshlayout/__init__.py @@ -0,0 +1 @@ +from .refreshlayout import MDScrollViewRefreshLayout # NOQA F401 diff --git a/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv b/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv new file mode 100644 index 0000000..26dc488 --- /dev/null +++ b/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv @@ -0,0 +1,27 @@ +#: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 diff --git a/sbapp/kivymd/uix/refreshlayout/refreshlayout.py b/sbapp/kivymd/uix/refreshlayout/refreshlayout.py new file mode 100755 index 0000000..845644c --- /dev/null +++ b/sbapp/kivymd/uix/refreshlayout/refreshlayout.py @@ -0,0 +1,223 @@ +""" +Components/RefreshLayout +======================== + +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 + + + + + MDBoxLayout: + orientation: 'vertical' + + MDTopAppBar: + 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() +""" + +__all__ = ("MDScrollViewRefreshLayout",) + +import os +from typing import Union + +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 ColorProperty, NumericProperty, ObjectProperty +from kivy.uix.floatlayout import FloatLayout +from kivy.uix.scrollview import ScrollView + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior + +with open( + os.path.join(uix_path, "refreshlayout", "refreshlayout.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class _RefreshScrollEffect(DampedScrollEffect): + """ + This class is simply based on DampedScrollEffect. + If you need any documentation please look at + :class:`~kivy.effects.dampedscrolleffect`. + """ + + min_scroll_to_reload = NumericProperty("-100dp") + """ + Minimum overscroll value to reload. + + :attr:`min_scroll_to_reload` is a :class:`~kivy.properties.NumericProperty` + and defaults to `'-100dp'`. + """ + + def on_overscroll( + self, instance_refresh_scroll_effect, overscroll: Union[int, float] + ) -> bool: + 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. + + :attr:`root_layout` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + 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) -> None: + if self.refresh_spinner: + self.refresh_spinner.hide_anim_spinner() + + +class RefreshSpinner(ThemableBehavior, FloatLayout): + spinner_color = ColorProperty([1, 1, 1, 1]) + """ + Color of spinner. + + :attr:`spinner_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[1, 1, 1, 1]`. + """ + + # kivymd.refreshlayout.MDScrollViewRefreshLayout object + _refresh_layout = ObjectProperty() + + def start_anim_spinner(self) -> None: + 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) -> None: + 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) -> None: + 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/sbapp/kivymd/uix/relativelayout.py b/sbapp/kivymd/uix/relativelayout.py new file mode 100644 index 0000000..96338a7 --- /dev/null +++ b/sbapp/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/sbapp/kivymd/uix/screen.py b/sbapp/kivymd/uix/screen.py new file mode 100644 index 0000000..40798cf --- /dev/null +++ b/sbapp/kivymd/uix/screen.py @@ -0,0 +1,60 @@ +""" +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.properties import ObjectProperty +from kivy.uix.screenmanager import Screen + +from kivymd.uix import MDAdaptiveWidget +from kivymd.uix.hero import MDHeroTo + + +class MDScreen(Screen, MDAdaptiveWidget): + hero_to = ObjectProperty() + """ + Must be a :class:`~kivymd.uix.hero.MDHeroTo` class. + See the documentation of the + `MDHeroTo `_ + widget for more detailed information. + + .. versionchanged:: 1.0.0 + + :attr:`hero_to` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + def on_hero_to(self, screen, widget) -> None: + if not isinstance(widget, MDHeroTo) or not issubclass( + widget.__class__, MDHeroTo + ): + raise TypeError( + f"The `{widget}` widget must be an `kivymd.uix.hero.MDHeroTo` " + f"class or inherited from this class" + ) diff --git a/sbapp/kivymd/uix/screenmanager.py b/sbapp/kivymd/uix/screenmanager.py new file mode 100644 index 0000000..1e23b68 --- /dev/null +++ b/sbapp/kivymd/uix/screenmanager.py @@ -0,0 +1,92 @@ +""" +Components/ScreenManager +======================== + +.. versionadded:: 1.0.0 + +:class:`~kivy.uix.screenmanager.ScreenManager` class equivalent. +If you want to use Hero animations you need to use :class:`~kivymd.uix.screenmanager.MDScreenManager` +not :class:`~kivy.uix.screenmanager.ScreenManager` class. +""" + +from kivy.clock import Clock +from kivy.properties import ListProperty, StringProperty +from kivy.uix.screenmanager import ScreenManager + +from kivymd.uix.hero import MDHeroFrom + + +class MDScreenManager(ScreenManager): + current_hero = StringProperty(None) + """ + The name of the current tag for the :class:`~kivymd.uix.hero.MDHeroFrom` and + :class:`~kivymd.uix.hero.MDHeroTo` objects that will be animated when + animating the transition between screens. + + See the `Hero `_ + module documentation for more information about creating and using Hero animations. + + :attr:`current_hero` is an :class:`~kivy.properties.StringProperty` + and defaults to `None`. + """ + + # Collection of `MDHeroFrom` objects on all screens of the current + # screen manager. + _heroes_data = ListProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self.check_transition) + + def check_transition(self, *args) -> None: + """Sets the default type transition.""" + + from kivymd.uix.transition.transition import MDTransitionBase + + if not issubclass(self.transition.__class__, MDTransitionBase): + from kivymd.uix.transition import MDSlideTransition + + self.transition = MDSlideTransition() + + def get_hero_from_widget(self) -> None: + """ + Get an :class:`~kivymd.uix.hero.MDHeroTo` object with the + :attr:`~current_hero` tag. + """ + + hero_from_widget = None + + for hero_widget in self._heroes_data: + if isinstance(hero_widget, MDHeroFrom) or issubclass( + hero_widget.__class__, MDHeroFrom + ): + if hero_widget.tag == self.current_hero: + hero_from_widget = hero_widget + break + + return hero_from_widget + + def add_widget(self, widget, *args, **kwargs): + super().add_widget(widget, *args, **kwargs) + Clock.schedule_once(lambda x: self._create_heroes_data(widget)) + + def _create_heroes_data(self, widget): + def find_hero_widget(child_widget): + widget_hero = None + + for w in child_widget.children: + if isinstance(w, MDHeroFrom) or issubclass( + w.__class__, MDHeroFrom + ): + self._heroes_data.append(w) + find_hero_widget(w) + + return widget_hero + + for child in widget.children: + if isinstance(child, MDHeroFrom) or issubclass( + child.__class__, MDHeroFrom + ): + self._heroes_data.append(child) + else: + find_hero_widget(child) diff --git a/sbapp/kivymd/uix/segmentedcontrol/__init__.py b/sbapp/kivymd/uix/segmentedcontrol/__init__.py new file mode 100644 index 0000000..c6517c5 --- /dev/null +++ b/sbapp/kivymd/uix/segmentedcontrol/__init__.py @@ -0,0 +1,4 @@ +from .segmentedcontrol import ( # NOQA F401 + MDSegmentedControl, + MDSegmentedControlItem, +) diff --git a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv new file mode 100644 index 0000000..9aed57f --- /dev/null +++ b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv @@ -0,0 +1,27 @@ + + adaptive_height: True + halign: "center" + pos_hint: {"center_y": .5} + markup: True + + + + size_hint: None, None + size: segment_panel.size + + SegmentSwitch: + id: segment_switch + height: segment_panel.height - dp(12) + pos_hint: {"center_y": .5} + x: root._segment_switch_x + md_bg_color: root.segment_color + elevation: 6 + _radius: root.radius[0] - 4 + + SegmentPanel: + id: segment_panel + radius: 12 + spacing: "12dp" + padding: "12dp" + size_hint: None, None + size: "320dp", root.segment_panel_height diff --git a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py new file mode 100644 index 0000000..243ae9a --- /dev/null +++ b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py @@ -0,0 +1,352 @@ +""" +Components/SegmentedControl +=========================== + +.. versionadded:: 1.0.0 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-control-preview.jpg + :align: center + +Usage +===== + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + + KV = ''' + MDScreen: + + MDSegmentedControl: + pos_hint: {"center_x": .5, "center_y": .5} + + MDSegmentedControlItem: + text: "Male" + + MDSegmentedControlItem: + text: "Female" + + MDSegmentedControlItem: + text: "All" + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +Or only in python code: + +.. code-block:: python + + from kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + from kivymd.uix.segmentedcontrol import MDSegmentedControl, MDSegmentedControlItem + + + class Test(MDApp): + def build(self): + screen = MDScreen() + segment_control = MDSegmentedControl(pos_hint={"center_x": .5, "center_y": .5}) + segment_control.add_widget(MDSegmentedControlItem(text="Male")) + segment_control.add_widget(MDSegmentedControlItem(text="Female")) + segment_control.add_widget(MDSegmentedControlItem(text="All")) + screen.add_widget(segment_control) + return screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-segmented-control-usage.gif + :align: center + +Events +====== + +.. code-block:: kv + + MDSegmentedControl: + on_active: app.on_active(*args) + +.. code-block:: python + + def on_active( + self, + segmented_control: MDSegmentedControl, + segmented_item: MDSegmentedControlItem, + ) -> None: + '''Called when the segment is activated.''' +""" + +__all__ = ("MDSegmentedControl", "MDSegmentedControlItem") + +import os + +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, + ColorProperty, + NumericProperty, + ObjectProperty, + StringProperty, + VariableListProperty, +) + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.button import MDRaisedButton +from kivymd.uix.card import MDSeparator +from kivymd.uix.label import MDLabel +from kivymd.uix.relativelayout import MDRelativeLayout + +with open( + os.path.join(uix_path, "segmentedcontrol", "segmentedcontrol.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDSegmentedControlItem(MDLabel): + """Implements a label to place on the :class:`~SegmentPanel` panel.""" + + +# TODO: Add an attribute for the color of the active segment label. +class MDSegmentedControl(ThemableBehavior, MDRelativeLayout): + """ + :Events: + `on_active` + Called when the segment is activated. + """ + + md_bg_color = ColorProperty([0, 0, 0, 0]) + """ + Background color of the segment panel. + + .. code-block:: kv + + MDSegmentedControl: + md_bg_color: "#451938" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-segmented-control-md-bg-color.png + :align: center + + :attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + segment_color = ColorProperty([0, 0, 0, 0]) + """ + Color of the active segment. + + .. code-block:: kv + + MDSegmentedControl: + md_bg_color: "#451938" + segment_color: "#e4514f" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-segmented-control-segment-color.png + :align: center + + .. code-block:: kv + + MDSegmentedControl: + md_bg_color: "#451938" + segment_color: "#e4514f" + + MDSegmentedControlItem: + text: "[color=fff]Male[/color]" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-segmented-control-text-color.png + :align: center + + :attr:`segment_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + segment_panel_height = NumericProperty("42dp") + """ + Height of the segment panel. + + .. code-block:: kv + + MDSegmentedControl: + segment_panel_height: "56dp" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-segmented-control-segment-panel-height.png + :align: center + + :attr:`segment_panel_height` is an :class:`~kivy.properties.NumericProperty` + and defaults to `'42dp'`. + """ + + separator_color = ColorProperty(None) + """ + The color of the separator between the segments. + + .. code-block:: kv + + MDSegmentedControl: + md_bg_color: "#451938" + segment_color: "#e4514f" + separator_color: 1, 1, 1, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-segmented-control-separator-color.png + :align: center + + :attr:`separator_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + radius = VariableListProperty([16], length=4) + """ + Radius of the segment panel. + + .. code-block:: kv + + MDSegmentedControl: + radius: 0 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-segmented-control-segment-radius.png + :align: center + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[16, 16, 16, 16]`. + """ + + segment_switching_transition = StringProperty("in_cubic") + """ + Name of the animation type for the switch segment. + + :attr:`segment_switching_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'in_cubic'`. + """ + + segment_switching_duration = NumericProperty(0.2) + """ + Name of the animation type for the switch segment. + + :attr:`segment_switching_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + current_active_segment = ObjectProperty() + """ + The current active element of the :class:`~MDSegmentedControlItem` class. + + :attr:`current_active_segment` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + _segment_switch_x = NumericProperty(dp(4)) + + def __init__(self, **kw): + super().__init__(**kw) + self.register_event_type("on_active") + + Clock.schedule_once(self.set_default_colors) + Clock.schedule_once(self._remove_last_separator) + # FIXME: Sometimes this interval is not enough to get the width + # of the segment label textures. + Clock.schedule_once(self._set_width_segment_switch, 2.2) + + def set_default_colors(self, *args) -> None: + """ + Sets the colors of the panel and the switch if the colors are not set + by the user. + """ + + if self.md_bg_color == [0, 0, 0, 0]: + self.md_bg_color = self.theme_cls.bg_darkest + if self.segment_color == [0, 0, 0, 0]: + self.segment_color = self.theme_cls.bg_dark + + def animation_segment_switch(self, widget: MDSegmentedControlItem) -> None: + """Animates the movement of the switch.""" + + Animation( + _segment_switch_x=widget.x - dp(6), + t=self.segment_switching_transition, + d=self.segment_switching_duration, + ).start(self) + + def update_segment_panel_width( + self, widget: MDSegmentedControlItem + ) -> None: + """ + Sets the width of the panel for the elements of the + :class:`~MDSegmentedControlItem` class. + """ + + widget.text_size = (None, None) + widget.texture_update() + self.ids.segment_panel.width += ( + widget.texture_size[0] + self.ids.segment_panel.spacing + ) + + def update_separator_color(self, widget: MDSeparator) -> None: + """Updates the color of the separators between segments.""" + + widget.color = ( + self.separator_color + if self.separator_color + else self.theme_cls.divider_color + ) + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, (SegmentPanel, SegmentSwitch)): + return super().add_widget(widget) + if isinstance(widget, MDSegmentedControlItem): + Clock.schedule_once( + lambda x: self.update_segment_panel_width(widget) + ) + widget.bind(on_touch_down=self.on_press_segment) + self.ids.segment_panel.add_widget(widget) + separator = MDSeparator(orientation="vertical") + self.ids.segment_panel.add_widget(separator) + Clock.schedule_once( + lambda x: self.update_separator_color(separator) + ) + + def on_active(self, *args) -> None: + """Called when the segment is activated.""" + + def on_press_segment(self, widget: MDSegmentedControlItem, touch): + if widget.collide_point(touch.x, touch.y): + self.animation_segment_switch(widget) + self.current_active_segment = widget + self.dispatch("on_active", widget) + + def _set_width_segment_switch(self, *args): + """ + Sets the width of the switch. I think this is not done quite correctly. + """ + + self.ids.segment_switch.width = self.ids.segment_panel.children[ + 0 + ].width + dp(12) + + def _remove_last_separator(self, *args): + self.ids.segment_panel.remove_widget(self.ids.segment_panel.children[0]) + + +class SegmentSwitch(MDRaisedButton): + """Implements a switch for the :class:`~MDSegmentedControl` class.""" + + _no_ripple_effect = BooleanProperty(True) + + +class SegmentPanel(MDBoxLayout): + """ + Implements a panel for placing items - :class:`~MDSegmentedControlItem` + for the :class:`~MDSegmentedControl` class. + """ diff --git a/sbapp/kivymd/uix/selection/__init__.py b/sbapp/kivymd/uix/selection/__init__.py new file mode 100644 index 0000000..9be34e2 --- /dev/null +++ b/sbapp/kivymd/uix/selection/__init__.py @@ -0,0 +1 @@ +from .selection import MDSelectionList # NOQA F401 diff --git a/sbapp/kivymd/uix/selection/selection.kv b/sbapp/kivymd/uix/selection/selection.kv new file mode 100644 index 0000000..7e3dead --- /dev/null +++ b/sbapp/kivymd/uix/selection/selection.kv @@ -0,0 +1,17 @@ + + theme_text_color: "Custom" + text_color: self.icon_check_color + + canvas.before: + PushMatrix + Scale: + x: root.scale + y: root.scale + z: root.scale + origin: self.center + canvas.after: + PopMatrix + + + + md_bg_color: root.overlay_color if root.selected else (0, 0, 0, 0) diff --git a/sbapp/kivymd/uix/selection/selection.py b/sbapp/kivymd/uix/selection/selection.py new file mode 100644 index 0000000..c5c9556 --- /dev/null +++ b/sbapp/kivymd/uix/selection/selection.py @@ -0,0 +1,664 @@ +""" +Components/Selection +==================== + +.. seealso:: + + `Material Design spec, Banner `_ + +.. rubric:: Selection refers to how users indicate specific items they intend to take action on. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-previous.png + :align: center + +Entering selection mode +----------------------- + +To select an item and enter selection mode, long press the item: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/enter-selection-mode.gif + :align: center + +Exiting selection mode +---------------------- + +To exit selection mode, tap each selected item until they’re all deselected: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/exit-selection-mode.gif + :align: center + +Larger selections +----------------- + +.. note:: This feature is missing yet. + +Events +------ + +.. code-block:: python + + def on_selected(self, instance_selection_list, instance_selection_item): + '''Called when a list item is selected.''' + + def on_unselected(self, instance_selection_list, instance_selection_item): + '''Called when a list item is unselected.''' + +Example with TwoLineAvatarListItem +---------------------------------- + +.. code-block:: python + + from kivy.animation import Animation + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.list import TwoLineAvatarListItem + + KV = ''' + + text: "Two-line item with avatar" + secondary_text: "Secondary text here" + _no_ripple_effect: True + + ImageLeftWidget: + source: "data/logo/kivy-icon-256.png" + + + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + id: toolbar + title: "Inbox" + left_action_items: [["menu"]] + right_action_items: [["magnify"], ["dots-vertical"]] + md_bg_color: 0, 0, 0, 1 + + MDBoxLayout: + padding: "24dp", "8dp", 0, "8dp" + adaptive_size: True + + MDLabel: + text: "Today" + adaptive_size: True + + ScrollView: + + MDSelectionList: + id: selection_list + spacing: "12dp" + overlay_color: app.overlay_color[:-1] + [.2] + icon_bg_color: app.overlay_color + on_selected: app.on_selected(*args) + on_unselected: app.on_unselected(*args) + on_selected_mode: app.set_selection_mode(*args) + ''' + + + class MyItem(TwoLineAvatarListItem): + pass + + + class Example(MDApp): + overlay_color = "#6042e4" + + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for i in range(10): + self.root.ids.selection_list.add_widget(MyItem()) + + def set_selection_mode(self, instance_selection_list, mode): + if mode: + md_bg_color = self.overlay_color + left_action_items = [ + [ + "close", + lambda x: self.root.ids.selection_list.unselected_all(), + ] + ] + right_action_items = [["trash-can"], ["dots-vertical"]] + else: + md_bg_color = (0, 0, 0, 1) + left_action_items = [["menu"]] + right_action_items = [["magnify"], ["dots-vertical"]] + self.root.ids.toolbar.title = "Inbox" + + Animation(md_bg_color=md_bg_color, d=0.2).start(self.root.ids.toolbar) + self.root.ids.toolbar.left_action_items = left_action_items + self.root.ids.toolbar.right_action_items = right_action_items + + def on_selected(self, instance_selection_list, instance_selection_item): + self.root.ids.toolbar.title = str( + len(instance_selection_list.get_selected_list_items()) + ) + + def on_unselected(self, instance_selection_list, instance_selection_item): + if instance_selection_list.get_selected_list_items(): + self.root.ids.toolbar.title = str( + len(instance_selection_list.get_selected_list_items()) + ) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-example-with-listItem.gif + :align: center + +Example with FitImage +--------------------- + +.. code-block:: python + + from kivy.animation import Animation + from kivy.lang import Builder + from kivy.properties import ColorProperty + + from kivymd.app import MDApp + from kivymd.utils.fitimage import FitImage + + KV = ''' + MDBoxLayout: + orientation: "vertical" + md_bg_color: app.theme_cls.bg_light + + MDTopAppBar: + id: toolbar + title: "Inbox" + left_action_items: [["menu"]] + right_action_items: [["magnify"], ["dots-vertical"]] + md_bg_color: app.theme_cls.bg_light + specific_text_color: 0, 0, 0, 1 + + MDBoxLayout: + padding: "24dp", "8dp", 0, "8dp" + adaptive_size: True + + MDLabel: + text: "Today" + adaptive_size: True + + ScrollView: + + MDSelectionList: + id: selection_list + padding: "24dp", 0, "24dp", "24dp" + cols: 3 + spacing: "12dp" + overlay_color: app.overlay_color[:-1] + [.2] + icon_bg_color: app.overlay_color + progress_round_color: app.progress_round_color + on_selected: app.on_selected(*args) + on_unselected: app.on_unselected(*args) + on_selected_mode: app.set_selection_mode(*args) + ''' + + + class Example(MDApp): + overlay_color = ColorProperty("#6042e4") + progress_round_color = "#ef514b" + + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for i in range(10): + self.root.ids.selection_list.add_widget( + FitImage( + source="image.png", + size_hint_y=None, + height="240dp", + ) + ) + + def set_selection_mode(self, instance_selection_list, mode): + if mode: + md_bg_color = self.overlay_color + left_action_items = [ + [ + "close", + lambda x: self.root.ids.selection_list.unselected_all(), + ] + ] + right_action_items = [["trash-can"], ["dots-vertical"]] + else: + md_bg_color = (1, 1, 1, 1) + left_action_items = [["menu"]] + right_action_items = [["magnify"], ["dots-vertical"]] + self.root.ids.toolbar.title = "Inbox" + + Animation(md_bg_color=md_bg_color, d=0.2).start(self.root.ids.toolbar) + self.root.ids.toolbar.left_action_items = left_action_items + self.root.ids.toolbar.right_action_items = right_action_items + + def on_selected(self, instance_selection_list, instance_selection_item): + self.root.ids.toolbar.title = str( + len(instance_selection_list.get_selected_list_items()) + ) + + def on_unselected(self, instance_selection_list, instance_selection_item): + if instance_selection_list.get_selected_list_items(): + self.root.ids.toolbar.title = str( + len(instance_selection_list.get_selected_list_items()) + ) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-example-with-fitimage.gif + :align: center +""" + +__all__ = ("MDSelectionList",) + +import os +from typing import Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.graphics.context_instructions import Color +from kivy.graphics.vertex_instructions import ( + Ellipse, + RoundedRectangle, + SmoothLine, +) +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + StringProperty, +) + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import TouchBehavior +from kivymd.uix.button import MDIconButton +from kivymd.uix.list import MDList +from kivymd.uix.relativelayout import MDRelativeLayout + +with open( + os.path.join(uix_path, "selection", "selection.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class SelectionIconCheck(MDIconButton): + """Implements the icon for the checked item.""" + + scale = NumericProperty(0) + icon_check_color = ColorProperty([0, 0, 0, 1]) + + +class SelectionItem(ThemableBehavior, MDRelativeLayout, TouchBehavior): + selected = BooleanProperty(False) + """ + Whether or not an item is checked. + + :attr:`selected` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + owner = ObjectProperty() + """ + Instance of :class:`~kivymd.uix.selection.MDSelectionList` class. + + :attr:`owner` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + instance_item = ObjectProperty() + """ + User item. Must be a Kivy or KivyMD widget. + + :attr:`instance_item` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + instance_icon = ObjectProperty() + """ + Instance of :class:`~kivymd.uix.selection.SelectionIconCheck` class. + + :attr:`instance_icon` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + overlay_color = ColorProperty([0, 0, 0, 0.2]) + """See :attr:`~MDSelectionList.overlay_color`.""" + + progress_round_size = NumericProperty(dp(46)) + """See :attr:`~MDSelectionList.progress_round_size`.""" + + progress_round_color = ColorProperty(None) + """See :attr:`~MDSelectionList.progress_round_color`.""" + + _progress_round = NumericProperty(0) + _progress_line_end = NumericProperty(0) + _progress_animation = BooleanProperty(False) + _touch_long = BooleanProperty(False) + _instance_progress_inner_circle_color = ObjectProperty() + _instance_progress_inner_circle_ellipse = ObjectProperty() + _instance_progress_inner_outer_color = ObjectProperty() + _instance_progress_inner_outer_line = ObjectProperty() + _instance_overlay_color = ObjectProperty() + _instance_overlay_rounded_rec = ObjectProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self.set_progress_round) + + def set_progress_round(self, interval: Union[int, float]) -> None: + with self.canvas.after: + self._instance_progress_inner_circle_color = Color( + rgba=(0, 0, 0, 0) + ) + self._instance_progress_inner_circle_ellipse = Ellipse( + size=self.get_progress_round_size(), + pos=self.get_progress_round_pos(), + ) + self.bind( + pos=self.update_progress_inner_circle_ellipse, + size=self.update_progress_inner_circle_ellipse, + ) + # FIXME: Radius value is not displayed. + self._instance_overlay_color = Color(rgba=(0, 0, 0, 0)) + self._instance_overlay_rounded_rec = RoundedRectangle( + size=self.size, + pos=self.pos, + radius=self.instance_item.radius + if hasattr(self.instance_item, "radius") + else [ + 0, + ], + ) + self.bind( + pos=self.update_overlay_rounded_rec, + size=self.update_overlay_rounded_rec, + ) + self._instance_progress_inner_outer_color = Color(rgba=(0, 0, 0, 0)) + self._instance_progress_inner_outer_line = SmoothLine( + width=dp(4), + circle=[ + self.center_x, + self.center_y, + self.progress_round_size * 0.58, + 0, + 0, + ], + ) + + def do_selected_item(self, *args) -> None: + Animation(scale=1, d=0.2).start(self.instance_icon) + self.selected = True + self._progress_animation = False + self._instance_overlay_color.rgba = self.get_overlay_color() + self.owner.dispatch("on_selected", self) + + def do_unselected_item(self) -> None: + Animation(scale=0, d=0.2).start(self.instance_icon) + self.selected = False + self._instance_overlay_color.rgba = self.get_overlay_color() + self.owner.dispatch("on_unselected", self) + + def do_animation_progress_line( + self, animation: Animation, instance_selection_item, value: float + ) -> None: + self._instance_progress_inner_outer_line.circle = ( + self.center_x, + self.center_y, + self.progress_round_size * 0.58, + 0, + 360 * value, + ) + + def update_overlay_rounded_rec(self, *args) -> None: + self._instance_overlay_rounded_rec.size = self.size + self._instance_overlay_rounded_rec.pos = self.pos + + def update_progress_inner_circle_ellipse(self, *args) -> None: + self._instance_progress_inner_circle_ellipse.size = ( + self.get_progress_round_size() + ) + self._instance_progress_inner_circle_ellipse.pos = ( + self.get_progress_round_pos() + ) + + def reset_progress_animation(self) -> None: + Animation.cancel_all(self) + self._progress_animation = False + self._instance_progress_inner_circle_color.rgba = (0, 0, 0, 0) + self._instance_progress_inner_outer_color.rgba = (0, 0, 0, 0) + self._instance_progress_inner_outer_line.circle = [ + self.center_x, + self.center_y, + self.progress_round_size * 0.58, + 0, + 0, + ] + self._progress_line_end = 0 + + def get_overlay_color(self) -> list: + return self.overlay_color if self.selected else (0, 0, 0, 0) + + def get_progress_round_pos(self) -> tuple: + return ( + self.center_x - self.progress_round_size / 2, + self.center_y - self.progress_round_size / 2, + ) + + def get_progress_round_size(self) -> tuple: + return self.progress_round_size, self.progress_round_size + + def get_progress_round_color(self) -> tuple: + return ( + self.theme_cls.primary_color + if not self.progress_round_color + else self.progress_round_color + ) + + def get_progress_line_color(self) -> tuple: + return ( + self.theme_cls.primary_color[:-1] + [0.5] + if not self.progress_round_color + else self.progress_round_color[:-1] + [0.5] + ) + + def on_long_touch(self, *args) -> None: + if not self.owner.get_selected(): + self._touch_long = True + self._progress_animation = True + + def on_touch_up(self, touch): + if self.collide_point(*touch.pos): + if self._touch_long: + self._touch_long = False + return super().on_touch_up(touch) + + def on_touch_down(self, touch): + if self.collide_point(*touch.pos): + if self.selected: + self.do_unselected_item() + else: + if self.owner.selected_mode: + self.do_selected_item() + return super().on_touch_down(touch) + + def on__touch_long(self, instance_selection_tem, touch_value: bool) -> None: + if not touch_value: + self.reset_progress_animation() + + def on__progress_animation( + self, instance_selection_tem, touch_value: bool + ) -> None: + if touch_value: + anim = Animation(_progress_line_end=360, d=1, t="in_out_quad") + anim.bind( + on_progress=self.do_animation_progress_line, + on_complete=self.do_selected_item, + ) + anim.start(self) + self._instance_progress_inner_outer_color.rgba = ( + self.get_progress_line_color() + ) + self._instance_progress_inner_circle_color.rgba = ( + self.get_progress_round_color() + ) + else: + self.reset_progress_animation() + + +class MDSelectionList(MDList): + """ + :Events: + `on_selected` + Called when a list item is selected. + `on_unselected` + Called when a list item is unselected. + """ + + selected_mode = BooleanProperty(False) + """ + List item selection mode. If `True` when clicking on a list item, it will + be selected. + + :attr:`selected_mode` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + icon = StringProperty("check") + """ + Name of the icon with which the selected list item will be marked. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'check'`. + """ + + icon_pos = ListProperty() + """ + The position of the icon that will mark the selected list item. + + :attr:`icon_pos` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + icon_bg_color = ColorProperty([1, 1, 1, 1]) + """ + Background color of the icon that will mark the selected list item. + + :attr:`icon_bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[1, 1, 1, 1]`. + """ + + icon_check_color = ColorProperty([0, 0, 0, 1]) + """ + Color of the icon that will mark the selected list item. + + :attr:`icon_check_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[1, 1, 1, 1]`. + """ + + overlay_color = ColorProperty([0, 0, 0, 0.2]) + """ + The overlay color of the selected list item.. + + :attr:`overlay_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0.2]]`. + """ + + progress_round_size = NumericProperty(dp(46)) + """ + Size of the spinner for switching of `selected_mode` mode. + + :attr:`progress_round_size` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(46)`. + """ + + progress_round_color = ColorProperty(None) + """ + Color of the spinner for switching of `selected_mode` mode. + + :attr:`progress_round_color` is an :class:`~kivy.properties.NumericProperty` + and defaults to `None`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_selected") + self.register_event_type("on_unselected") + + def add_widget(self, widget, index=0, canvas=None): + selection_icon = SelectionIconCheck( + icon=self.icon, + md_bg_color=self.icon_bg_color, + icon_check_color=self.icon_check_color, + ) + container = SelectionItem( + size_hint=(1, None), + height=widget.height, + instance_item=widget, + instance_icon=selection_icon, + overlay_color=self.overlay_color, + progress_round_size=self.progress_round_size, + progress_round_color=self.progress_round_color, + owner=self, + ) + container.add_widget(widget) + + if not self.icon_pos: + pos = ( + dp(12), + container.height / 2 - selection_icon.height / 2, + ) + else: + pos = self.icon_pos + selection_icon.pos = pos + container.add_widget(selection_icon) + return super().add_widget(container, index, canvas) + + def get_selected(self) -> bool: + """Returns ``True`` if at least one item in the list is checked.""" + + selected = False + for item in self.children: + if item.selected: + selected = True + break + return selected + + def get_selected_list_items(self) -> list: + """ + Returns a list of marked objects: + + [, ...] + """ + + selected_list_items = [] + for item in self.children: + if item.selected: + selected_list_items.append(item) + return selected_list_items + + def unselected_all(self) -> None: + for item in self.children: + item.do_unselected_item() + self.selected_mode = False + + def selected_all(self) -> None: + for item in self.children: + item.do_selected_item() + self.selected_mode = True + + def on_selected(self, *args): + """Called when a list item is selected.""" + + if not self.selected_mode: + self.selected_mode = True + + def on_unselected(self, *args): + """Called when a list item is unselected.""" + + self.selected_mode = self.get_selected() diff --git a/sbapp/kivymd/uix/selectioncontrol/__init__.py b/sbapp/kivymd/uix/selectioncontrol/__init__.py new file mode 100644 index 0000000..769a289 --- /dev/null +++ b/sbapp/kivymd/uix/selectioncontrol/__init__.py @@ -0,0 +1 @@ +from .selectioncontrol import MDCheckbox, MDSwitch, Thumb # NOQA F401 diff --git a/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.kv b/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.kv new file mode 100644 index 0000000..bfbfcda --- /dev/null +++ b/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.kv @@ -0,0 +1,74 @@ + + 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(14), dp(28)) \ + if root.widget_style == "ios" else \ + (self.width - dp(8), dp(16)) + pos: + (self.x - dp(2), self.center_y - dp(14)) \ + if root.widget_style == "ios" else \ + (self.x + dp(8), self.center_y - dp(8)) + radius: + [dp(14)] if root.widget_style == "ios" else [dp(7)] + Color: + rgba: + ( \ + self.theme_cls.disabled_hint_text_color[:-1] + [.2] \ + if not root.active else (0, 0, 0, 0) \ + ) \ + if root.widget_style == "ios" else (0, 0, 0, 0) + Line: + width: 1 + rounded_rectangle: + ( \ + self.x - dp(2), self.center_y - dp(14), self.width + dp(14), \ + dp(28), dp(14), dp(14), dp(14), dp(14), dp(28) \ + ) \ + if root.widget_style == "ios" else \ + (1, 1, 1, 1, 1, 1, 1, 1, 1) + + 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: 8 if root.active else 5 + on_release: setattr(root, "active", not root.active) diff --git a/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py b/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py new file mode 100755 index 0000000..d0503af --- /dev/null +++ b/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py @@ -0,0 +1,540 @@ +""" +Components/SelectionControls +============================ + +.. 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 = ''' + MDFloatLayout: + + 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) + + + MDFloatLayout: + + 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 = ''' + MDFloatLayout: + + 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:`~MDSwitch` 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") + +import os + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp, sp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + OptionProperty, + 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 import uix_path +from kivymd.color_definitions import colors +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + CircularRippleBehavior, + FakeCircularElevationBehavior, +) +from kivymd.uix.label import MDIcon + +with open( + os.path.join(uix_path, "selectioncontrol", "selectioncontrol.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +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") + """ + 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'`. + """ + + 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") + """ + 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'`. + """ + + selected_color = ColorProperty(None) + """ + Selected color in ``rgba`` format. + + :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + unselected_color = ColorProperty(None) + """ + Unelected color in ``rgba`` format. + + :attr:`unselected_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + disabled_color = ColorProperty(None) + """ + Disabled color in ``rgba`` format. + + :attr:`disabled_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _current_color = ColorProperty([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.theme_cls.bind(theme_style=self.update_primary_color) + self.update_icon() + self.update_color() + + def update_primary_color(self, instance, value): + if value in ("Dark", "Light"): + if not self.disabled: + self.color = self.theme_cls.primary_color + else: + self.color = self.disabled_color + else: + 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) + if not self.group: + 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( + FakeCircularElevationBehavior, + 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 = ColorProperty(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 = ColorProperty([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 = ColorProperty( + 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. + """ + + theme_thumb_color = OptionProperty("Primary", options=["Primary", "Custom"]) + """ + Thumb color scheme name + + :attr:`theme_thumb_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `Primary`. + """ + + theme_thumb_down_color = OptionProperty( + "Primary", options=["Primary", "Custom"] + ) + """ + Thumb Down color scheme name + + :attr:`theme_thumb_down_color` is an :class:`~kivy.properties.OptionProperty` + and defaults to `Primary`. + """ + + _track_color_active = ColorProperty([0, 0, 0, 0]) + _track_color_normal = ColorProperty([0, 0, 0, 0]) + _track_color_disabled = ColorProperty([0, 0, 0, 0]) + _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) + Clock.schedule_once(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": + + if self.theme_thumb_down_color == "Primary": + self._track_color_active = self.theme_cls.primary_color + else: + self._track_color_active = self.thumb_color_down + + self._track_color_active[3] = 0.5 + self._track_color_disabled = get_color_from_hex("FFFFFF") + self._track_color_disabled[3] = 0.1 + + if self.theme_thumb_color == "Primary": + self.thumb_color = get_color_from_hex(colors["Gray"]["400"]) + + if self.theme_thumb_down_color == "Primary": + self.thumb_color_down = get_color_from_hex( + colors[self.theme_cls.primary_palette]["200"] + ) + else: + if self.theme_thumb_down_color == "Primary": + self._track_color_active = get_color_from_hex( + colors[self.theme_cls.primary_palette]["200"] + ) + else: + self._track_color_active = self.thumb_color_down + + self._track_color_active[3] = 0.5 + self._track_color_disabled = self.theme_cls.disabled_hint_text_color + + if self.theme_thumb_down_color == "Primary": + self.thumb_color_down = self.theme_cls.primary_color + + if self.theme_thumb_color == "Primary": + self.thumb_color = get_color_from_hex(colors["Gray"]["50"]) + + def _update_thumb_pos(self, *args, animation=True): + if self.active: + _thumb_pos = (self.width - dp(14), 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/sbapp/kivymd/uix/slider/__init__.py b/sbapp/kivymd/uix/slider/__init__.py new file mode 100644 index 0000000..a4edb11 --- /dev/null +++ b/sbapp/kivymd/uix/slider/__init__.py @@ -0,0 +1 @@ +from .slider import MDSlider # NOQA F401 diff --git a/sbapp/kivymd/uix/slider/slider.kv b/sbapp/kivymd/uix/slider/slider.kv new file mode 100644 index 0000000..9af6ce5 --- /dev/null +++ b/sbapp/kivymd/uix/slider/slider.kv @@ -0,0 +1,116 @@ +#:import images_path kivymd.images_path +#:import Thumb kivymd.uix.selectioncontrol.Thumb + + + + + + + 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.color if not self.disabled \ + else self._track_color_disabled) + Rectangle: + size: + ((self.width - self.padding * 2) * self.value_normalized, sp(4)) \ + if root.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: + (root.value_pos[0] - dp(8), root.center_y - thumb.height / 2 - dp(2)) \ + if root.orientation == "horizontal" \ + else (root.center_x - thumb.width / 2 - dp(2), \ + root.value_pos[1] - dp(8)) + color: + (0, 0, 0, 0) if root._is_off else (root._track_color_disabled \ + if root.disabled else root.color) + elevation: 0 if root._is_off else (4 if root.active else 2) + + HintBoxContainer: + id: hint_box + size_hint: None, None + md_bg_color: root.hint_bg_color + elevation: 0 + opacity: 1 if root.active else 0 + radius: root.hint_radius + padding: "6dp", "6dp", "6dp", "8dp" + size: + lbl_value.width + self.padding[0] * 2, \ + lbl_value.height + self.padding[0] + pos: + (root.value_pos[0] - dp(9), root.center_y - hint_box.height / 2 + dp(30)) \ + if root.orientation == "horizontal" \ + else \ + (root.center_x - hint_box.width / 2 + dp(30), root.value_pos[1] - dp(8)) + + MDLabel: + id: lbl_value + font_style: "Caption" + halign: "center" + theme_text_color: "Custom" + -text_size: None, None + adaptive_size: True + pos_hint: {"center_x": .5, "center_y": .5} + text_color: + ( \ + root.color \ + if root.active \ + else (0, 0, 0, 0) \ + ) \ + if not root.hint_text_color \ + else \ + root.hint_text_color + text: + str(root.value) \ + if isinstance(root.step, float) \ + else \ + str(int(root.value)) diff --git a/sbapp/kivymd/uix/slider/slider.py b/sbapp/kivymd/uix/slider/slider.py new file mode 100644 index 0000000..5611d3e --- /dev/null +++ b/sbapp/kivymd/uix/slider/slider.py @@ -0,0 +1,238 @@ +""" +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 = ''' + MDScreen + + 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 + color: app.theme_cls.accent_color + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/slider-3.png + :align: center +""" + +__all__ = ("MDSlider",) + +import os + +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + ListProperty, + VariableListProperty, +) +from kivy.uix.slider import Slider +from kivy.utils import get_color_from_hex + +from kivymd import uix_path +from kivymd.color_definitions import colors +from kivymd.theming import ThemableBehavior + +with open( + os.path.join(uix_path, "slider", "slider.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +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 = ColorProperty([0, 0, 0, 0]) + """ + Hint rectangle color in ``rgba`` format. + + :attr:`hint_bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + hint_text_color = ColorProperty(None) + """ + Hint text color in ``rgba`` format. + + :attr:`hint_text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + hint_radius = VariableListProperty([dp(4), dp(4), dp(4), dp(4)]) + """ + Hint radius. + + :attr:`hint_radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[dp(4), dp(4), dp(4), dp(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`. + """ + + color = ColorProperty([0, 0, 0, 0]) + """ + Color slider in ``rgba`` format. + + :attr:`color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _track_color_active = ColorProperty([0, 0, 0, 0]) + _track_color_normal = ColorProperty([0, 0, 0, 0]) + _track_color_disabled = ColorProperty([0, 0, 0, 0]) + _thumb_pos = ListProperty([0, 0]) + _thumb_color_disabled = ColorProperty( + get_color_from_hex(colors["Gray"]["400"]) + ) + # Internal state of ring + _is_off = BooleanProperty(False) + # Internal adjustment to reposition sliders for ring + _offset = 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 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 on__is_off(self, *args): + self._update_offset() + + def on_active(self, *args): + self._update_offset() + + 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 + + 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 _update_is_off(self): + self._is_off = self.show_off and (self.value_normalized == 0) + + 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 + if self.color == [0, 0, 0, 0]: + self.color = 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 + if self.color == [0, 0, 0, 0]: + self.color = self.theme_cls.primary_color diff --git a/sbapp/kivymd/uix/sliverappbar/__init__.py b/sbapp/kivymd/uix/sliverappbar/__init__.py new file mode 100644 index 0000000..e88fa47 --- /dev/null +++ b/sbapp/kivymd/uix/sliverappbar/__init__.py @@ -0,0 +1,5 @@ +from .sliverappbar import ( + MDSliverAppbar, + MDSliverAppbarContent, + MDSliverAppbarHeader, +) diff --git a/sbapp/kivymd/uix/sliverappbar/sliverappbar.kv b/sbapp/kivymd/uix/sliverappbar/sliverappbar.kv new file mode 100644 index 0000000..fd7fe7f --- /dev/null +++ b/sbapp/kivymd/uix/sliverappbar/sliverappbar.kv @@ -0,0 +1,41 @@ +#:import ScrollEffect kivy.effects.scroll.ScrollEffect +#:import get_color_from_hex kivy.utils.get_color_from_hex + + + + + FloatLayout: + id: float_box + + BoxLayout: + canvas.after: + Color: + rgba: + root.background_color \ + if root.background_color else \ + get_color_from_hex(root.theme_cls.primary_color) + a: root._opacity + Rectangle: + pos: self.pos + size: self.size + + id: header + size_hint_y: None + height: root.max_height + root.radius[0] + pos: self.x, root.height - root.max_height - root.radius[0] + + ScrollView: + id: scroll + effect_cls: ScrollEffect + on_vbar: root.on_vbar() + on_scroll_start: + if not root._scroll_was_moving: root._scroll_was_moving = True + + MDBoxLayout: + id: scroll_box + adaptive_height: True + orientation: "vertical" + + BoxLayout: + size_hint_y: None + height: root.max_height diff --git a/sbapp/kivymd/uix/sliverappbar/sliverappbar.py b/sbapp/kivymd/uix/sliverappbar/sliverappbar.py new file mode 100644 index 0000000..4591741 --- /dev/null +++ b/sbapp/kivymd/uix/sliverappbar/sliverappbar.py @@ -0,0 +1,489 @@ +""" +Components/SliverAppbar +======================= + +.. versionadded:: 1.0.0 + +.. rubric:: MDSliverAppbar is a Material Design widget in KivyMD which gives + scrollable or collapsible + `MDTopAppBar `_ + +.. note:: This widget is a modification of the + `silverappbar.py `_ module. + +Usage +----- + +.. code-block:: kv + + MDScreen: + + MDSliverAppbar: + + MDSliverAppbarHeader: + + # Custom content. + ... + + # Custom list. + MDSliverAppbarContent: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-usage.png + :align: center + +Example +------- + +.. code-block:: python + + from kivy.lang.builder import Builder + + from kivymd.uix.card import MDCard + from kivymd.uix.behaviors import RoundedRectangularElevationBehavior + + KV = ''' + + size_hint_y: None + height: "86dp" + padding: "4dp" + radius: 12 + elevation: 4 + + FitImage: + source: "avatar.jpg" + radius: root.radius + size_hint_x: None + width: root.height + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "6dp" + padding: "12dp", 0, 0, 0 + pos_hint: {"center_y": .5} + + MDLabel: + text: "Title text" + font_style: "H5" + bold: True + adaptive_height: True + + MDLabel: + text: "Subtitle text" + theme_text_color: "Hint" + adaptive_height: True + + + MDScreen: + + MDSliverAppbar: + background_color: "2d4a50" + + MDSliverAppbarHeader: + + MDRelativeLayout: + + FitImage: + source: "bg.jpg" + + MDSliverAppbarContent: + id: content + orientation: "vertical" + padding: "12dp" + spacing: "12dp" + adaptive_height: True + ''' + + + class CardItem(MDCard, RoundedRectangularElevationBehavior): + pass + + + class Example(MDApp): + def build(self): + return Builder.load_string(KV) + + def on_start(self): + for x in range(10): + self.root.ids.content.add_widget(CardItem()) + + + Example().run() +""" + + +__all__ = ("MDSliverAppbar", "MDSliverAppbarHeader", "MDSliverAppbarContent") + +import os +from typing import Union + +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.lang.builder import Builder +from kivy.properties import ( + BooleanProperty, + ColorProperty, + NumericProperty, + ObjectProperty, + VariableListProperty, +) + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.toolbar import MDTopAppBar + +with open( + os.path.join(uix_path, "sliverappbar", "sliverappbar.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDSliverAppbarException(Exception): + pass + + +class MDSliverAppbarContent(ThemableBehavior, MDBoxLayout): + """Implements a box for a scrollable list of custom items.""" + + md_bg_color = ColorProperty([0, 0, 0, 0]) + """ + See :attr:`~kivymd.uix.sliverappbar.sliverappbar.MDSliverAppbar.background_color`. + + :attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self.set_bg_color) + + def set_bg_color(self, interval: Union[int, float]) -> None: + if self.md_bg_color == [0, 0, 0, 0]: + self.md_bg_color = self.theme_cls.bg_normal + + +class MDSliverAppbarHeader(MDBoxLayout): + pass + + +class MDSliverAppbar(ThemableBehavior, MDBoxLayout): + """ + MDSliverAppbar class. + See module documentation for more information. + + :Events: + :attr:`on_scroll_content` + Called when the list of custom content is being scrolled. + """ + + toolbar_cls = ObjectProperty() + """ + Must be an object of the :class:`~kivymd.uix.toolbar.toolbar.MDTopAppBar' class. + See :class:`~kivymd.uix.toolbar.toolbar.MDTopAppBar` class documentation + for more information. + + By default, MDSliverAppbar widget uses the MDTopAppBar class with no + parameters. + + .. code-block:: python + + from kivy.lang.builder import Builder + + from kivymd.uix.card import MDCard + from kivymd.uix.toolbar import MDTopAppBar + from kivymd.uix.behaviors import RoundedRectangularElevationBehavior + + KV = ''' + #:import SliverToolbar __main__.SliverToolbar + + + + size_hint_y: None + height: "86dp" + padding: "4dp" + radius: 12 + elevation: 4 + + FitImage: + source: "avatar.jpg" + radius: root.radius + size_hint_x: None + width: root.height + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + spacing: "6dp" + padding: "12dp", 0, 0, 0 + pos_hint: {"center_y": .5} + + MDLabel: + text: "Title text" + font_style: "H5" + bold: True + adaptive_height: True + + MDLabel: + text: "Subtitle text" + theme_text_color: "Hint" + adaptive_height: True + + + MDScreen: + + MDSliverAppbar: + background_color: "2d4a50" + toolbar_cls: SliverToolbar() + + MDSliverAppbarHeader: + + MDRelativeLayout: + + FitImage: + source: "bg.jpg" + + MDSliverAppbarContent: + id: content + orientation: "vertical" + padding: "12dp" + spacing: "12dp" + adaptive_height: True + ''' + + + class CardItem(MDCard, RoundedRectangularElevationBehavior): + pass + + + class SliverToolbar(MDTopAppBar): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.type_height = "medium" + self.headline_text = "Headline medium" + self.left_action_items = [["arrow-left", lambda x: x]] + self.right_action_items = [ + ["attachment", lambda x: x], + ["calendar", lambda x: x], + ["dots-vertical", lambda x: x], + ] + + + class Example(MDApp): + def build(self): + self.theme_cls.material_style = "M3" + return Builder.load_string(KV) + + def on_start(self): + for x in range(10): + self.root.ids.content.add_widget(CardItem()) + + + Example().run() + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-toolbar-cls.gif + :align: center + + :attr:`toolbar_cls` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + background_color = ColorProperty(None) + """ + Background color of toolbar in (r, g, b, a) format. + + .. code-block:: kv + + MDSliverAppbar: + background_color: "2d4a50" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-background-color.png + :align: center + + :attr:`background_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + max_height = NumericProperty(Window.height / 2) + """ + Distance from top of screen to start of custom list content. + + .. code-block:: kv + + MDSliverAppbar: + max_height: "200dp" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-max-height.png + :align: center + + :attr:`max_height` is an :class:`~kivy.properties.NumericProperty` + and defaults to `Window.height / 2`. + """ + + hide_toolbar = BooleanProperty(True) + """ + Whether to hide the toolbar when scrolling through a list + of custom content. + + .. code-block:: kv + + MDSliverAppbar: + hide_toolbar: False + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-hide-toolbar.gif + :align: center + + MDSliverAppbar: + hide_toolbar: True + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-hide-toolbar-true.gif + :align: center + + :attr:`hide_toolbar` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + radius = VariableListProperty([20], length=4) + """ + Box radius for custom item list. + + .. code-block:: kv + + MDSliverAppbar: + radius: 20 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-radius.png + :align: center + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[20]`. + """ + + max_opacity = NumericProperty(1) + """ + Maximum background transparency value for the + :class:`~kivymd.uix.sliverappbar.sliverappbar.MDSliverAppbarHeader` class. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-max-opacity.gif + :align: center + + .. code-block:: kv + + MDSliverAppbar: + max_opacity: .5 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/sliver-app-bar-max-opacity-05.gif + :align: center + + :attr:`max_opacity` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + _opacity = NumericProperty() + _scroll_was_moving = BooleanProperty(False) + _last_scroll_y_pos = 0.0 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_scroll_content") + + def on_scroll_content( + self, + instance_sliverappbar: object = None, + value: float = 1.0, + direction: str = "up", + ): + """ + Called when the list of custom content is being scrolled. + + :param instance_sliverappbar: :class:`~MDSliverAppbar` + :param value: see :attr:`~kivy.uix.scrollview.ScrollView.scroll_y` + :param direction: scroll direction: 'up/down' + """ + + def on_background_color( + self, instance_sliver_appbar, color_value: list + ) -> None: + if self.toolbar_cls: + self.toolbar_cls.md_bg_color = color_value + + def on_toolbar_cls( + self, instance_sliver_appbar, instance_toolbar_cls: MDTopAppBar + ) -> None: + """Called when a value is set to the :attr:`toolbar_cls` parameter.""" + + # If an MDTopAppBar object is already in use, delete it + # before adding a new MDTopAppBar object. + for widget in self.ids.float_box.children: + if issubclass(widget.__class__, MDTopAppBar): + self.ids.float_box.remove_widget(widget) + + # Adding a custom MDTopAppBar object. + if issubclass(instance_toolbar_cls.__class__, MDTopAppBar): + instance_toolbar_cls.pos_hint = {"top": 1} + self.ids.float_box.add_widget(instance_toolbar_cls) + else: + raise MDSliverAppbarException( + "The `toolbar_cls` parameter must be an object of the " + "`kivymd.uix.toolbar.MDTopAppBar class`" + ) + + def on_vbar(self) -> None: + if not self.background_color: + self.background_color = self.theme_cls.primary_color + + if not self.toolbar_cls: + self.toolbar_cls = self.get_default_toolbar() + + scroll_box = self.ids.scroll_box + vbar = self.ids.scroll.vbar + toolbar_percent = (self.toolbar_cls.height / scroll_box.height) * 100 + current_percent = (vbar[0] + vbar[1]) * 100 + percent_min = ( + 1 - self.max_height / scroll_box.height + ) * 100 + toolbar_percent + + if self._scroll_was_moving: + direction = self._get_direction_swipe(self.ids.scroll.scroll_y) + self._last_scroll_y_pos = self.ids.scroll.scroll_y + self.dispatch( + "on_scroll_content", self.ids.scroll.scroll_y, direction + ) + + if self.hide_toolbar: + if percent_min <= current_percent: + opacity = (current_percent - percent_min) / (100 - percent_min) + self._opacity = self.max_opacity * (1 - opacity) + self.background_color = self.background_color[0:3] + [ + 1 - opacity + ] + self.toolbar_cls._hard_shadow_a = 1 - opacity + self.toolbar_cls._soft_shadow_a = 1 - opacity + else: + self.background_color = self.background_color[0:3] + [1] + + def get_default_toolbar(self) -> MDTopAppBar: + """Called if no value is passed for the toolbar_cls attribute.""" + + return MDTopAppBar( + pos_hint={"top": 1}, md_bg_color=self.background_color + ) + + def add_widget(self, widget, index=0, canvas=None): + if issubclass(widget.__class__, MDSliverAppbarContent): + Clock.schedule_once(lambda x: self._set_radius(widget)) + self.ids.scroll_box.add_widget(widget) + elif issubclass(widget.__class__, MDSliverAppbarHeader): + self.ids.header.add_widget(widget) + else: + super().add_widget(widget, index=index, canvas=canvas) + + def _set_radius(self, instance: MDSliverAppbarContent) -> None: + instance.radius = self.radius + + def _get_direction_swipe(self, current_percent: float) -> str: + if self._last_scroll_y_pos > current_percent: + direction = "up" + else: + direction = "down" + return direction diff --git a/sbapp/kivymd/uix/snackbar/__init__.py b/sbapp/kivymd/uix/snackbar/__init__.py new file mode 100644 index 0000000..ed3c68d --- /dev/null +++ b/sbapp/kivymd/uix/snackbar/__init__.py @@ -0,0 +1 @@ +from .snackbar import BaseSnackbar, Snackbar # NOQA F401 diff --git a/sbapp/kivymd/uix/snackbar/snackbar.kv b/sbapp/kivymd/uix/snackbar/snackbar.kv new file mode 100644 index 0000000..4dcef8b --- /dev/null +++ b/sbapp/kivymd/uix/snackbar/snackbar.kv @@ -0,0 +1,34 @@ +#:import window kivy.core.window + + + + size_hint_y: None + height: "58dp" + spacing: "10dp" + padding: "10dp", "10dp", "10dp", "10dp" + md_bg_color: "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: "ffffff" + shorten: True + shorten_from: "right" + markup: True + pos_hint: {"center_y": .5} diff --git a/sbapp/kivymd/uix/snackbar/snackbar.py b/sbapp/kivymd/uix/snackbar/snackbar.py new file mode 100755 index 0000000..5154441 --- /dev/null +++ b/sbapp/kivymd/uix/snackbar/snackbar.py @@ -0,0 +1,613 @@ +""" +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: '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", "BaseSnackbar") + +import os + +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, + ColorProperty, + ListProperty, + NumericProperty, + OptionProperty, + StringProperty, +) + +from kivymd import uix_path +from kivymd.uix.behaviors import FakeRectangularElevationBehavior +from kivymd.uix.button import BaseButton +from kivymd.uix.card import MDCard + +with open( + os.path.join(uix_path, "snackbar", "snackbar.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class BaseSnackbar(MDCard, FakeRectangularElevationBehavior): + """ + :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 = ColorProperty(None) + """ + Snackbar background. + + :attr:`bg_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + 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/sbapp/kivymd/uix/spinner/__init__.py b/sbapp/kivymd/uix/spinner/__init__.py new file mode 100644 index 0000000..a1d79e1 --- /dev/null +++ b/sbapp/kivymd/uix/spinner/__init__.py @@ -0,0 +1 @@ +from .spinner import MDSpinner # NOQA F401 diff --git a/sbapp/kivymd/uix/spinner/spinner.kv b/sbapp/kivymd/uix/spinner/spinner.kv new file mode 100644 index 0000000..a532803 --- /dev/null +++ b/sbapp/kivymd/uix/spinner/spinner.kv @@ -0,0 +1,18 @@ + + canvas.before: + PushMatrix + Rotate: + angle: self._rotation_angle + origin: self.center + canvas: + Color: + rgba: self.color if self.color else self.theme_cls.primary_color + a: self._alpha + SmoothLine: + cap: 'square' + width: root.line_width + circle: + self.center_x, self.center_y, self.width / 2, \ + self._angle_start, self._angle_end + canvas.after: + PopMatrix diff --git a/sbapp/kivymd/uix/spinner/spinner.py b/sbapp/kivymd/uix/spinner/spinner.py new file mode 100755 index 0000000..fd6b214 --- /dev/null +++ b/sbapp/kivymd/uix/spinner/spinner.py @@ -0,0 +1,317 @@ +""" +Components/Spinner +================== + +.. seealso:: + + `Material Design spec, Menus `_ + +.. rubric:: Circular progress indicator in Google's Material Design. + +Usage +----- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + 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 + +Determinate mode +---------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDSpinner: + size_hint: None, None + size: dp(48), dp(48) + pos_hint: {'center_x': .5, 'center_y': .5} + determinate: 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-determinate.gif + :align: center +""" + +__all__ = ("MDSpinner",) + +import os +from typing import Union + +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, + ColorProperty, + ListProperty, + NumericProperty, +) +from kivy.uix.widget import Widget + +from kivymd import uix_path +from kivymd.theming import ThemableBehavior + +with open( + os.path.join(uix_path, "spinner", "spinner.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +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. + + :Events: + `on_determinate_complete` + The event is called at the end of the spinner loop in the + `determinate = True` mode. + """ + + 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`. + """ + + line_width = NumericProperty(dp(2.25)) + """ + Progress line width of spinner. + + :attr:`line_width` is a :class:`~kivy.properties.NumericProperty` + and defaults to `dp(2.25)`. + """ + + 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 = ColorProperty(None, allownone=True) + """ + Spinner color. + + :attr:`color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + 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) + if not self.color: + self.color = self.theme_cls.primary_color + if self.color == self.theme_cls.primary_color: + self.theme_cls.bind(primary_color=self._update_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, + on_progress=self._on_determinate_progress, + ) + self.register_event_type("on_determinate_complete") + Clock.schedule_once(self.check_determinate) + + 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 on_palette(self, instance_spinner, palette_list: list) -> None: + self._palette = iter(palette_list) + + def on_active(self, instance_spinner, active_value: bool) -> None: + self._reset() + if self.active: + self.check_determinate() + + def on_determinate_complete(self, *args): + """ + The event is called at the end of the spinner loop in the + `determinate = True` mode. + """ + + def check_determinate(self, interval: Union[float, int] = 0) -> None: + if self.active: + 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) + Animation( + _rotation_angle=0, + duration=self.determinate_time * 0.7, + t="out_quad", + ).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 _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 + + def _on_determinate_progress( + self, instance_animation, instance_spinner, value + ): + if value == 1: + self.dispatch("on_determinate_complete") diff --git a/sbapp/kivymd/uix/stacklayout.py b/sbapp/kivymd/uix/stacklayout.py new file mode 100644 index 0000000..4782302 --- /dev/null +++ b/sbapp/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 + width: 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/sbapp/kivymd/uix/swiper/__init__.py b/sbapp/kivymd/uix/swiper/__init__.py new file mode 100644 index 0000000..44cedae --- /dev/null +++ b/sbapp/kivymd/uix/swiper/__init__.py @@ -0,0 +1 @@ +from .swiper import MDSwiper, MDSwiperItem diff --git a/sbapp/kivymd/uix/swiper/swiper.kv b/sbapp/kivymd/uix/swiper/swiper.kv new file mode 100644 index 0000000..15f7b38 --- /dev/null +++ b/sbapp/kivymd/uix/swiper/swiper.kv @@ -0,0 +1,18 @@ + + do_scroll_y: False + bar_width: 0 + + MDBoxLayout: + id: anchor_scroll + adaptive_width: True + padding: [root.items_spacing, 0 ] + + + + size_hint: None, None + + +<_ItemsBox> + size_hint_x: None + anchor_x: "center" + anchor_y: "center" diff --git a/sbapp/kivymd/uix/swiper/swiper.py b/sbapp/kivymd/uix/swiper/swiper.py new file mode 100644 index 0000000..4fd620b --- /dev/null +++ b/sbapp/kivymd/uix/swiper/swiper.py @@ -0,0 +1,561 @@ +""" +Components/MDSwiper +=================== + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mdswiper-preview.gif + :align: center + +Usage +===== + +.. code-block:: kv + + MDSwiper: + + MDSwiperItem: + + MDSwiperItem: + + MDSwiperItem: + +Example +======= + +.. code-block:: python + + from kivymd.app import MDApp + from kivy.lang.builder import Builder + + kv = ''' + + + FitImage: + source: "guitar.png" + radius: [20,] + + MDScreen: + + MDTopAppBar: + id: toolbar + title: "MDSwiper" + elevation: 10 + pos_hint: {"top": 1} + + MDSwiper: + size_hint_y: None + height: root.height - toolbar.height - dp(40) + y: root.height - self.height - toolbar.height - dp(20) + + MySwiper: + + MySwiper: + + MySwiper: + + MySwiper: + + MySwiper: + ''' + + + class Main(MDApp): + def build(self): + return Builder.load_string(kv) + + Main().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mdswiper-example.gif + :align: center + +.. warning:: + The width of :class:`MDSwiperItem` is adjusted automatically. Consider changing + that by :attr:`~MDSwiperItem.width_mult`. + +.. warning:: + The width of :class:`MDSwiper` is automatically adjusted according to the width of the window. + +.. rubric:: :class:`~MDSwiper` provides the following events for use: + +.. code-block:: python + + __events__ = ( + "on_swipe", + "on_pre_swipe", + "on_overswipe_right", + "on_overswipe_left", + "on_swipe_left", + "on_swipe_right" + ) + +.. code-block:: kv + + MDSwiper: + on_swipe: print("on_swipe") + on_pre_swipe: print("on_pre_swipe") + on_overswipe_right: print("on_overswipe_right") + on_overswipe_left: print("on_overswipe_left") + on_swipe_left: print("on_swipe_left") + on_swipe_right: print("on_swipe_right") + +Example +======= + +.. code-block:: python + + from kivy.lang.builder import Builder + + from kivymd.app import MDApp + + kv = ''' + + + + + + RelativeLayout: + + FitImage: + source: "guitar.png" + radius: [20,] + + MDBoxLayout: + adaptive_height: True + spacing: "12dp" + + MagicButton: + id: icon + icon: "weather-sunny" + user_font_size: "56sp" + opposite_colors: True + + MDLabel: + text: "MDLabel" + font_style: "H5" + size_hint_y: None + height: self.texture_size[1] + pos_hint: {"center_y": .5} + opposite_colors: True + + + MDScreen: + + MDTopAppBar: + id: toolbar + title: "MDSwiper" + elevation: 10 + pos_hint: {"top": 1} + + MDSwiper: + size_hint_y: None + height: root.height - toolbar.height - dp(40) + y: root.height - self.height - toolbar.height - dp(20) + on_swipe: self.get_current_item().ids.icon.shake() + + MySwiper: + + MySwiper: + + MySwiper: + + MySwiper: + + MySwiper: + ''' + + + class Main(MDApp): + def build(self): + return Builder.load_string(kv) + + + Main().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/mdswiper-on-swipe.gif + :align: center + +How to automatically switch a SwiperItem? +========================================= + +Use method :attr:`~MDSwiper.set_current` which takes the index of :class:`MDSwiperItem` as argument. + +Example +======= + +.. code-block:: kv + + MDSwiper: + id: swiper + + MDSwiperItem: # First widget with index 0 + + MDSwiperItem: # Second widget with index 1 + + MDRaisedButton: + text: "Go to Second" + on_release: swiper.set_current(1) +""" + +__all__ = ("MDSwiperItem", "MDSwiper") + +import os + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.effects.dampedscroll import DampedScrollEffect +from kivy.event import EventDispatcher +from kivy.lang.builder import Builder +from kivy.properties import ( + BooleanProperty, + NumericProperty, + ObjectProperty, + StringProperty, +) +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.scrollview import ScrollView +from kivy.utils import platform + +from kivymd import uix_path + +with open( + os.path.join(uix_path, "swiper", "swiper.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class _ScrollViewHardStop(DampedScrollEffect): + def stop(self, val, t=None): + return super().stop(val, t=0.01) + + +class _ItemsBox(AnchorLayout): + _root = ObjectProperty() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self._update) + Window.bind(on_resize=self._set_size) + + def _update(self, *args): + self._set_size() + + def _set_size(self, *args): + window_size = Window.size + self.size = [ + window_size[0] + - self._root.items_spacing * self._root.width_mult * 2, + self._root.height, + ] + + +class MDSwiperItem(BoxLayout): + """ + :class:`MDSwiperItem` is a :class:`BoxLayout` but it's size is adjusted + automatically. + """ + + _root = ObjectProperty() + _selected = False + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Clock.schedule_once(self._set_size) + Window.bind(on_resize=self._set_size) + + def _set_size(self, *args): + Clock.schedule_once(lambda x: self._root._reset_size()) + if self._selected: + self._selected_size() + else: + self._dismiss_size() + + def _selected_size(self): + size = [ + Window.size[0] + - self._root.items_spacing * self._root.width_mult * 2, + self._root.height, + ] + anim = Animation( + size=size, d=self._root.size_duration, t=self._root.size_transition + ) + anim.start(self) + + def _dismiss_size(self): + size = [ + Window.size[0] + - self._root.items_spacing * (1 + self._root.width_mult) * 2, + self._root.height - self._root.items_spacing * 2, + ] + anim = Animation( + size=size, d=self._root.size_duration, t=self._root.size_transition + ) + anim.start(self) + + +class MDSwiper(ScrollView, EventDispatcher): + items_spacing = NumericProperty("20dp") + """ + The space between each :class:`MDSwiperItem`. + + :attr:`items_spacing` is an :class:`~kivy.properties.NumericProperty` + and defaults to `20dp`. + """ + + transition_duration = NumericProperty(0.2) + """ + Duration of switching between :class:`MDSwiperItem`. + + :attr:`transition_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + size_duration = NumericProperty(0.2) + """ + Duration of changing the size of :class:`MDSwiperItem`. + + :attr:`transition_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + size_transition = StringProperty("out_quad") + """ + The type of animation used for changing the size of :class:`MDSwiperItem`. + + :attr:`size_transition` is an :class:`~kivy.properties.StringProperty` + and defaults to `out_quad`. + """ + + swipe_transition = StringProperty("out_quad") + """ + The type of animation used for swiping. + + :attr:`swipe_transition` is an :class:`~kivy.properties.StringProperty` + and defaults to `out_quad`. + """ + + swipe_distance = NumericProperty("70dp") + """ + Distance to move before swiping the :class:`MDSwiperItem`. + + :attr:`swipe_distance` is an :class:`~kivy.properties.NumericProperty` + and defaults to `70dp`. + """ + + width_mult = NumericProperty(3) + """ + This number is multiplied by :attr:`items_spacing` x2 and + then subtracted from the width of window to specify the width of + :class:`MDSwiperItem`. So by decreasing the :attr:`width_mult` the width + of :class:`MDSwiperItem` increases and vice versa. + + :attr:`width_mult` is an :class:`~kivy.properties.NumericProperty` + and defaults to `3`. + """ + + swipe_on_scroll = BooleanProperty(True) + """ + Wheter to swipe on mouse wheel scrolling or not. + + :attr:`swipe_on_scroll` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + _selected = 0 + _start_touch_x = None + + __events__ = ( + "on_swipe", + "on_pre_swipe", + "on_overswipe_right", + "on_overswipe_left", + "on_swipe_left", + "on_swipe_right", + ) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_swipe") + self.register_event_type("on_pre_swipe") + self.register_event_type("on_overswipe_right") + self.register_event_type("on_overswipe_left") + self.register_event_type("on_swipe_left") + self.register_event_type("on_swipe_right") + + self.effect_cls = _ScrollViewHardStop + + def add_widget(self, widget, index=0): + if issubclass(widget.__class__, MDSwiperItem): + widget._root = self + items_box = _ItemsBox(_root=self) + items_box.add_widget(widget) + self.ids.anchor_scroll.add_widget(items_box) + return + else: + return super().add_widget(widget, index=index) + + def remove_widget(self, widget): + if not issubclass(widget.__class__, MDSwiperItem): + return + + for item_box in self.ids.anchor_scroll.children: + if widget in item_box.children: + return self.ids.anchor_scroll.remove_widget(item_box) + + def set_current(self, index): + """Switch to given :class:`MDSwiperItem` index.""" + + self._selected = index + self.dispatch("on_pre_swipe") + self._reset_size() + self.dispatch("on_swipe") + + def get_current_index(self): + """Returns the current :class:`MDSwiperItem` index.""" + + return self._selected + + def get_current_item(self): + """Returns the current :class:`MDSwiperItem` instance.""" + + return list(reversed(self.ids.anchor_scroll.children))[ + self._selected + ].children[0] + + def get_items(self): + """Returns the list of :class:`MDSwiperItem` children. + + .. note:: + + Use `get_items()` to get the list of children instead of + `MDSwiper.children`. + + """ + + children = list(reversed(self.ids.anchor_scroll.children)) + items = [item.children[0] for item in children] + return items + + def _reset_size(self, *args): + children = list(reversed(self.ids.anchor_scroll.children)) + if not children: + return + + child = children[self._selected] + total_width = self.ids.anchor_scroll.width - Window.width + + if self.get_current_index() == 0: + view_x = child.x - self.items_spacing + elif self.get_current_index() == len(children) - 1: + view_x = ( + child.x + - self.items_spacing * self.width_mult + - self.items_spacing * 2 + ) + else: + view_x = child.x - self.items_spacing * self.width_mult + + anim = Animation( + scroll_x=view_x / total_width, + d=self.transition_duration, + t=self.swipe_transition, + ) + anim.start(self) + + for widget in children: + widget.children[0]._dismiss_size() + widget.children[0]._selected = False + + child.children[0]._selected_size() + child.children[0]._selected = True + + def on_swipe(self): + pass + + def on_pre_swipe(self): + pass + + def on_overswipe_right(self): + pass + + def on_overswipe_left(self): + pass + + def on_swipe_left(self): + pass + + def on_swipe_right(self): + pass + + def swipe_left(self): + previous_index = self._selected - 1 + if previous_index == -1: + self.set_current(0) + self.dispatch("on_overswipe_left") + else: + self.set_current(previous_index) + self.dispatch("on_swipe_left") + + def swipe_right(self): + next_index = self._selected + 1 + last_index = len(self.ids.anchor_scroll.children) - 1 + if next_index == last_index + 1: + self.set_current(last_index) + self.dispatch("on_overswipe_right") + else: + self.set_current(next_index) + self.dispatch("on_swipe_right") + + def on_scroll_start(self, touch, check_children=True): + + if platform in ["ios", "android"]: + return super().on_scroll_start(touch) + + # on touch pad events + if touch.button == "scrollright": + self.swipe_left() + elif touch.button == "scrollleft": + self.swipe_right() + return super().on_scroll_start(touch) + + def on_touch_down(self, touch): + super().on_touch_down(touch) + + if not self.collide_point(touch.pos[0], touch.pos[1]): + return + + if platform not in ["ios", "android"] and self.swipe_on_scroll: + if touch.button == "scrolldown": + self.swipe_right() + elif touch.button == "scrollup": + self.swipe_left() + else: + self._start_touch_x = touch.pos[0] + else: + self._start_touch_x = touch.pos[0] + + def on_touch_up(self, touch): + super().on_touch_up(touch) + if not self._start_touch_x: + return + + if self._start_touch_x: + touch_x_diff = abs(self._start_touch_x - touch.pos[0]) + else: + return + + if touch_x_diff <= self.swipe_distance: + if touch_x_diff == 0: + return + self._reset_size() + return + + # swipe to left + if self._start_touch_x < touch.pos[0]: + self.swipe_left() + # swipe to right + else: + self.swipe_right() + + self._start_touch_x = None + return diff --git a/sbapp/kivymd/uix/tab/__init__.py b/sbapp/kivymd/uix/tab/__init__.py new file mode 100644 index 0000000..c511198 --- /dev/null +++ b/sbapp/kivymd/uix/tab/__init__.py @@ -0,0 +1 @@ +from .tab import MDTabs, MDTabsBase, MDTabsLabel # NOQA F401 diff --git a/sbapp/kivymd/uix/tab/tab.kv b/sbapp/kivymd/uix/tab/tab.kv new file mode 100644 index 0000000..824f82e --- /dev/null +++ b/sbapp/kivymd/uix/tab/tab.kv @@ -0,0 +1,124 @@ +#:import DampedScrollEffect kivy.effects.dampedscroll.DampedScrollEffect + + + + _set_start_tab: False + size_hint: None, 1 + halign: "center" + valign: "center" + group: "tabs" + font: root.font_name + allow_no_selection: False + markup: True + on_width: + if not self._set_start_tab: \ + self.tab_bar.parent._update_indicator( \ + self.tab_bar.parent.carousel.current_slide.tab_label); \ + self._set_start_tab = True + on_tab_bar: + self.text_size = (None, None) \ + if self.tab_bar.parent.allow_stretch else (self.width, None) + on_ref_press: + self.tab_bar.parent.dispatch( \ + "on_ref_press", + self, \ + self.tab, \ + self.tab_bar, \ + self.tab_bar.parent.carousel) + color: + ( \ + self.text_color_active \ + if self.text_color_active else self.specific_secondary_text_color \ + ) \ + if self.state == "down" else \ + ( \ + self.text_color_normal \ + if self.text_color_normal else self.theme_cls.text_color \ + ) + + + + 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" + + _line_x: 0 + _line_width: 0 + _line_height: 0 + _line_radius: 0 + + on_size: + root._update_padding(layout) + + 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) + + MDTabsBar: + id: tab_bar + padding: root.tab_padding + 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 + do_scroll_x: False if layout.width <= self.width else True + + MDGridLayout: + id: layout + rows: 1 + size_hint_y: 1 + adaptive_width: True + on_size: root._update_padding(layout) + + canvas.before: + Color: + rgba: root.underline_color + Line: + width: dp(2) + rectangle: [0, 0, layout.width, dp(2)] + Color: + rgba: + root.theme_cls.accent_color \ + if not root.indicator_color else \ + root.indicator_color + RoundedRectangle: + group: "Indicator_line" + pos: self.pos + size: 0, root.tab_indicator_height + radius: [0,] + Line: + width: dp(2) + rounded_rectangle: + [ \ + root._line_x, \ + self.pos[1], \ + root._line_width, \ + root._line_height, \ + root._line_radius \ + ] diff --git a/sbapp/kivymd/uix/tab/tab.py b/sbapp/kivymd/uix/tab/tab.py new file mode 100755 index 0000000..b78ffd6 --- /dev/null +++ b/sbapp/kivymd/uix/tab/tab.py @@ -0,0 +1,1529 @@ +""" +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(MDFloatLayout, MDTabsBase): + '''Class implementing content for a tab.''' + content_text = StringProperty("") + +.. code-block:: kv + + + content_text + + MDLabel: + text: root.content_text + pos_hint: {"center_x": .5, "center_y": .5} + +All tabs must be contained inside a :class:`~MDTabs` widget: + +.. code-block:: kv + + Root: + + MDTabs: + + Tab: + title: "Tab 1" + content_text: f"This is an example text for {self.title}" + + Tab: + title: "Tab 2" + content_text: f"This is an example text for {self.title}" + + ... + +Example with tab icon +--------------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.tab import MDTabsBase + from kivymd.uix.floatlayout import MDFloatLayout + from kivymd.icon_definitions import md_icons + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "Example Tabs" + + MDTabs: + id: tabs + on_tab_switch: app.on_tab_switch(*args) + + + + + MDIconButton: + id: icon + icon: root.icon + user_font_size: "48sp" + pos_hint: {"center_x": .5, "center_y": .5} + ''' + + + class Tab(MDFloatLayout, 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 tab_name in self.icons: + self.root.ids.tabs.add_widget(Tab(icon=tab_name)) + + 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; + ''' + # get the tab icon. + count_icon = instance_tab.icon + # print it on shell/bash. + print(f"Welcome to {count_icon}' tab'") + + + 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, the class will send a message + stating that the icon could not be found. + + if the tab has no icon, title or tab_label_text, the class will raise a + ValueError. + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.floatlayout import MDFloatLayout + from kivymd.uix.tab import MDTabsBase + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "Example Tabs" + + MDTabs: + id: tabs + on_tab_switch: app.on_tab_switch(*args) + + + + + MDLabel: + id: label + text: "Tab 0" + halign: "center" + ''' + + + class Tab(MDFloatLayout, 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(title=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 kivymd.app import MDApp + from kivymd.uix.tab import MDTabsBase + from kivymd.uix.floatlayout import MDFloatLayout + from kivymd.icon_definitions import md_icons + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "Example Tabs" + + MDTabs: + id: tabs + ''' + + + class Tab(MDFloatLayout, 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(icon=name_tab, title=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 = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + 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()[-1] + ) + + + 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 kivymd.app import MDApp + from kivymd.uix.floatlayout import MDFloatLayout + from kivymd.font_definitions import fonts + from kivymd.uix.tab import MDTabsBase + from kivymd.icon_definitions import md_icons + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + 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(MDFloatLayout, 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 kivymd.app import MDApp + from kivymd.icon_definitions import md_icons + from kivymd.uix.floatlayout import MDFloatLayout + from kivymd.uix.tab import MDTabsBase + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "Example Tabs" + + MDTabs: + id: tabs + + + + + MDBoxLayout: + orientation: "vertical" + pos_hint: {"center_x": .5, "center_y": .5} + size_hint: None, None + spacing: dp(48) + + MDIconButton: + id: icon + icon: "arrow-right" + user_font_size: "48sp" + on_release: app.switch_tab_by_name() + + MDIconButton: + id: icon2 + icon: "page-next" + user_font_size: "48sp" + on_release: app.switch_tab_by_object() + ''' + + + class Tab(MDFloatLayout, MDTabsBase): + '''Class implementing content for a tab.''' + + + class Example(MDApp): + icons = list(md_icons.keys())[15:30] + + def build(self): + self.iter_list_names = 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(tab_label_text=name_tab)) + self.iter_list_objects = iter(list(self.root.ids.tabs.get_tab_list())) + + def switch_tab_by_object(self): + try: + x = next(self.iter_list_objects) + print(f"Switch slide by object, next element to show: [{x}]") + self.root.ids.tabs.switch_tab(x) + except StopIteration: + # reset the iterator an begin again. + self.iter_list_objects = iter(list(self.root.ids.tabs.get_tab_list())) + self.switch_tab_by_object() + + def switch_tab_by_name(self): + '''Switching the tab by name.''' + + try: + x = next(self.iter_list_names) + print(f"Switch slide by name, next element to show: [{x}]") + self.root.ids.tabs.switch_tab(x) + except StopIteration: + # Reset the iterator an begin again. + self.iter_list_names = iter(list(self.icons)) + self.switch_tab_by_name() + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/switching-tab-by-name.gif + :align: center +""" + +__all__ = ("MDTabs", "MDTabsBase") + +import os +from typing import Union + +from kivy.clock import Clock +from kivy.graphics.texture import Texture +from kivy.lang import Builder +from kivy.logger import Logger +from kivy.metrics import dp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + BoundedNumericProperty, + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.anchorlayout import AnchorLayout +from kivy.uix.behaviors import ToggleButtonBehavior +from kivy.uix.scrollview import ScrollView +from kivy.uix.widget import Widget +from kivy.utils import boundary + +from kivymd import uix_path +from kivymd.font_definitions import fonts, theme_font_styles +from kivymd.icon_definitions import md_icons +from kivymd.theming import ThemableBehavior, ThemeManager +from kivymd.uix.behaviors import ( + FakeRectangularElevationBehavior, + RectangularRippleBehavior, + SpecificBackgroundColorBehavior, +) +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.carousel import MDCarousel +from kivymd.uix.label import MDLabel + +with open(os.path.join(uix_path, "tab", "tab.kv"), encoding="utf-8") as kv_file: + Builder.load_string(kv_file.read()) + + +class MDTabsException(Exception): + pass + + +class MDTabsLabel(ToggleButtonBehavior, RectangularRippleBehavior, MDLabel): + """This class it represent the label of each tab.""" + + text_color_normal = ColorProperty(None) + text_color_active = ColorProperty(None) + tab = ObjectProperty() + tab_bar = ObjectProperty() + font_name = StringProperty("Roboto") + + def __init__(self, **kwargs): + self.split_str = " ,-" + super().__init__(**kwargs) + self.max_lines = 2 + self.size_hint_x = None + self.size_hint_min_x = dp(90) + self.min_space = dp(98) + self.bind( + text=self._update_text_size, + ) + + def on_release(self) -> None: + 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, instance_tabs_label, texture: Texture) -> None: + # Just save the minimum width of the label based of the content. + if texture: + max_width = dp(360) + min_width = dp(90) + if texture.width > max_width: + self.width = max_width + self.text_size = (max_width, None) + elif texture.width < min_width: + self.width = min_width + else: + self.width = texture.width + + def _update_text_size(self, *args): + if not self.tab_bar: + return + if self.tab_bar.parent.allow_stretch is True: + self.text_size = (None, None) + else: + self.width = self.tab_bar.parent.fixed_tab_label_width + self.text_size = (self.width, None) + Clock.schedule_once(self.tab_bar._label_request_indicator_update, 0) + + +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. + """ + + icon = StringProperty() + """ + This property will set the Tab's Label Icon. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + title_icon_mode = OptionProperty("Lead", options=["Lead", "Top"]) + """ + This property sets the mode in wich the tab's title and icon are shown. + + :attr:`title_icon_mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Lead'`. + """ + + title = StringProperty() + """ + This property will set the Name of the tab. + + .. note:: + As a side note. + + All tabs have set `markup = True`. + Thanks to this, you can use the kivy markup language to set a colorful + and fully customizable tabs titles. + + .. warning:: + The material design requires that every title label is written in + capital letters, because of this, the `string.upper()` will be applied + to it's contents. + + :attr:`title` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + title_is_capital = BooleanProperty(False) + """ + This value controls wether if the title property should be converted to + capital letters. + + :attr:`title_is_capital` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + text = StringProperty(deprecated=True) + """ + This property is the actual title of the tab. + use the property :attr:`icon` and :attr:`title` to set this property + correctly. + + This property is kept public for specific and backward compatibility + purposes. + + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + + .. deprecated:: 1.0.0 + Use :attr:`tab_label_text` instead. + """ + + tab_label_text = StringProperty() + """ + This property is the actual title's Label of the tab. + use the property :attr:`icon` and :attr:`title` to set this property + correctly. + + This property is kept public for specific and backward compatibility + purposes. + + :attr:`tab_label_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 _get_label_font_style(self): + if self.tab_label: + return self.tab_label.font_style + + def _set_label_font_style(self, value): + if self.tab_label: + if value in theme_font_styles: + self.tab_label.font_style = value + else: + raise ValueError( + "tab_label_font_style:\n\t" + "font_style not found in theme_font_styles\n\t" + f"font_style = {value}" + ) + else: + Clock.schedule_once(lambda x: self._set_label_font_style(value)) + return True + + tab_label_font_style = AliasProperty( + _get_label_font_style, + _set_label_font_style, + cache=True, + ) + """ + :attr:`tab_label_font_style` is an :class:`~kivy.properties.AliasProperty` + that behavies similar to an :class:`~kivy.properties.OptionProperty`. + + This property's behavior allows the developer to use any new label style + registered to the app. + + This property will affect the Tab's Title Label widget. + """ + + def __init__(self, **kwargs): + self.tab_label = MDTabsLabel(tab=self) + super().__init__(**kwargs) + self.bind( + icon=self._update_text, + title=self._update_text, + title_icon_mode=self._update_text, + text=self.update_label_text, + tab_label_text=self.update_label_text, + title_is_capital=self.update_label_text, + ) + Clock.schedule_once( + self._update_text + ) # this will ensure the text is correct + + def _update_text(self, *args): + # Ensures that the title is in capital letters. + if self.title and self.title_is_capital is True: + if self.title != self.title.upper(): + self.title = self.title.upper() + # Avoids event recursion. + return + # Add the icon. + if self.icon and self.icon in md_icons: + self.tab_label_text = f"[size=24sp][font={fonts[-1]['fn_regular']}]{md_icons[self.icon]}[/size][/font]" + if self.title: + self.tab_label_text = ( + self.text + + (" " if self.title_icon_mode == "Lead" else "\n") + + self.title + ) + # Add the title. + else: + if self.icon: + Logger.error( + f"{self}: [UID] = [{self.uid}]:\n\t" + f"Icon '{self.icon}' not found in md_icons" + ) + if self.title: + self.tab_label_text = self.title + else: + if not self.tab_label_text: + raise ValueError( + f"{self}: [UID] = [{self.uid}]:\n\t" + "No valid Icon was found.\n\t" + "No valid Title was found.\n\t" + f"Icon\t= '{self.icon}'\n\t" + f"Title\t= '{self.title}'\n\t" + ) + + self.tab_label.padding = dp(16), 0 + self.update_label_text(None, self.tab_label_text) + + def update_label_text(self, instance_user_tab, text_tab: str) -> None: + self.tab_label.text = self.text = self.tab_label_text + + def on_text(self, instance_user_tab, text_tab: str) -> None: + 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): + if self.lock_swiping: # lock a 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: Union[float, None], scroll_y: Union[float, None] + ) -> None: + """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, FakeRectangularElevationBehavior, MDBoxLayout +): + """ + This class is just a boxlayout that contains the scroll view for tabs. + It is also responsible for resizing the tab shortcut when necessary. + """ + + target = ObjectProperty(None, allownone=True) + """ + It 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): + canvas_instructions = self.layout.canvas.before.get_group( + "Indicator_line" + ) + return canvas_instructions[0] + + indicator = AliasProperty(get_rect_instruction, cache=True) + """ + It is the :class:`~kivy.graphics.vertex_instructions.RoundedRectangle` + 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): + super().__init__(**kwargs) + + def update_indicator( + self, x: Union[float, int], w: Union[float, int], radius=None + ) -> None: + # Update position and size of the indicator. + if self.parent.tab_indicator_type == "line-round": + self.parent._line_x = x + self.parent._line_width = w + self.parent._line_height = self.parent.tab_indicator_height + self.parent._line_radius = self.parent.tab_indicator_height / 2 + elif self.parent.tab_indicator_type == "line-rect": + self.parent._line_x = x + self.parent._line_width = w + self.parent._line_height = self.parent.tab_indicator_height + else: + self.indicator.pos = (x, 0) + self.indicator.size = (w, self.parent.tab_indicator_height) + if radius: + self.indicator.radius = radius + + def tab_bar_autoscroll(self, instance_tab_label: MDTabsLabel, step: float): + # Automatic scroll animation of the tab bar. + bound_left = self.center_x - self.x + bound_right = self.layout.width - bound_left + dt = instance_tab_label.center_x - bound_left + sx, sy = self.scrollview.convert_distance_to_scroll(dt, 0) + lsx = self.last_scroll_x # ast scroll x of the tab bar + scroll_is_late = lsx < sx # determine scroll direction + dst = abs(lsx - sx) * step # distance to run + + if not dst: + return + if scroll_is_late and instance_tab_label.center_x > bound_left: + x = lsx + dst + elif not scroll_is_late and instance_tab_label.center_x < bound_right: + x = lsx - dst + else: + return + x = boundary(x, 0.0, 1.0) + self.scrollview.goto(x, None) + + def android_animation( + self, instance_carousel: MDTabsCarousel, offset: Union[float, int] + ): + # Try to reproduce the android animation effect. + if offset != 0 and abs(offset) < instance_carousel.width: + forward = offset < 0 + offset = abs(offset) + step = offset / float(instance_carousel.width) + indicator_animation = self.parent.tab_indicator_anim + + skip_slide = ( + instance_carousel.slides[instance_carousel._skip_slide] + if instance_carousel._skip_slide is not None + else None + ) + next_slide = ( + instance_carousel.next_slide + if forward + else instance_carousel.previous_slide + ) + self.target = skip_slide if skip_slide else next_slide + + if not self.target: + return + + a = instance_carousel.current_slide.tab_label + b = self.target.tab_label + self.tab_bar_autoscroll(b, step) + + # Avoids the animation if `indicator_animation` is True. + if indicator_animation is False: + return + gap_x = abs((a.x) - (b.x)) + gap_w = (b.width) - (a.width) + if forward: + x_step = a.x + (gap_x * step) + else: + x_step = a.x - gap_x * step + w_step = a.width + (gap_w * step) + self.update_indicator(x_step, w_step) + + def _label_request_indicator_update(self, *args): + widget = self.carousel.current_slide.tab_label + self.update_indicator(widget.x, widget.width) + + +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. + """ + + 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_padding = ListProperty([0, 0, 0, 0]) + """ + Padding of the tab bar. + + :attr:`tab_padding` is an :class:`~kivy.properties.ListProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + 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'`. + """ + + tab_indicator_type = OptionProperty( + "line", options=["line", "fill", "round", "line-round", "line-rect"] + ) + """ + Type of tab indicator. Available options are: `'line'`, `'fill'`, + `'round'`, `'line-rect'` and `'line-round'`. + + :attr:`tab_indicator_type` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'line'`. + """ + + tab_hint_x = BooleanProperty(False) + """ + This option affects the size of each child. if it's `True`, the size of + each tab will be ignored and will use the size available by the container. + + :attr:`tab_hint_x` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + 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 `True`, the tab will update dynamically (if :attr:`tab_hint_x` is `True`) + to it's content width, and wrap any text if the widget is wider than `"360dp"`. + + If `False`, the tab won't update to it's maximum texture width. + this means that the `fixed_tab_label_width` will be used as the label + width. this will wrap any text inside to fit the fixed value. + + :attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + fixed_tab_label_width = NumericProperty("140dp") + """ + If :attr:`allow_stretch` is `False`, the class will set this value as the + width to all the tabs title label. + + :attr:`fixed_tab_label_width` is an :class:`~kivy.properties.NumericProperty` + and defaults to `140dp`. + """ + + background_color = ColorProperty(None) + """ + Background color of tabs in ``rgba`` format. + + :attr:`background_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + underline_color = ColorProperty([0, 0, 0, 0]) + """ + Underline color of tabs in ``rgba`` format. + + :attr:`underline_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + text_color_normal = ColorProperty(None) + """ + Text color of the label when it is not selected. + + :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + text_color_active = ColorProperty(None) + """ + Text color of the label when it is selected. + + :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + elevation = NumericProperty(0) + """ + Tab value elevation. + + .. seealso:: + + `Behaviors/Elevation `_ + + :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + indicator_color = ColorProperty(None) + """ + Color indicator in ``rgba`` format. + + :attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + 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'`. + """ + + ripple_duration = NumericProperty(2) + """ + Ripple duration when long touching to tab. + + :attr:`ripple_duration` is an :class:`~kivy.properties.NumericProperty` + and defaults to `2`. + """ + + no_ripple_effect = BooleanProperty(True) + """ + Whether to use the ripple effect when tapping on a tab. + + :attr:`no_ripple_effect` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + title_icon_mode = OptionProperty("Lead", options=["Lead", "Top"]) + """ + This property sets the mode in wich the tab's title and icon are shown. + + :attr:`title_icon_mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Lead'`. + """ + + force_title_icon_mode = BooleanProperty(True) + """ + If this property is se to `True`, it will force the class to update every + tab inside the scroll view to the current `title_icon_mode` + + :attr:`force_title_icon_mode` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + 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) + self.theme_cls.bind( + primary_palette=self.update_icon_color, + theme_style=self.update_icon_color, + ) + self.bind( + force_title_icon_mode=self._parse_icon_mode, + title_icon_mode=self._parse_icon_mode, + ) + self.bind(tab_hint_x=self._update_tab_hint_x) + + def update_icon_color( + self, + instance_theme_manager: ThemeManager, + name_theme_style_name_palette: str, + ) -> None: + """ + Called when the app's color scheme or style has changed + (dark theme/light theme). + """ + + for tab_label in self.get_tab_list(): + if not self.text_color_normal: + tab_label.text_color_normal = self.theme_cls.text_color + if not self.text_color_active: + tab_label.text_color_active = self.specific_secondary_text_color + + def switch_tab(self, name_tab: Union[MDTabsLabel, str], search_by="text"): + """ + This method switch between tabs + name_tab can be either a String or a :class:`~MDTabsBase`. + + `search_by` will look up through the properties of every tab. + + If the value doesnt match, it will raise a ValueError. + + Search_by options: + text : will search by the raw text of the label (`tab_label_text`) + icon : will search by the `icon` property + title : will search by the `title` property + """ + + if isinstance(name_tab, str): + if search_by == "title": + for tab_instance in self.tab_bar.parent.carousel.slides: + if tab_instance.title_is_capital is True: + _name_tab = name_tab.upper() + else: + _name_tab = name_tab + if tab_instance.title == _name_tab: + self.carousel.load_slide(tab_instance) + return + # Search by icon. + elif search_by == "icon": + for tab_instance in self.tab_bar.parent.carousel.slides: + if tab_instance.icon == name_tab: + self.carousel.load_slide(tab_instance) + return + # Search by title. + else: + for tab_instance in self.tab_bar.parent.carousel.slides: + if tab_instance.tab_label_text == name_tab: + self.carousel.load_slide(tab_instance) + return + raise ValueError( + "switch_tab:\n\t" + "name_tab not found in the tab list\n\t" + f"search_by = {repr(search_by)} \n\t" + f"name_tab = {repr(name_tab)} \n\t" + ) + else: + self.carousel.load_slide(name_tab.tab) + + def get_tab_list(self) -> list: + """Returns a list of :class:`~MDTabsLabel` objects.""" + + return self.tab_bar.layout.children[::-1] + + def get_slides(self) -> list: + """Returns a list of user tab objects.""" + + return self.carousel.slides + + def get_current_tab(self): + """ + Returns current tab object. + + .. versionadded:: 1.0.0 + """ + + return self.carousel.current_slide + + def add_widget(self, widget, index=0, canvas=None): + # You can add only subclass of MDTabsBase. + if not isinstance(widget, (MDTabsBase, MDTabsMain, MDTabsBar)): + raise ValueError( + f"MDTabs[{self.uid}].add_widget:\n\t" + "The widget provided is not a subclass of MDTabsBase." + ) + if len(self.children) >= 2: + try: + # FIXME: Can't set the value of the `no_ripple_effect` + # and `ripple_duration` properties for widget.tab_label. + widget.tab_label._no_ripple_effect = self.no_ripple_effect + widget.tab_label.ripple_duration_in_slow = self.ripple_duration + widget.tab_label.group = str(self) + widget.tab_label.tab_bar = self.tab_bar + widget.tab_label.font_name = self.font_name + widget.tab_label.text_color_normal = ( + self.text_color_normal + if self.text_color_normal + else self.specific_secondary_text_color + ) + widget.tab_label.text_color_active = ( + self.text_color_active + if self.text_color_active + else self.specific_text_color + ) + self.bind( + allow_stretch=widget.tab_label._update_text_size, + fixed_tab_label_width=widget.tab_label._update_text_size, + font_name=widget.tab_label.setter("font_name"), + text_color_active=widget.tab_label.setter( + "text_color_active" + ), + text_color_normal=widget.tab_label.setter( + "text_color_normal" + ), + ) + Clock.schedule_once(widget.tab_label._update_text_size, 0) + self.tab_bar.layout.add_widget(widget.tab_label) + self.carousel.add_widget(widget) + if self.force_title_icon_mode is True: + widget.title_icon_mode = self.title_icon_mode + Clock.schedule_once( + self.tab_bar._label_request_indicator_update, 0 + ) + return + except AttributeError: + pass + if isinstance(widget, (MDTabsMain, MDTabsBar)): + return super().add_widget(widget) + + def remove_widget(self, widget): + # You can remove only subclass of MDTabsLabel or MDTabsBase. + if not issubclass(widget.__class__, (MDTabsLabel, MDTabsBase)): + raise MDTabsException( + "MDTabs can remove only subclass of MDTabsLabel or MDTabsBase" + ) + # If the widget is an instance of MDTabsBase, then the widget is + # set as the widget's tab_label object. + if issubclass(widget.__class__, MDTabsBase): + slide = widget + title_label = widget.tab_label + else: + # We already got the label, so we set the slide reference. + slide = widget.tab + title_label = widget + # Set memory. + # Search object next tab. + # Clean all bindings to allow the widget to be collected. + self.unbind( + allow_stretch=title_label._update_text_size, + fixed_tab_label_width=title_label._update_text_size, + font_name=title_label.setter("font_name"), + text_color_active=title_label.setter("text_color_active"), + text_color_normal=title_label.setter("text_color_normal"), + ) + self.carousel.remove_widget(slide) + self.tab_bar.layout.remove_widget(title_label) + # Clean the references. + slide = None + title_label = None + widget = None + + def on_slide_progress(self, *args) -> None: + """ + This event is deployed every available frame while the tab is scrolling. + """ + + def on_carousel_index(self, instance_tabs_carousel, index: int) -> None: + """ + Called when the Tab index have changed. + + This event is deployed by the built in carousel of the class. + """ + + # When the index of the carousel change, update tab indicator, + # select the current tab and reset threshold data. + if instance_tabs_carousel.current_slide: + current_tab_label = instance_tabs_carousel.current_slide.tab_label + if current_tab_label.state == "normal": + # current_tab_label._do_press() + current_tab_label.dispatch("on_release") + current_tab_label._release_group(self) + current_tab_label.state = "down" + + if self.tab_indicator_type == "round": + self.tab_indicator_height = self.tab_bar_height + if index == 0: + radius = [ + 0, + self.tab_bar_height / 2, + self.tab_bar_height / 2, + 0, + ] + self.tab_bar.update_indicator( + current_tab_label.x, current_tab_label.width, radius + ) + elif index == len(self.get_tab_list()) - 1: + radius = [ + self.tab_bar_height / 2, + 0, + 0, + self.tab_bar_height / 2, + ] + self.tab_bar.update_indicator( + current_tab_label.x, current_tab_label.width, radius + ) + else: + radius = [ + self.tab_bar_height / 2, + ] + self.tab_bar.update_indicator( + current_tab_label.x, current_tab_label.width, radius + ) + elif ( + self.tab_indicator_type == "fill" + or self.tab_indicator_type == "line-round" + or self.tab_indicator_type == "line-rect" + ): + self.tab_indicator_height = self.tab_bar_height + self.tab_bar.update_indicator( + current_tab_label.x, current_tab_label.width + ) + else: + self.tab_bar.update_indicator( + current_tab_label.x, current_tab_label.width + ) + + def on_ref_press(self, *args) -> None: + """ + This event will be launched every time the user press a markup enabled + label with a link or reference inside. + """ + + def on_tab_switch(self, *args) -> None: + """This event is launched every time the current tab is changed.""" + + def on_size(self, instance_tab, size: list) -> None: + """Called when the application screen is resized.""" + + if self.carousel.current_slide: + self._update_indicator(self.carousel.current_slide.tab_label) + + def _update_tab_hint_x(self, *args): + if not self.ids.layout.children: + return + if self.tab_hint_x is True: + self.fixed_tab_label_width = self.width // len( + self.ids.layout.children + ) + self.allow_stretch = False + else: + self.allow_stretch = True + + def _parse_icon_mode(self, *args): + if self.force_title_icon_mode is True: + for slide in self.carousel.slides: + slide.title_icon_mode = self.title_icon_mode + if self.title_icon_mode == "Top": + self.tab_bar_height = dp(72) + else: + self.tab_bar_height = dp(48) + + def _carousel_bind(self, interval): + self.carousel.bind(on_slide_progress=self._on_slide_progress) + + def _on_slide_progress(self, *args): + self.dispatch("on_slide_progress", args) + + def _update_indicator(self, current_tab_label): + def update_indicator(interval): + self.tab_bar.update_indicator( + current_tab_label.x, current_tab_label.width + ) + + if not current_tab_label: + current_tab_label = self.tab_bar.layout.children[-1] + Clock.schedule_once(update_indicator) + + def _update_padding(self, layout, *args): + if self.tab_hint_x is True: + layout.padding = [0, 0] + Clock.schedule_once(self._update_tab_hint_x) + return True + padding = [0, 0] + # FIXME: It's not entirely clear why the `padding = [dp (52), 0]` + # instruction is needed? This creates an extra 52px left padding and + # looks like a bug. This instruction was added by the contributors in + # previous commits and I have not yet figured out why this was done. + # This is more efficient than to use sum([layout.children]). + # width = layout.width - (layout.padding[0] * 2) + # Forces the padding of the tab_bar when the tab_bar is scrollable. + # if width > self.width: + # padding = [dp(52), 0] + # Set the new padding. + layout.padding = padding + # Update the indicator. + if self.carousel.current_slide: + self._update_indicator(self.carousel.current_slide.tab_label) + Clock.schedule_once( + lambda x: setattr( + self.carousel.current_slide.tab_label, "state", "down" + ), + -1, + ) + return True diff --git a/sbapp/kivymd/uix/taptargetview.py b/sbapp/kivymd/uix/taptargetview.py new file mode 100644 index 0000000..1e57ff0 --- /dev/null +++ b/sbapp/kivymd/uix/taptargetview.py @@ -0,0 +1,849 @@ +""" +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.anim_ripple = None + + 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. + if self.anim_ripple is not None: + 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/sbapp/kivymd/uix/templates/__init__.py b/sbapp/kivymd/uix/templates/__init__.py new file mode 100644 index 0000000..0e627d7 --- /dev/null +++ b/sbapp/kivymd/uix/templates/__init__.py @@ -0,0 +1,10 @@ +""" +Templates +========= + +Base classes for controlling the scale, rotation of the widget, etc. +""" + +from .rotatewidget import RotateWidget +from .scalewidget import ScaleWidget +from .stencilwidget import StencilWidget diff --git a/sbapp/kivymd/uix/templates/rotatewidget/__init__.py b/sbapp/kivymd/uix/templates/rotatewidget/__init__.py new file mode 100644 index 0000000..1fb518f --- /dev/null +++ b/sbapp/kivymd/uix/templates/rotatewidget/__init__.py @@ -0,0 +1 @@ +from .rotatewidget import RotateWidget diff --git a/sbapp/kivymd/uix/templates/rotatewidget/rotatewidget.kv b/sbapp/kivymd/uix/templates/rotatewidget/rotatewidget.kv new file mode 100644 index 0000000..77b57df --- /dev/null +++ b/sbapp/kivymd/uix/templates/rotatewidget/rotatewidget.kv @@ -0,0 +1,9 @@ + + canvas.before: + PushMatrix + Rotate: + angle: self.rotate_value_angle + axis: tuple(self.rotate_value_axis) + origin: self.center + canvas.after: + PopMatrix diff --git a/sbapp/kivymd/uix/templates/rotatewidget/rotatewidget.py b/sbapp/kivymd/uix/templates/rotatewidget/rotatewidget.py new file mode 100644 index 0000000..d3491df --- /dev/null +++ b/sbapp/kivymd/uix/templates/rotatewidget/rotatewidget.py @@ -0,0 +1,128 @@ +""" +Templates/RotateWidget +====================== + +.. versionadded:: 1.0.0 + +Base class for controlling the rotate of the widget. + +.. note:: See `kivy.graphics.Rotate + `_ + for more information. + +Kivy +---- + +.. code-block:: python + + from kivy.animation import Animation + from kivy.lang import Builder + from kivy.app import App + from kivy.properties import NumericProperty + from kivy.uix.button import Button + + KV = ''' + Screen: + + RotateButton: + size_hint: .5, .5 + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.change_rotate(self) + + canvas.before: + PushMatrix + Rotate: + angle: self.rotate_value_angle + axis: 0, 0, 1 + origin: self.center + canvas.after: + PopMatrix + ''' + + + class RotateButton(Button): + rotate_value_angle = NumericProperty(0) + + + class Test(App): + def build(self): + return Builder.load_string(KV) + + def change_rotate(self, instance_button: Button) -> None: + Animation(rotate_value_angle=45, d=0.3).start(instance_button) + + + Test().run() + +KivyMD +------ + +.. code-block:: python + + from kivy.animation import Animation + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.button import MDRaisedButton + from kivymd.uix.templates import RotateWidget + + KV = ''' + MDScreen: + + RotateButton: + size_hint: .5, .5 + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.change_rotate(self) + elevation:0 + ''' + + + class RotateButton(MDRaisedButton, RotateWidget): + pass + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def change_rotate(self, instance_button: MDRaisedButton) -> None: + Animation(rotate_value_angle=45, d=0.3).start(instance_button) + + + Test().run() +""" + +__all__ = ("RotateWidget",) + +import os + +from kivy.lang import Builder +from kivy.properties import ListProperty, NumericProperty + +from kivymd import uix_path + +with open( + os.path.join(uix_path, "templates", "rotatewidget", "rotatewidget.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class RotateWidget: + """Base class for controlling the rotate of the widget.""" + + rotate_value_angle = NumericProperty(0) + """ + Property for getting/setting the angle of the rotation. + + :attr:`rotate_value_angle` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + rotate_value_axis = ListProperty((0, 0, 1)) + """ + Property for getting/setting the axis of the rotation. + + :attr:`rotate_value_axis` is an :class:`~kivy.properties.NumericProperty` + and defaults to `(0, 0, 1)`. + """ diff --git a/sbapp/kivymd/uix/templates/scalewidget/__init__.py b/sbapp/kivymd/uix/templates/scalewidget/__init__.py new file mode 100644 index 0000000..45d304d --- /dev/null +++ b/sbapp/kivymd/uix/templates/scalewidget/__init__.py @@ -0,0 +1 @@ +from .scalewidget import ScaleWidget diff --git a/sbapp/kivymd/uix/templates/scalewidget/scalewidget.kv b/sbapp/kivymd/uix/templates/scalewidget/scalewidget.kv new file mode 100644 index 0000000..5fe002f --- /dev/null +++ b/sbapp/kivymd/uix/templates/scalewidget/scalewidget.kv @@ -0,0 +1,10 @@ + + canvas.before: + PushMatrix + Scale: + x: self.scale_value_x + y: self.scale_value_y + z: self.scale_value_x + origin: self.center + canvas.after: + PopMatrix diff --git a/sbapp/kivymd/uix/templates/scalewidget/scalewidget.py b/sbapp/kivymd/uix/templates/scalewidget/scalewidget.py new file mode 100644 index 0000000..9f9204f --- /dev/null +++ b/sbapp/kivymd/uix/templates/scalewidget/scalewidget.py @@ -0,0 +1,150 @@ +""" +Templates/ScaleWidget +===================== + +.. versionadded:: 1.0.0 + +Base class for controlling the scale of the widget. + +.. note:: See `kivy.graphics.Scale + `_ + for more information. + +Kivy +---- + +.. code-block:: python + + from kivy.animation import Animation + from kivy.lang import Builder + from kivy.properties import NumericProperty + from kivy.uix.button import Button + from kivy.app import App + + + KV = ''' + Screen: + + ScaleButton: + size_hint: .5, .5 + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.change_scale(self) + + canvas.before: + PushMatrix + Scale: + x: self.scale_value_x + y: self.scale_value_y + z: self.scale_value_x + origin: self.center + canvas.after: + PopMatrix + ''' + + + class ScaleButton(Button): + scale_value_x = NumericProperty(1) + scale_value_y = NumericProperty(1) + scale_value_z = NumericProperty(1) + + + class Test(App): + def build(self): + return Builder.load_string(KV) + + def change_scale(self, instance_button: Button) -> None: + Animation( + scale_value_x=0.5, + scale_value_y=0.5, + scale_value_z=0.5, + d=0.3, + ).start(instance_button) + + + Test().run() + +KivyMD +------ + +.. code-block:: python + + from kivy.animation import Animation + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.button import MDRaisedButton + from kivymd.uix.templates import ScaleWidget + + KV = ''' + MDScreen: + + ScaleButton: + size_hint: .5, .5 + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.change_scale(self) + elevation:0 + ''' + + + class ScaleButton(MDRaisedButton, ScaleWidget): + pass + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def change_scale(self, instance_button: MDRaisedButton) -> None: + Animation( + scale_value_x=0.5, + scale_value_y=0.5, + scale_value_z=0.5, + d=0.3, + ).start(instance_button) + + + Test().run() +""" + +__all__ = ("ScaleWidget",) + +import os + +from kivy.lang import Builder +from kivy.properties import NumericProperty + +from kivymd import uix_path + +with open( + os.path.join(uix_path, "templates", "scalewidget", "scalewidget.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class ScaleWidget: + """Base class for controlling the scale of the widget.""" + + scale_value_x = NumericProperty(1) + """ + X-axis value. + + :attr:`scale_value_x` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + scale_value_y = NumericProperty(1) + """ + Y-axis value. + + :attr:`scale_value_y` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + scale_value_z = NumericProperty(1) + """ + Z-axis value. + + :attr:`scale_value_z` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ diff --git a/sbapp/kivymd/uix/templates/stencilwidget/__init__.py b/sbapp/kivymd/uix/templates/stencilwidget/__init__.py new file mode 100644 index 0000000..a249f51 --- /dev/null +++ b/sbapp/kivymd/uix/templates/stencilwidget/__init__.py @@ -0,0 +1 @@ +from .stencilwidget import StencilWidget diff --git a/sbapp/kivymd/uix/templates/stencilwidget/stencilwidget.kv b/sbapp/kivymd/uix/templates/stencilwidget/stencilwidget.kv new file mode 100644 index 0000000..04807dc --- /dev/null +++ b/sbapp/kivymd/uix/templates/stencilwidget/stencilwidget.kv @@ -0,0 +1,19 @@ + + canvas.before: + StencilPush + RoundedRectangle: + pos: root.pos + size: root.size + # FIXME: Sometimes the radius has the value [], which get a + # `GraphicException: Invalid radius value, must be list of tuples/numerics` error + radius: root.radius if root.radius else [0, 0, 0, 0] + StencilUse + canvas.after: + StencilUnUse + RoundedRectangle: + pos: root.pos + size: root.size + # FIXME: Sometimes the radius has the value [], which get a + # `GraphicException: Invalid radius value, must be list of tuples/numerics` error + radius: root.radius if root.radius else [0, 0, 0, 0] + StencilPop diff --git a/sbapp/kivymd/uix/templates/stencilwidget/stencilwidget.py b/sbapp/kivymd/uix/templates/stencilwidget/stencilwidget.py new file mode 100644 index 0000000..3aa8716 --- /dev/null +++ b/sbapp/kivymd/uix/templates/stencilwidget/stencilwidget.py @@ -0,0 +1,116 @@ +""" +Templates/StencilWidget +======================= + +.. versionadded:: 1.0.0 + +Base class for controlling the stencil instructions of the widget. + +.. note:: See `Stencil instructions + `_ + for more information. + +Kivy +---- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.app import App + + KV = ''' + Carousel: + + Button: + size_hint: .9, .8 + pos_hint: {"center_x": .5, "center_y": .5} + + canvas.before: + StencilPush + RoundedRectangle: + pos: root.pos + size: root.size + StencilUse + canvas.after: + StencilUnUse + RoundedRectangle: + pos: root.pos + size: root.size + StencilPop + ''' + + + class Test(App): + def build(self): + return Builder.load_string(KV) + + + Test().run() + +KivyMD +------ + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.templates import StencilWidget + from kivymd.utils.fitimage import FitImage + + KV = ''' + MDCarousel: + + StencilImage: + size_hint: .9, .8 + pos_hint: {"center_x": .5, "center_y": .5} + source: "image.png" + ''' + + + class StencilImage(FitImage, StencilWidget): + pass + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + + Test().run() +""" + +__all__ = ("StencilWidget",) + +import os + +from kivy.lang import Builder +from kivy.properties import VariableListProperty + +from kivymd import uix_path + +with open( + os.path.join(uix_path, "templates", "stencilwidget", "stencilwidget.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class StencilWidget: + """Base class for controlling the stencil instructions of the widget""" + + radius = VariableListProperty([0], length=4) + """ + Canvas radius. + + .. versionadded:: 1.0.0 + + .. code-block:: python + + # Top left corner slice. + MDWidget: + radius: [25, 0, 0, 0] + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[0, 0, 0, 0]`. + """ diff --git a/sbapp/kivymd/uix/textfield/__init__.py b/sbapp/kivymd/uix/textfield/__init__.py new file mode 100644 index 0000000..6ac1738 --- /dev/null +++ b/sbapp/kivymd/uix/textfield/__init__.py @@ -0,0 +1,2 @@ +# NOQA F401 +from .textfield import MDTextField, MDTextFieldRect, MDTextFieldRound diff --git a/sbapp/kivymd/uix/textfield/textfield.kv b/sbapp/kivymd/uix/textfield/textfield.kv new file mode 100644 index 0000000..3f73c32 --- /dev/null +++ b/sbapp/kivymd/uix/textfield/textfield.kv @@ -0,0 +1,240 @@ +#:import images_path kivymd.images_path + + + + canvas.before: + Clear + + # "round" mode. + Color: + rgba: self._fill_color if self.mode == "round" else (0, 0, 0, 0) + 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_focus if not self.error else self.error_color) \ + if self.focus \ + else self.theme_cls.disabled_hint_text_color \ + ) \ + if self.mode == "round" else \ + (0, 0, 0, 0) + 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 + + # "fill" mode. + Color: + rgba: self._fill_color if self.mode == "fill" else (0, 0, 0, 0) + RoundedRectangle: + pos: self.x, self.y + size: self.width, self.height + radius: self.radius + + # Static underline texture. + Color: + rgba: + (self._line_color_normal \ + if self.line_color_normal else self.theme_cls.divider_color) \ + if self.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 underline (on focus) texture. + Color: + rgba: + self._line_color_focus \ + if self.mode in ("line", "fill") and self.active_line \ + else (0, 0, 0, 0) + Rectangle: + size: self._underline_width, dp(1) + pos: + self.center_x - (self._underline_width / 2), \ + self.y + (dp(16) if self.mode != "fill" else 0) + + # Helper text texture. + Color: + rgba: + self._helper_text_color + Rectangle: + texture: self._helper_text_label.texture + size: self._helper_text_label.texture_size + pos: + self.x + (dp(16) if self.mode == "fill" else 0), \ + self.y + (dp(-18) if self.mode in ("fill", "rectangle", "round") else dp(-2)) + + # Right/left icon texture. + Color: + rgba: self._icon_right_color if self.icon_right else self._icon_left_color + Rectangle: + texture: + self._icon_right_label.texture if self.icon_right else self._icon_left_label.texture + size: + (0, 0) if (not self.icon_right and not self.icon_left) else \ + (self._icon_right_label.texture_size if self.icon_right else self._icon_left_label.texture_size) + pos: + ( \ + (self.width + self.x) - (self._icon_right_label.texture_size[1]) - dp(8), \ + self.center[1] - self._icon_right_label.texture_size[1] / 2 + ((dp(8) if self.mode != "round" else 0) if self.mode != "fill" else 0) \ + if self.mode != "rectangle" else \ + self.center[1] - self._icon_right_label.texture_size[1] / 2 - dp(4) \ + ) \ + if self.icon_right else \ + ( \ + self.x + (dp(0) if self.mode != "fill" else (dp(4) if not self.icon_left else dp(16))), \ + self.center[1] - self._icon_left_label.texture_size[1] / 2 + (((dp(4) if self.mode != "round" else 0) if self.mode not in ("rectangle", "fill") else dp(8)) if self.mode != "fill" else 0) \ + if self.mode != "rectangle" else \ + self.center[1] - self._icon_left_label.texture_size[1] / 2 - dp(4) \ + ) + + # Max length texture. + Color: + rgba: self._max_length_text_color + Rectangle: + texture: self._max_length_label.texture + size: self._max_length_label.texture_size + pos: + self.x + self.width - self._max_length_label.texture_size[0] - (dp(12) if self.mode != "round" else dp(0)), \ + self.y - (dp(2) if self.mode == "line" else dp(18)) + + # Cursor blink. + Color: + rgba: + (self.text_color_focus if self.focus else self._text_color_normal) \ + 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 texture. + Color: + rgba: + self._hint_text_color + Rectangle: + texture: self._hint_text_label.texture + size: self._hint_text_label.texture_size + pos: + ( \ + self.x + ((dp(16) if not self.icon_left else dp(52)) \ + if self.mode == "fill" else (0 if not self.icon_left else \ + dp(36))) if not self.focus and not self.text else \ + self.x + ((dp(16) if self.mode != "round" else dp(36)) if self.mode in ("fill", "rectangle", "round") and \ + self.icon_left else 0) + self._hint_x + ), \ + self.y + self.height + (((dp(4) if self.mode != "round" else dp(10)) \ + if self.mode != "line" else \ + dp(-6)) if self.mode != "rectangle" else dp(-4)) - self._hint_y + + # "rectangle" mode + Color: + rgba: + (self.line_color_focus if not self.error else self.error_color) \ + if self.focus else \ + self.line_color_normal + Line: + width: dp(1) if self.mode == "rectangle" else dp(0.00001) + points: + ( + self.x + self._line_blank_space_right_point, self.top - self._hint_text_label.texture_size[1] // 2, + self.right + dp(12), self.top - self._hint_text_label.texture_size[1] // 2, + self.right + dp(12), self.y, + self.x - dp(12), self.y, + self.x - dp(12), self.top - self._hint_text_label.texture_size[1] // 2, + self.x + self._line_blank_space_left_point, self.top - self._hint_text_label.texture_size[1] // 2 + ) + + # Text color. + Color: + rgba: + self.disabled_foreground_color if self.disabled else \ + ( \ + self.text_color_focus if self.focus else self._text_color_normal + ) \ + if not self.error else self.error_color + + font_name: "Roboto" if not self.font_name else self.font_name + foreground_color: self.theme_cls.text_color + bold: False + padding: + (0 if not self.icon_left else "36dp") if self.mode != "fill" else ("16dp" if not self.icon_left else "52dp"), \ + "24dp" if self.mode != "round" else "8dp", \ + 0 if self.mode != "fill" and not self.icon_right else ("14dp" if not self.icon_right else self._icon_right_label.texture_size[1] + dp(20)), \ + "8dp" if self.mode == "fill" else (("22dp" if self.mode != "round" else "8dp") if self.icon_left and self.mode != "rectangle" else ("16dp" if self.mode in ("fill", "rectangle") else "20dp" if self.mode != "round" else "8dp")) + multiline: False + size_hint_y: None + height: self.minimum_height + + + + size_hint_x: None + width: self.texture_size[0] + shorten: True + shorten_from: "right" + + + + on_focus: + self.anim_rect((self.x, self.y, self.right, self.y, self.right, \ + self.top, self.x, self.top, self.x, self.y), 1) if self.focus \ + else self.anim_rect((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)), 0) + + canvas.after: + Color: + group: "color" + rgba: self._primary_color + Line: + group: "rectangle" + 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) + ) diff --git a/sbapp/kivymd/uix/textfield/textfield.py b/sbapp/kivymd/uix/textfield/textfield.py new file mode 100755 index 0000000..727498b --- /dev/null +++ b/sbapp/kivymd/uix/textfield/textfield.py @@ -0,0 +1,1512 @@ +""" +Components/TextField +==================== + +.. 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 + +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" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-mode.gif + :align: center + +Round mode +--------- + +.. code-block:: kv + + MDTextField: + hint_text: "Round mode" + mode: "round" + max_text_length: 15 + helper_text: "Massage" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-round-mode.png + :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. + +Clickable icon for MDTextField +------------------------------ + +.. 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 + + MDTextField: + id: text_field + hint_text: root.hint_text + text: root.text + password: True + icon_left: "key-variant" + + MDIconButton: + icon: "eye-off" + pos_hint: {"center_y": .5} + pos: text_field.width - self.width + dp(8), 0 + theme_text_color: "Hint" + 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() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-clickable_right-icon.gif + :align: center + +.. seealso:: + + See more information in the :class:`~MDTextFieldRect` class. +""" + +__all__ = ("MDTextField", "MDTextFieldRect", "MDTextFieldRound") + +import os +import re +from typing import Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.logger import Logger +from kivy.metrics import dp, sp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + ColorProperty, + DictProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.label import Label +from kivy.uix.textinput import TextInput + +from kivymd import uix_path +from kivymd.font_definitions import theme_font_styles +from kivymd.theming import ThemableBehavior +from kivymd.uix.label import MDIcon + +with open( + os.path.join(uix_path, "textfield", "textfield.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDTextFieldRect(ThemableBehavior, TextInput): + line_anim = BooleanProperty(True) + """ + If True, then text field shows animated line when on focus. + + :attr:`line_anim` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + def get_rect_instruction(self): + canvas_instructions = self.canvas.after.get_group("rectangle") + return canvas_instructions[0] + + _rectangle = AliasProperty(get_rect_instruction, cache=True) + """ + It is the :class:`~kivy.graphics.vertex_instructions.Line` + instruction reference of the field rectangle. + + :attr:`_rectangle` is an :class:`~kivy.properties.AliasProperty`. + """ + + def get_color_instruction(self): + canvas_instructions = self.canvas.after.get_group("color") + return canvas_instructions[0] + + _rectangle_color = AliasProperty(get_color_instruction, cache=True) + """ + It is the :class:`~kivy.graphics.context_instructions.Color` + instruction reference of the field rectangle. + + :attr:`_rectangle_color` is an :class:`~kivy.properties.AliasProperty`. + """ + + _primary_color = ColorProperty((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): + 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 if self.line_anim else 0), t="out_cubic" + ).start(self._rectangle) + Animation(a=alpha, d=(d_color if self.line_anim else 0)).start( + self._rectangle_color + ) + + def _update_primary_color(self, *args): + self._primary_color = self.theme_cls.primary_color + self._primary_color[3] = 0 + + +class TextfieldLabel(ThemableBehavior, Label): + """Base texture for :class:`~MDTextField` class.""" + + 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() + """ + Text for ``helper_text`` mode. + + :attr:`helper_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + helper_text_mode = OptionProperty( + "on_focus", options=["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"], deprecated=True + ) + """ + Color text mode. Available options are: `'primary'`, `'accent'`, + `'custom'`. + + .. deprecated:: 1.0.0 + Don't use this attribute. + + :attr:`color_mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'primary'`. + """ + + mode = OptionProperty( + "line", options=["rectangle", "round", "fill", "line"] + ) + """ + Text field mode. + Available options are: `'line'`, `'rectangle'`, `'fill'`, `'round'`. + + :attr:`mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'line'`. + """ + + line_color_normal = ColorProperty([0, 0, 0, 0]) + """ + Line color normal (static underline line) in ``rgba`` format. + + .. code-block:: kv + + MDTextField: + hint_text: "line_color_normal" + line_color_normal: 1, 0, 1, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-line-color-normal.gif + :align: center + + :attr:`line_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + line_color_focus = ColorProperty([0, 0, 0, 0]) + """ + Line color focus (active underline line) in ``rgba`` format. + + .. code-block:: kv + + MDTextField: + hint_text: "line_color_focus" + line_color_focus: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-line-color-focus.gif + :align: center + + :attr:`line_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + line_anim = BooleanProperty(True) + """ + If True, then text field shows animated underline when on focus. + + :attr:`line_anim` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + error_color = ColorProperty([0, 0, 0, 0]) + """ + Error color in ``rgba`` format for ``required = True``. + + :attr:`error_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + fill_color = ColorProperty([0, 0, 0, 0], deprecated=True) + """ + The background color of the fill in rgba format when the ``mode`` parameter + is "fill". + + .. deprecated:: 1.0.0 + Use :attr:`fill_color_normal` and :attr:`fill_color_focus` instead. + + :attr:`fill_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + fill_color_normal = ColorProperty([0, 0, 0, 0]) + """ + Fill background color in 'fill' mode when text field is out of focus. + + :attr:`fill_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + fill_color_focus = ColorProperty([0, 0, 0, 0]) + """ + Fill background color in 'fill' mode when the text field has focus. + + :attr:`fill_color_focus` is an :class:`~kivy.properties.ColorProperty` + 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 = ColorProperty([0, 0, 0, 0], deprecated=True) + """ + Hint text color. + + .. deprecated:: 1.0.0 + Use :attr:`hint_text_color_normal` and :attr:`hint_text_color_focus` instead. + + :attr:`current_hint_text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + hint_text_color_normal = ColorProperty([0, 0, 0, 0]) + """ + Hint text color when text field is out of focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + hint_text: "hint_text_color_normal" + hint_text_color_normal: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-hint-text-color-normal.gif + :align: center + + :attr:`hint_text_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + hint_text_color_focus = ColorProperty([0, 0, 0, 0]) + """ + Hint text color when the text field has focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + hint_text: "hint_text_color_focus" + hint_text_color_focus: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-hint-text-color-focus.gif + :align: center + + :attr:`hint_text_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + helper_text_color_normal = ColorProperty([0, 0, 0, 0]) + """ + Helper text color when text field is out of focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + helper_text: "helper_text_color_normal" + helper_text_mode: "persistent" + helper_text_color_normal: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-text-color-normal.png + :align: center + + :attr:`helper_text_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + helper_text_color_focus = ColorProperty([0, 0, 0, 0]) + """ + Helper text color when the text field has focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + helper_text: "helper_text_color_focus" + helper_text_mode: "persistent" + helper_text_color_focus: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-helper-text-color-focus.gif + :align: center + + :attr:`helper_text_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + icon_right_color_normal = ColorProperty([0, 0, 0, 0]) + """ + Color of right icon when text field is out of focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + icon_right: "language-python" + hint_text: "icon_right_color_normal" + icon_right_color_normal: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-icon-right-color-normal.gif + :align: center + + :attr:`icon_right_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + icon_right_color_focus = ColorProperty([0, 0, 0, 0]) + """ + Color of right icon when the text field has focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + icon_right: "language-python" + hint_text: "icon_right_color_focus" + icon_right_color_focus: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-icon-right-color-focus.gif + :align: center + + :attr:`icon_right_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + icon_left_color_normal = ColorProperty([0, 0, 0, 0]) + """ + Color of right icon when text field is out of focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + icon_right: "language-python" + hint_text: "icon_right_color_normal" + icon_left_color_normal: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-icon-right-color-normal.gif + :align: center + + :attr:`icon_left_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + icon_left_color_focus = ColorProperty([0, 0, 0, 0]) + """ + Color of right icon when the text field has focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + icon_right: "language-python" + hint_text: "icon_right_color_focus" + icon_right_color_focus: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-icon-right-color-focus.gif + :align: center + + :attr:`icon_left_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + max_length_text_color = ColorProperty([0, 0, 0, 0]) + """ + Text color of the maximum length of characters to be input. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + hint_text: "max_length_text_color" + max_length_text_color: 0, 1, 0, 1 + max_text_length: 5 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-max-length-text-color.gif + :align: center + + :attr:`max_length_text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + icon_right = StringProperty() + """ + Right icon texture. + + .. note:: It's just a texture. It has no press/touch events. + + :attr:`icon_right` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon_left = StringProperty() + """ + Left icon texture. + + .. versionadded:: 1.0.0 + + .. note:: It's just a texture. It has no press/touch events. + Also note that you cannot use the left and right icons at the same time yet. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-left-icon.png + :align: center + + :attr:`icon_left` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + icon_right_color = ColorProperty([0, 0, 0, 1], deprecated=True) + """ + Color of right icon in ``rgba`` format. + + .. deprecated:: 1.0.0 + Don't use this attribute. + + :attr:`icon_right_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 1]`. + """ + + text_color = ColorProperty([0, 0, 0, 0], deprecated=True) + """ + Text color in ``rgba`` format. + + .. deprecated:: 1.0.0 + Use :attr:`text_color_normal` and :attr:`text_color_focus` instead. + + :attr:`text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + text_color_normal = ColorProperty([0, 0, 0, 0]) + """ + Text color in ``rgba`` format when text field is out of focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + hint_text: "text_color_normal" + text_color_normal: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-color-normal.gif + :align: center + + :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + text_color_focus = ColorProperty([0, 0, 0, 0]) + """ + Text color in ``rgba`` format when text field has focus. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTextField: + hint_text: "text_color_focus" + text_color_focus: 0, 1, 0, 1 + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-color-focus.gif + :align: center + + :attr:`text_color_focus` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + font_size = NumericProperty("16sp") + """ + Font size of the text in pixels. + + :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and + defaults to `'16sp'`. + """ + + # TODO: Add minimum allowed height. Otherwise, if the value is, + # for example, 20, the text field will simply be lessened. + max_height = NumericProperty(0) + """ + Maximum height of the text box when `multiline = True`. + + .. code-block:: kv + + MDTextField: + size_hint_x: .5 + hint_text: "multiline=True" + max_height: "200dp" + mode: "fill" + fill_color: 0, 0, 0, .4 + multiline: True + pos_hint: {"center_x": .5, "center_y": .5} + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-fill-mode-multiline-max-height.gif + :align: center + + :attr:`max_height` is a :class:`~kivy.properties.NumericProperty` and + defaults to `0`. + """ + + radius = ListProperty([10, 10, 0, 0]) + """ + The corner radius for a text field in `fill` mode. + + :attr:`radius` is a :class:`~kivy.properties.ListProperty` and + defaults to `[10, 10, 0, 0]`. + """ + + font_name_helper_text = StringProperty("Roboto") + """ + Font name for helper text. + + :attr:`font_name_helper_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Roboto'`. + """ + + font_name_hint_text = StringProperty("Roboto") + """ + Font name for hint text. + + :attr:`font_name_hint_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Roboto'`. + """ + + font_name_max_length = StringProperty("Roboto") + """ + Font name for max text length. + + :attr:`font_name_max_length` is an :class:`~kivy.properties.StringProperty` + and defaults to `'Roboto'`. + """ + + # The x-axis position of the hint text in the text field. + _hint_x = NumericProperty(0) + # The y-axis position of the hint text in the text field. + _hint_y = NumericProperty("38dp") + # Width of underline that animates when the focus of the text field. + _underline_width = NumericProperty(0) + # Font size for hint text. + _hint_text_font_size = NumericProperty(sp(16)) + + # Label object for `helper_text` parameter. + _helper_text_label = None + # Label object for `max_text_length` parameter. + _max_length_label = None + # Label object for `hint_text` parameter. + _hint_text_label = None + # `MDIcon` object for the icon on the right. + _icon_right_label = None + # `MDIcon` object for the icon on the left. + _icon_left_label = None + + # The left and right coordinates of the text field in 'rectangle' mode. + # + # ┍──blank_space_left blank_space_right──────────────┑ + # | | + # | | + # | | + # ┕──────────────────────────────────────────────────────┙ + _line_blank_space_right_point = NumericProperty(0) + _line_blank_space_left_point = NumericProperty(0) + + # The values of colors that are used in the KV file to display the color + # of the corresponding texture. + _fill_color = ColorProperty([0, 0, 0, 0]) + _text_color_normal = ColorProperty([0, 0, 0, 0]) + _hint_text_color = ColorProperty([0, 0, 0, 0]) + _helper_text_color = ColorProperty([0, 0, 0, 0]) + _max_length_text_color = ColorProperty([0, 0, 0, 0]) + _icon_right_color = ColorProperty([0, 0, 0, 0]) + _icon_left_color = ColorProperty([0, 0, 0, 0]) + _line_color_normal = ColorProperty([0, 0, 0, 0]) + _line_color_focus = ColorProperty([0, 0, 0, 0]) + + # Text to restore the text of the tale after clearing the text field. + __hint_text = StringProperty() + # List of color attribute names that should be updated when changing the + # application color palette. + _colors_to_updated = ListProperty() + + def __init__(self, **kwargs): + self.set_objects_labels() + Clock.schedule_once(self._set_attr_names_to_updated) + Clock.schedule_once(self.set_colors_to_updated) + Clock.schedule_once(self.set_default_colors) + super().__init__(**kwargs) + self.bind( + _hint_text_font_size=self._hint_text_label.setter("font_size"), + _icon_right_color=self._icon_right_label.setter("text_color"), + _icon_left_color=self._icon_left_label.setter("text_color"), + text=self.set_text, + ) + self.theme_cls.bind( + primary_color=lambda x, y: self.set_default_colors(0, True), + theme_style=lambda x, y: self.set_default_colors(0, True), + ) + Clock.schedule_once(self.check_text) + + # TODO: Is this method necessary? + # During testing, a quick double-click on the text box does not stop + # the animation of the hint text height. + def cancel_all_animations_on_double_click(self) -> None: + """ + Cancels the animations of the text field when double-clicking on the + text field. + """ + + if ( + self._hint_y == dp(38) + and not self.text + or self._hint_y == dp(14) + and self.text + ): + Animation.cancel_all( + self, + "_underline_width", + "_hint_y", + "_hint_x", + "_hint_text_font_size", + ) + + def set_colors_to_updated(self, interval: Union[float, int]) -> None: + for attr_name in self._attr_names_to_updated.keys(): + if getattr(self, attr_name) == [0, 0, 0, 0]: + self._colors_to_updated.append(attr_name) + + def set_default_colors( + self, interval: Union[float, int], updated: bool = False + ) -> None: + """ + Sets the default text field colors when initializing a text field + object. Also called when the application palette changes. + + :param updated: If `True` - the color theme of the application has + been changed. Updating the meanings of the colors. + """ + + self._set_attr_names_to_updated(0) + for attr_name in self._attr_names_to_updated.keys(): + self._set_color( + attr_name, self._attr_names_to_updated[attr_name], updated + ) + + if self.error_color == [0, 0, 0, 0] or updated: + self.error_color = self.theme_cls.error_color + if self.max_length_text_color == [0, 0, 0, 0] or updated: + self.max_length_text_color = self.theme_cls.disabled_hint_text_color + + self._hint_text_color = self.hint_text_color_normal + self._text_color_normal = self.text_color_normal + self._fill_color = self.fill_color_normal + self._icon_right_color = self.icon_right_color_normal + self._icon_left_color = self.icon_left_color_normal + self._max_length_text_color = [0, 0, 0, 0] + + if self.helper_text_mode in ("on_focus", "on_error"): + self._helper_text_color = [0, 0, 0, 0] + elif self.helper_text_mode == "persistent": + self._helper_text_color = self.helper_text_color_normal + + self._line_color_normal = self.line_color_normal + self._line_color_focus = self.line_color_focus + + def set_notch_rectangle(self, joining: bool = False) -> None: + """ + Animates a notch for the hint text in the rectangle of the text field + of type `rectangle`. + """ + + def on_progress(*args): + self._line_blank_space_right_point = ( + self._hint_text_label.width + dp(5) if not joining else 0 + ) + + if self.hint_text: + animation = Animation( + _line_blank_space_left_point=self._hint_text_label.x - dp(5) + if not joining + else 0, + duration=0.2, + t="out_quad", + ) + animation.bind(on_progress=on_progress) + animation.start(self) + + def set_active_underline_width(self, width: Union[float, int]) -> None: + """Animates the width of the active underline line.""" + + Animation( + _underline_width=width, + duration=(0.2 if self.line_anim else 0), + t="out_quad", + ).start(self) + + def set_static_underline_color(self, color: list) -> None: + """Animates the color of a static underline line.""" + + Animation( + _line_color_normal=color, + duration=(0.2 if self.line_anim else 0), + t="out_quad", + ).start(self) + + def set_active_underline_color(self, color: list) -> None: + """Animates the fill color for 'fill' mode.""" + + Animation(_line_color_focus=color, duration=0.2, t="out_quad").start( + self + ) + + def set_fill_color(self, color: list) -> None: + """Animates the color of the hint text.""" + + Animation(_fill_color=color, duration=0.2, t="out_quad").start(self) + + def set_helper_text_color(self, color: list) -> None: + """Animates the color of the hint text.""" + + Animation(_helper_text_color=color, duration=0.2, t="out_quad").start( + self + ) + + def set_max_length_text_color(self, color: list) -> None: + """Animates the color of the max length text.""" + + Animation( + _max_length_text_color=color, duration=0.2, t="out_quad" + ).start(self) + + def set_icon_right_color(self, color: list) -> None: + """Animates the color of the icon right.""" + + Animation(_icon_right_color=color, duration=0.2, t="out_quad").start( + self + ) + + def set_icon_left_color(self, color: list) -> None: + """Animates the color of the icon left.""" + + Animation(_icon_left_color=color, duration=0.2, t="out_quad").start( + self + ) + + def set_hint_text_color(self, focus: bool, error: bool = False) -> None: + """Animates the color of the hint text.""" + + if self.mode != "round": + Animation( + _hint_text_color=( + self.hint_text_color_normal + if not focus + else self.hint_text_color_focus + ) + if not error + else self.error_color, + duration=0.2, + t="out_quad", + ).start(self) + + def set_pos_hint_text(self, y: float, x: float = 0) -> None: + """Animates the x-axis width and y-axis height of the hint text.""" + + if self.mode != "round": + Animation(_hint_y=y, duration=0.2, t="out_quad").start(self) + if self.mode == "rectangle": + if not self.icon_left: + _hint_x = x + else: + if y == dp(10): + _hint_x = dp(-16) + else: + _hint_x = dp(20) + + Animation( + _hint_x=_hint_x, + duration=0.2, + t="out_quad", + ).start(self) + elif self.mode == "fill": + Animation( + _hint_x=dp(16) if not self.icon_left else dp(36), + duration=0.2, + t="out_quad", + ).start(self) + elif self.mode == "line": + Animation( + _hint_x=dp(0) if not self.icon_left else dp(36), + duration=0.2, + t="out_quad", + ).start(self) + + def set_hint_text_font_size(self, font_size: float) -> None: + """Animates the font size of the hint text.""" + + if self.mode != "round": + Animation( + _hint_text_font_size=font_size, duration=0.2, t="out_quad" + ).start(self) + + def set_max_text_length(self) -> None: + """Called when text is entered into a text field.""" + + if self.max_text_length: + self._max_length_label.text = ( + f"{len(self.text)}/{self.max_text_length}" + ) + + def check_text(self, interval: Union[float, int]) -> None: + self.set_text(self, self.text) + + def set_text(self, instance_text_field, text: str) -> None: + """Called when text is entered into a text field.""" + + self.text = re.sub("\n", " ", text) if not self.multiline else text + self.set_max_text_length() + + if self.text and self.max_length_text_color and self._get_has_error(): + self.error = True + if ( + self.text + and self.max_length_text_color + and not self._get_has_error() + ): + self.error = False + + # Start the appropriate texture animations when programmatically + # pasting text into a text field. + if len(self.text) != 0 and not self.focus: + self.set_pos_hint_text( + (dp(28) if self.mode != "line" else dp(18)) + if self.mode != "rectangle" + else dp(10) + ) + + self.set_hint_text_font_size(sp(12)) + if self.mode == "rectangle": + self.set_notch_rectangle() + + if not self.text and not self.focus: + self.on_focus(instance_text_field, False) + + if self.mode == "round" and self.text: + self.hint_text = "" + if self.mode == "round" and not self.text: + self.hint_text = self.__hint_text + + def set_x_pos(self): + pass + + def set_objects_labels(self) -> None: + """ + Creates labels objects for the parameters`helper_text`,`hint_text`, + etc. + """ + + self._helper_text_label = TextfieldLabel( + font_style="Caption", + halign="left", + valign="middle", + field=self, + font_name=self.font_name_helper_text, + ) + self._max_length_label = TextfieldLabel( + font_style="Caption", + halign="right", + valign="middle", + text="", + field=self, + ) + self._hint_text_label = TextfieldLabel( + font_style="Subtitle1", halign="left", valign="middle", field=self + ) + self._icon_right_label = MDIcon(theme_text_color="Custom") + self._icon_left_label = MDIcon(theme_text_color="Custom") + + def on_helper_text(self, instance_text_field, helper_text: str) -> None: + self._helper_text_label.text = helper_text + + def on_focus(self, instance_text_field, focus: bool) -> None: + # TODO: See `cancel_all_animations_on_double_click` method. + # self.cancel_all_animations_on_double_click() + + if focus: + if self.mode == "rectangle": + self.set_notch_rectangle() + self.set_static_underline_color([0, 0, 0, 0]) + if ( + self.helper_text_mode in ("on_focus", "persistent") + and self.helper_text + ): + self.set_helper_text_color(self.helper_text_color_focus) + if self.mode == "fill": + self.set_fill_color(self.fill_color_focus) + self.set_active_underline_width(self.width) + + self.set_pos_hint_text( + (dp(28) if self.mode != "line" else dp(18)) + if self.mode != "rectangle" + else dp(10) + ) + self.set_hint_text_color(focus) + self.set_hint_text_font_size(sp(12)) + + if self.max_text_length: + self.set_max_length_text_color(self.max_length_text_color) + if self.icon_right: + self.set_icon_right_color(self.icon_right_color_focus) + if self.icon_left: + self.set_icon_left_color(self.icon_left_color_focus) + + if self.error: + if self.hint_text: + self.set_hint_text_color(focus, self.error) + if self.helper_text: + self.set_helper_text_color(self.error_color) + if self.max_text_length: + self.set_max_length_text_color(self.error_color) + if self.icon_right: + self.set_icon_right_color(self.error_color) + if self.icon_left: + self.set_icon_left_color(self.error_color) + else: + if self.helper_text_mode == "persistent" and self.helper_text: + self.set_helper_text_color(self.helper_text_color_normal) + if self.mode == "rectangle" and not self.text: + self.set_notch_rectangle(joining=True) + if not self.text: + if self.mode == "rectangle": + y = dp(38) + elif self.mode == "fill": + y = dp(46) + else: + y = dp(34) + + self.set_pos_hint_text(y) + self.set_hint_text_font_size(sp(16)) + if self.icon_right: + self.set_icon_right_color(self.icon_right_color_normal) + if self.icon_left: + self.set_icon_left_color(self.icon_left_color_normal) + if self.hint_text: + self.set_hint_text_color(focus, self.error) + + self.set_active_underline_width(0) + self.set_max_length_text_color([0, 0, 0, 0]) + + if self.mode == "fill": + self.set_fill_color(self.fill_color_normal) + + self.error = self._get_has_error() or self.error + if self.error: + self.set_static_underline_color(self.error_color) + else: + self.set_static_underline_color(self.line_color_normal) + + def on_icon_left(self, instance_text_field, icon_name: str) -> None: + self._icon_left_label.icon = icon_name + + def on_icon_right(self, instance_text_field, icon_name: str) -> None: + self._icon_right_label.icon = icon_name + + def on_disabled(self, instance_text_field, disabled_value: bool) -> None: + pass + + def on_error(self, instance_text_field, error: bool) -> None: + """ + Changes the primary colors of the text box to match the `error` value + (text field is in an error state or not). + """ + + if error: + self.set_max_length_text_color(self.error_color) + self.set_active_underline_color(self.error_color) + if self.hint_text: + self.set_hint_text_color(self.focus, self.error) + if self.helper_text: + self.set_helper_text_color(self.error_color) + if self.icon_right: + self.set_icon_right_color(self.error_color) + if self.icon_left: + self.set_icon_left_color(self.error_color) + if self.helper_text_mode == "on_error": + self.set_helper_text_color(self.error_color) + else: + self.set_max_length_text_color(self.max_length_text_color) + self.set_active_underline_color(self.line_color_focus) + if self.hint_text: + self.set_hint_text_color(self.focus) + if self.helper_text: + self.set_helper_text_color(self.helper_text_color_focus) + if self.icon_right: + self.set_icon_right_color(self.icon_right_color_focus) + if self.icon_left: + self.set_icon_left_color(self.icon_left_color_focus) + if self.helper_text_mode in ("on_focus", "on_error"): + self.set_helper_text_color([0, 0, 0, 0]) + elif self.helper_text_mode == "persistent": + self.set_helper_text_color(self.helper_text_color_normal) + + def on_hint_text(self, instance_text_field, hint_text: str) -> None: + if hint_text: + self.__hint_text = hint_text + self._hint_text_label.text = hint_text + self._hint_text_label.font_size = sp(16) + + def on_width(self, instance_text_field, width: float) -> None: + """Called when the application window is resized.""" + + if self.focus: + self._underline_width = self.width + + def on_height(self, instance_text_field, value_height: float) -> None: + if value_height >= self.max_height and self.max_height: + self.height = self.max_height + + def on_text_color_normal(self, instance_text_field, color: list): + self._text_color_normal = color + + def on_hint_text_color_normal(self, instance_text_field, color: list): + self._hint_text_color = color + + def on_helper_text_color_normal(self, instance_text_field, color: list): + self._helper_text_color = color + + def on_icon_right_color_normal(self, instance_text_field, color: list): + self._icon_right_color = color + + def on_line_color_normal(self, instance_text_field, color: list): + self._line_color_normal = color + + def on_max_length_text_color(self, instance_text_field, color: list): + self._max_length_text_color = color + + def _set_color(self, attr_name: str, color: str, updated: bool) -> None: + if attr_name in self._colors_to_updated or updated: + if attr_name in self._colors_to_updated: + setattr(self, attr_name, color) + + def _set_attr_names_to_updated(self, interval: Union[float, int]) -> None: + """ + Sets and update the default color dictionary for text field textures. + """ + + self._attr_names_to_updated = { + "line_color_normal": self.theme_cls.disabled_hint_text_color, + "line_color_focus": self.theme_cls.primary_color, + "hint_text_color_normal": self.theme_cls.disabled_hint_text_color, + "hint_text_color_focus": self.theme_cls.primary_color, + "helper_text_color_normal": self.theme_cls.disabled_hint_text_color, + "helper_text_color_focus": self.theme_cls.disabled_hint_text_color, + "text_color_normal": self.theme_cls.disabled_hint_text_color, + "text_color_focus": self.theme_cls.primary_color, + "fill_color_normal": self.theme_cls.bg_darkest, + "fill_color_focus": self.theme_cls.bg_dark, + "icon_right_color_normal": self.theme_cls.disabled_hint_text_color, + "icon_right_color_focus": self.theme_cls.primary_color, + "icon_left_color_normal": self.theme_cls.disabled_hint_text_color, + "icon_left_color_focus": self.theme_cls.primary_color, + } + + def _get_has_error(self) -> bool: + """ + Returns `False` or `True` depending on the state of the text field, + for example when the allowed character limit has been exceeded or when + the :attr:`~MDTextField.required` parameter is set to `True`. + """ + + if self.max_text_length and len(self.text) > self.max_text_length: + has_error = True + else: + if all((self.required, len(self.text) == 0)): + has_error = True + else: + has_error = False + return has_error + + def _refresh_hint_text(self): + """Method override to avoid duplicate hint text texture.""" + + +class MDTextFieldRound(MDTextField): + """ + .. deprecated:: 1.0.0 + Use :class:`~MDTextField` class instead. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Logger.warning( + "KivyMD: " + "The `MDTextFieldRound` class has been deprecated. " + "Use the `MDTextField` class instead with `mode='round'`" + ) + + +if __name__ == "__main__": + from kivy.lang import Builder + from kivy.uix.textinput import TextInput + + from kivymd.app import MDApp + + KV = """ +MDScreen: + + MDBoxLayout: + id: box + orientation: "vertical" + spacing: "20dp" + adaptive_height: True + size_hint_x: .8 + pos_hint: {"center_x": .5, "center_y": .5} + + MDTextField: + hint_text: "Label" + helper_text: "Error massage" + mode: "rectangle" + max_text_length: 5 + + MDTextField: + icon_left: "git" + hint_text: "Label" + helper_text: "Error massage" + mode: "rectangle" + + MDTextField: + icon_left: "git" + hint_text: "Label" + helper_text: "Error massage" + mode: "fill" + + MDTextField: + hint_text: "Label" + helper_text: "Error massage" + mode: "fill" + + MDTextField: + hint_text: "Label" + helper_text: "Error massage" + + MDTextField: + icon_left: "git" + hint_text: "Label" + helper_text: "Error massage" + + MDTextField: + hint_text: "Round mode" + mode: "round" + max_text_length: 15 + helper_text: "Massage" + + MDFlatButton: + text: "SET TEXT" + pos_hint: {"center_x": .5} + on_release: app.set_text() +""" + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def set_text(self): + for widget in self.root.ids.box.children: + if issubclass(widget.__class__, TextInput): + widget.text = "Input text" + + Test().run() diff --git a/sbapp/kivymd/uix/toolbar/__init__.py b/sbapp/kivymd/uix/toolbar/__init__.py new file mode 100644 index 0000000..15a8177 --- /dev/null +++ b/sbapp/kivymd/uix/toolbar/__init__.py @@ -0,0 +1,2 @@ +# NOQA F401 +from .toolbar import MDBottomAppBar, MDToolbar, MDTopAppBar diff --git a/sbapp/kivymd/uix/toolbar/toolbar.kv b/sbapp/kivymd/uix/toolbar/toolbar.kv new file mode 100644 index 0000000..2ea68f7 --- /dev/null +++ b/sbapp/kivymd/uix/toolbar/toolbar.kv @@ -0,0 +1,110 @@ + + + IconLeftWidget: + icon: root.icon + + + + size_hint_y: None + padding: [root.theme_cls.horizontal_margins - dp(12), 0] + elevation: root.elevation + + canvas: + Color: + rgba: + ( \ + root.theme_cls.primary_color \ + if root.md_bg_color == [0, 0, 0, 0] \ + else root.md_bg_color \ + ) \ + if root.type == "top" else \ + ( \ + ( \ + root.theme_cls.primary_color \ + if not self.md_bg_bottom_color else \ + self.md_bg_bottom_color \ + ) \ + if root.parent.md_bg_color == [0, 0, 0, 0] \ + else root.parent.md_bg_color \ + ) + Mesh: + vertices: root._vertices_left + indices: root._indices_left + mode: "triangle_fan" + + Mesh: + vertices: root._vertices_right + indices: root._indices_right + mode: "triangle_fan" + + RoundedRectangle: + pos: root._rectangle_left_pos + size: root._rectangle_left_width, root._rounded_rectangle_height + radius: + [0,] if root.mode == "normal" \ + else [0, root.notch_radius * root._rounding_percentage, 0, 0] + + RoundedRectangle: + pos: root._rectangle_right_pos + size: root._rectangle_right_width, root._rounded_rectangle_height + radius: + [0,] if root.mode == "normal" \ + else [root.notch_radius * root._rounding_percentage, 0, 0, 0] + + + + orientation: "vertical" + + MDBoxLayout: + padding: 0, 0, 0, root.height - headline_box.height - (dp(48) + dp(20)) + + MDBoxLayout: + id: left_actions + orientation: "horizontal" + size_hint_x: None + padding: [0, (self.height - dp(48)) / 2] + + MDLabel: + id: label_title + font_style: "H6" + opposite_colors: root.opposite_colors + theme_text_color: "Custom" if not root.opposite_colors else "Primary" + text_color: root.specific_text_color + text: root.title + shorten: True + shorten_from: "right" + markup: True + padding: dp(12), 0 + halign: + root.anchor_title \ + if root.anchor_title else \ + root.update_anchor_title(app.theme_cls.material_style) + + MDBoxLayout: + id: right_actions + orientation: "horizontal" + adaptive_width: True + padding: [0, (self.height - dp(48)) / 2] + + MDBoxLayout: + id: headline_box + size_hint_y: None + height: label_headline.texture_size[1] if label_headline.text else 0 + padding: "16dp" + + MDLabel: + id: label_headline + adaptive_height: True + shorten: True + shorten_from: "right" + theme_text_color: "Custom" + text_color: + label_title.text_color \ + if not root.headline_text_color else \ + root.headline_text_color + text: + root.headline_text \ + if root.type_height in ("medium", "large") \ + and app.theme_cls.material_style == "M3" \ + and root.type != "bottom" else \ + "" diff --git a/sbapp/kivymd/uix/toolbar/toolbar.py b/sbapp/kivymd/uix/toolbar/toolbar.py new file mode 100755 index 0000000..84c5492 --- /dev/null +++ b/sbapp/kivymd/uix/toolbar/toolbar.py @@ -0,0 +1,1410 @@ +""" +Components/Toolbar +================== + +.. seealso:: + + `Material Design spec, App bars: top `_ + + `Material Design spec, App bars: bottom `_ + + `Material Design 3 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 = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "MDTopAppBar" + + 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 + + MDTopAppBar: + title: "MDTopAppBar" + left_action_items: [["menu", lambda x: app.callback()]] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-2.png + :align: center + +.. note:: + + The callback is optional. ``left_action_items: [["menu"]]`` would also work for a button that does nothing. + +Add right menu +-------------- + +.. code-block:: kv + + MDTopAppBar: + title: "MDTopAppBar" + 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 + + MDTopAppBar: + title: "MDTopAppBar" + 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 + + MDTopAppBar: + title: "MDTopAppBar" + 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 + + MDTopAppBar: + title: "MDTopAppBar" + 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 + + MDTopAppBar: + 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 = ''' + MDBoxLayout: + + # Will always be at the bottom of the screen. + MDBottomAppBar: + + MDTopAppBar: + 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: + + MDTopAppBar: + 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: + + MDTopAppBar: + 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: + + MDTopAppBar: + 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 + +Custom color +------------ + +.. code-block:: kv + + MDBottomAppBar: + md_bg_color: 0, 1, 0, 1 + + MDTopAppBar: + title: "Title" + icon: "git" + type: "bottom" + left_action_items: [["menu", lambda x: x]] + icon_color: 0, 1, 0, 1 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-11.png + :align: center + +Tooltips +-------- + +You can add MDTooltips to the Toolbar icons by ading a text string to the toolbar item, as shown below + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.snackbar import Snackbar + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "MDTopAppBar" + left_action_items: [["menu", "This is the navigation"]] + right_action_items: + [["dots-vertical", lambda x: app.callback(x), "this is the More Actions"]] + + MDLabel: + text: "Content" + halign: "center" + ''' + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def callback(self, button): + Snackbar(text="Hello World").open() + + Test().run() + +Material design 3 style +----------------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.toolbar import MDTopAppBar + + KV = ''' + MDScreen: + + MDBoxLayout: + id: box + orientation: "vertical" + spacing: "12dp" + pos_hint: {"top": 1} + adaptive_height: True + ''' + + + class TestNavigationDrawer(MDApp): + def build(self): + self.theme_cls.material_style = "M3" + return Builder.load_string(KV) + + def on_start(self): + for type_height in ["medium", "large", "small"]: + self.root.ids.box.add_widget( + MDTopAppBar( + type_height=type_height, + headline_text=f"Headline {type_height.lower()}", + md_bg_color="#2d2734", + left_action_items=[["arrow-left", lambda x: x]], + right_action_items=[ + ["attachment", lambda x: x], + ["calendar", lambda x: x], + ["dots-vertical", lambda x: x], + ], + title="Title" if type_height == "small" else "" + ) + ) + + + TestNavigationDrawer().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-m3.png + :align: center +""" + +__all__ = ("MDTopAppBar", "MDBottomAppBar", "MDToolbar") + +import os +from math import cos, radians, sin +from typing import Union + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.lang import Builder +from kivy.logger import Logger +from kivy.metrics import dp +from kivy.properties import ( + AliasProperty, + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + ObjectProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.boxlayout import BoxLayout +from kivy.uix.floatlayout import FloatLayout + +from kivymd import uix_path +from kivymd.color_definitions import text_colors +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import ( + FakeRectangularElevationBehavior, + SpecificBackgroundColorBehavior, +) +from kivymd.uix.button import MDFloatingActionButton, MDIconButton +from kivymd.uix.controllers import WindowController +from kivymd.uix.list import OneLineIconListItem +from kivymd.uix.menu import MDDropdownMenu +from kivymd.uix.templates import ScaleWidget +from kivymd.uix.tooltip import MDTooltip +from kivymd.utils.set_bars_colors import set_bars_colors + +with open( + os.path.join(uix_path, "toolbar", "toolbar.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class ActionBottomAppBarButton(MDFloatingActionButton, ScaleWidget): + """ + Implements a floating action button (FAB) for a toolbar with type 'bottom'. + """ + + +class ActionTopAppBarButton(MDIconButton, MDTooltip): + """Implements action buttons on the toolbar.""" + + # The text of the menu item of the corresponding action button that will + # be displayed in the `OverFlowMenu` menu. + overflow_text = StringProperty() + + +class ActionOverFlowButton(ActionTopAppBarButton): + """Implements a toolbar action button for the `OverFlowMenu` menu.""" + + icon = "dots-vertical" + + +class OverFlowMenu(MDDropdownMenu): + """ + Implements a menu for the items (:class:`~OverFlowMenuItem`) of the + corresponding action buttons. + """ + + +class OverFlowMenuItem(OneLineIconListItem): + """Implements a menu (:class:`~OverFlowMenu`) item.""" + + icon = StringProperty() + + +class NotchedBox( + ThemableBehavior, + FakeRectangularElevationBehavior, + SpecificBackgroundColorBehavior, + BoxLayout, +): + elevation = NumericProperty(6) + notch_radius = NumericProperty() + notch_center_x = NumericProperty("100dp") + + _indices_right = ListProperty() + _vertices_right = ListProperty() + _indices_left = ListProperty() + _vertices_left = ListProperty() + _rounded_rectangle_height = NumericProperty("6dp") + _total_angle = NumericProperty(180) + _rectangle_left_pos = ListProperty([0, 0]) + _rectangle_left_width = NumericProperty() + _rectangle_right_pos = ListProperty([0, 0]) + _rectangle_right_width = NumericProperty() + _rounding_percentage = NumericProperty(0.15) + _shift = NumericProperty(dp(4)) + + def __init__(self, **kw): + super().__init__(**kw) + self.bind( + size=self._update_canvas, + pos=self._update_canvas, + notch_radius=self._update_canvas, + notch_center_x=self._update_canvas, + ) + Clock.schedule_once(self._update_canvas) + + def _update_canvas(self, *args): + pos = self.pos + size = [ + self.width, + self.size[1] - self._rounded_rectangle_height / 2, + ] + notch_center_x = self.pos[0] + self.notch_center_x + circle_radius = self.notch_radius + degree_diff = int((180 - self._total_angle) / 2) + circle_center = [notch_center_x, pos[1] + size[1]] + left_circle_pos = self._points_on_circle( + circle_center, circle_radius, 180 + degree_diff, 270 + ) + + self._rectangle_left_pos = [ + pos[0], + pos[1] + size[1] - self._rounded_rectangle_height / 2, + ] + self._rectangle_left_width = left_circle_pos[0][0] - self.pos[0] + + right_circle_pos = self._points_on_circle( + circle_center, circle_radius, -degree_diff, -90 + ) + + self._rectangle_right_pos = [ + right_circle_pos[0][0], + pos[1] + size[1] - self._rounded_rectangle_height / 2, + ] + self._rectangle_right_width = pos[0] + size[0] - right_circle_pos[0][0] + + raw_vertices_left = self._make_vertices( + pos, [notch_center_x - pos[0], size[1]], "left", left_circle_pos + ) + raw_vertices_right = self._make_vertices( + [notch_center_x, pos[1]], + [size[0] + pos[0] - notch_center_x, size[1]], + "right", + right_circle_pos, + ) + + left_vertices, left_indices = self._make_vertices_indices( + raw_vertices_left + ) + right_vertices, right_indices = self._make_vertices_indices( + raw_vertices_right + ) + + self._update_mesh(left_vertices, left_indices, "left") + self._update_mesh(right_vertices, right_indices, "right") + + def _update_mesh(self, vertices, indices, mode): + if mode == "left": + self._indices_left = indices + self._vertices_left = vertices + else: + self._indices_right = indices + self._vertices_right = vertices + return True + + @staticmethod + def _make_vertices_indices(points_list): + vertices = [] + indices = [] + for index, point in enumerate(points_list): + indices.append(index) + vertices.extend([point[0], point[1], 0, 0]) + + return [vertices, indices] + + @staticmethod + def _make_vertices(rectangle_pos, rectangle_size, mode, notch_points=[]): + x = rectangle_pos[0] + y = rectangle_pos[1] + w = rectangle_size[0] + h = rectangle_size[1] + + if mode == "left": + rectangle_vertices = [[x, y], [x, y + h]] + elif mode == "right": + rectangle_vertices = [[x + w, y], [x + w, y + h]] + rectangle_vertices.extend(notch_points) + if mode == "left": + rectangle_vertices.extend([[x + w, y]]) + elif mode == "right": + rectangle_vertices.extend([[x, y]]) + + return rectangle_vertices + + @staticmethod + def _points_on_circle(center, radius, start_angle, end_angle): + points = [] + y_diff = False + if end_angle >= 180: + step = 1 + end_angle += 1 + elif end_angle <= 0: + step = -1 + end_angle -= 1 + else: + raise Exception("Invalid value for start angle") + + for degree in range(start_angle, end_angle, step): + + angle = radians(degree) + x = center[0] + (radius * cos(angle)) + y = center[1] + (radius * sin(angle)) + + if y_diff is False: + y_diff = abs(y - center[1]) + + y += y_diff + points.append([x, y]) + + return points + + +class MDTopAppBar(NotchedBox, WindowController): + """ + :Events: + `on_action_button` + Method for the button used for the :class:`~MDBottomAppBar` class. + """ + + left_action_items = ListProperty() + """ + The icons on the left of the toolbar. + To add one, append a list like the following: + + .. code-block:: kv + + MDTopAppBar: + left_action_items: ["dots-vertical", callback, "tooltip text", "overflow text"] + + ``icon_name`` - is a string that corresponds to an icon definition: + + .. code-block:: kv + + MDTopAppBar: + right_action_items: [["home"]] + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-icon.png + :align: center + + ``callback`` - is the function called on a touch release event and: + + .. code-block:: kv + + MDTopAppBar: + right_action_items: [["home", lambda x: app.callback(x)]] + + .. code-block:: python + + class Test(MDApp): + def callback(self, instance_action_top_appbar_button): + print(instance_action_top_appbar_button) + + ``tooltip text`` - is the text to be displayed in the tooltip: + + .. code-block:: kv + + MDTopAppBar: + right_action_items: + [ + ["home", lambda x: app.callback(x), "Home"], + ["message-star", lambda x: app.callback(x), "Message star"], + ["message-question", lambda x: app.callback(x), "Message question"], + ["message-reply", lambda x: app.callback(x), "Message reply"], + ] + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-tooltip-text.gif + :align: center + + ``overflow text`` - is the text for menu items (:class:`~OverFlowMenuItem`) + of the corresponding action buttons: + + .. code-block:: kv + + MDTopAppBar: + right_action_items: + [ + ["home", lambda x: app.callback(x), "", "Home"], + ["message-star", lambda x: app.callback(x), "", "Message star"], + ["message-question", lambda x: app.callback(x), "" , "Message question"], + ["message-reply", lambda x: app.callback(x), "", "Message reply"], + ] + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-overflow-text.png + :align: center + + Both the ``callback`` and ``tooltip text`` and ``overflow text`` are + optional but the order must be preserved. + + :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. + + .. code-block:: kv + + MDTopAppBar: + title: "MDTopAppBar" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-title.png + :align: center + + :attr:`title` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + 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'`. + + .. rubric:: Mode "end": + + .. code-block:: kv + + MDBottomAppBar: + + MDTopAppBar: + 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 + + .. rubric:: Mode "free-end": + + .. code-block:: kv + + MDBottomAppBar: + + MDTopAppBar: + mode: "free-end" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-10.png + :align: center + + .. rubric:: Mode "free-center": + + .. code-block:: kv + + MDBottomAppBar: + + MDTopAppBar: + mode: "free-center" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-free-center.png + :align: center + + .. rubric:: Mode "center": + + .. code-block:: kv + + MDBottomAppBar: + + MDTopAppBar: + mode: "center" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-center.png + :align: center + + :attr:`mode` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'center'`. + """ + + type = OptionProperty("top", options=["top", "bottom"]) + """ + When using the :class:`~MDBottomAppBar` class, the parameter ``type`` + must be set to `'bottom'`: + + .. code-block:: kv + + MDBottomAppBar: + + MDTopAppBar: + type: "bottom" + + Available options are: `'top'`, `'bottom'`. + + :attr:`type` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'top'`. + """ + + opposite_colors = BooleanProperty(False) + """ + Changes the color of the label to the color opposite to the main theme. + + .. code-block:: kv + + MDTopAppBar: + opposite_colors: True + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-true.png + :align: center + + .. code-block:: kv + + MDTopAppBar: + opposite_colors: True + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-false.png + :align: center + """ + + md_bg_bottom_color = ColorProperty(None) + """ + The background color in (r, g, b, a) format for the toolbar with the + ``bottom`` mode. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDBottomAppBar: + + MDTopAppBar: + md_bg_bottom_color: 0, 1, 0, 1 + icon_color: self.md_bg_bottom_color + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-md-bg-bottom-color.png + :align: center + + :attr:`md_bg_bottom_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + set_bars_color = BooleanProperty(False) + """ + If `True` the background color of the bar status will be set automatically + according to the current color of the toolbar. + + .. versionadded:: 1.0.0 + + See `set_bars_colors ` + for more information. + + :attr:`set_bars_color` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + use_overflow = BooleanProperty(False) + """ + As a top app bar is resized, actions move to the overflow menu from right + to left. + + .. versionadded:: 1.0.0 + + .. code-block:: kv + + MDTopAppBar: + title: "MDTopAppBar" + use_overflow: True + right_action_items: + [ + ["home", lambda x: app.callback(x), "Home", "Home"], + ["message-star", lambda x: app.callback(x), "Message star", "Message star"], + ["message-question", lambda x: app.callback(x), "Message question", "Message question"], + ["message-reply", lambda x: app.callback(x), "Message reply", "Message reply"], + ] + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-use-overflow.gif + :align: center + + :attr:`use_overflow` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + overflow_cls = ObjectProperty() + """ + Must be an object of the :class:`~kivymd.uix.menu.MDDropdownMenu' class. + See :class:`~kivymd.uix.menu.MDDropdownMenu` class documentation for more + information. + + .. versionadded:: 1.0.0 + + .. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + #:import CustomOverFlowMenu __main__.CustomOverFlowMenu + + + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "MDTopAppBar" + use_overflow: True + overflow_cls: CustomOverFlowMenu() + right_action_items: + [ + ["home", lambda x: app.callback(x), "Home", "Home"], + ["message-star", lambda x: app.callback(x), "Message star", "Message star"], + ["message-question", lambda x: app.callback(x), "Message question", "Message question"], + ["message-reply", lambda x: app.callback(x), "Message reply", "Message reply"], + ] + + MDLabel: + text: "Content" + halign: "center" + ''' + + + class CustomOverFlowMenu(MDDropdownMenu): + # In this class you can set custom properties for the overflow menu. + pass + + + class Test(MDApp): + def build(self): + return Builder.load_string(KV) + + def callback(self, instance_action_top_appbar_button): + print(instance_action_top_appbar_button) + + + Test().run() + + :attr:`overflow_cls` is an :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + # Attributes only for the BottomAppBar class. + + icon = StringProperty() + """ + Floating button. Only for :class:`~MDBottomAppBar` class. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `'android'`. + """ + + icon_color = ColorProperty() + """ + Color action button. Only for :class:`~MDBottomAppBar` class. + + :attr:`icon_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[]`. + """ + + # MD3 Style attributes. + + anchor_title = OptionProperty(None, options=["left", "center", "right"]) + """ + Position toolbar title. Only used with `material_style = 'M3'` + Available options are: `'left'`, `'center'`, `'right'`. + + :attr:`anchor_title` is an :class:`~kivy.properties.OptionProperty` + and defaults to `None`. + """ + + headline_text = StringProperty() + """ + Headline text toolbar. + + .. versionadded:: 1.0.0 + + :attr:`headline_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + headline_text_color = ColorProperty(None) + """ + Headline text color. + + .. versionadded:: 1.0.0 + + :attr:`headline_text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + type_height = OptionProperty("small", options=["medium", "large", "small"]) + """ + Toolbar height type. + + .. versionadded:: 1.0.0 + + Available options are: 'small', 'large', 'small'. + + :attr:`type_height` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'small'`. + """ + + round = NumericProperty("10dp", deprecated=True) + """ + Rounding the corners at the notch for a button. + Only for :class:`~MDBottomAppBar` class. + + .. deprecated:: 1.0.0 + Don't use this attribute. + + :attr:`round` is an :class:`~kivy.properties.NumericProperty` + and defaults to `'10dp'`. + """ + + # List of action buttons (ActionTopAppBarButton instance) that have been + # .added to the overflow + _hidden_items = [] + # See `kivymd.uix.menu.MDDropdownMenu.items` attribute. + _overflow_menu_items = [] + + def __init__(self, **kwargs): + self.action_button = ActionBottomAppBarButton() + super().__init__(**kwargs) + self.register_event_type("on_action_button") + + if not self.icon_color: + self.icon_color = self.theme_cls.primary_color + + self.bind(specific_text_color=self.update_action_bar_text_colors) + self.theme_cls.bind(material_style=self.update_bar_height) + self.theme_cls.bind(primary_palette=self.update_md_bg_color) + + 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) + ) + Clock.schedule_once(lambda x: self.set_md_bg_color(0, self.md_bg_color)) + Clock.schedule_once(lambda x: self.on_type_height(0, self.type_height)) + Clock.schedule_once( + lambda x: self.update_anchor_title(self.theme_cls.material_style) + ) + Clock.schedule_once(self.update_floating_radius) + Clock.schedule_once(self.check_overflow_cls) + + def set_headline_font_style(self, interval: Union[int, float]) -> None: + if self.type_height in ("medium", "large"): + self.ids.label_headline.font_style = { + "medium": "H6", + "large": "H5", + }[self.type_height] + + def on_width(self, instance_toolbar, width: float) -> None: + """ + Called when the toolbar is resized (size of the application window). + """ + + 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 + + # The user reduces the width of the window. + if ( + self.get_window_width_resizing_direction() == "left" + and self.use_overflow + and self.ids.label_title.is_shortened + ): + if not self.overflow_action_button_is_added(): + self.add_overflow_button() + self.add_action_button_to_overflow() + # The user increases the width of the window. + if ( + self.get_window_width_resizing_direction() == "right" + and self.use_overflow + and not self.ids.label_title.is_shortened + and self.overflow_cls.items + ): + self.return_action_button_to_toolbar() + + def return_action_button_to_toolbar(self) -> None: + if len(self._hidden_items): + action_button = self._hidden_items.pop() + self.ids.right_actions.add_widget(action_button, index=1) + self.update_overflow_menu_items(action_button) + if not len(self._hidden_items): + self.remove_overflow_button() + + def remove_overflow_button(self) -> None: + """Removes an overflow button to the toolbar.""" + + if self.overflow_action_button_is_added(): + action_overflow_button = self.ids.right_actions.children[0] + self.ids.right_actions.remove_widget(action_overflow_button) + self._overflow_menu_items = [] + + def add_overflow_button(self) -> None: + """Adds an overflow button to the toolbar.""" + + self.ids.right_actions.add_widget( + ActionOverFlowButton( + theme_text_color="Custom" + if not self.opposite_colors + else "Primary", + text_color=self.specific_text_color, + opposite_colors=self.opposite_colors, + on_release=lambda x: self.overflow_cls.open(), + ) + ) + + def overflow_action_button_is_added(self) -> bool: + """ + Returns `True` if at least one action button + (:class:`~ActionTopAppBarButton') on the toolbar is added to the + overflow. + """ + + if ( + not self.ids.right_actions.children[0].__class__ + is ActionOverFlowButton + ): + return False + return True + + def add_action_button_to_overflow(self): + """Adds an overflow button to the toolbar.""" + + if len(self.ids.right_actions.children) > 1: + button_to_be_added = self.ids.right_actions.children[1] + self._hidden_items.append(button_to_be_added) + self.ids.right_actions.remove_widget(button_to_be_added) + + self._overflow_menu_items.append( + { + "viewclass": "OverFlowMenuItem", + "icon": button_to_be_added.icon, + "text": button_to_be_added.overflow_text, + "height": dp(48), + "on_press": lambda *x: button_to_be_added.on_release(*x), + } + ) + self.overflow_cls.items = self._overflow_menu_items + self.overflow_cls.caller = self.ids.right_actions.children[0] + + def check_overflow_cls(self, interval: Union[int, float]) -> None: + """ + If the user does not set the :attr:`overflow_cls` attribute but uses + overflows, the :attr:`overflow_cls` attribute will use the default + value. + """ + + if not self.overflow_cls: + self.overflow_cls = self.get_default_overflow_cls() + + def on_type(self, instance_toolbar, type_value: str) -> None: + """Called when the value of the :attr:`type` attribute changes.""" + + if type_value == "bottom": + self.action_button.bind(center_x=self.setter("notch_center_x")) + 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 + ) + self.on_mode(None, self.mode) + + def on_type_height(self, instance_toolbar, height_type_value: str) -> None: + """ + Called when the value of the :attr:`type_height` attribute changes. + """ + + if self.theme_cls.material_style == "M2": + self.height = self.theme_cls.standard_increment + else: + if self.type != "bottom": + if height_type_value == "small": + self.height = dp(64) + elif height_type_value == "medium": + self.height = dp(112) + elif height_type_value == "large": + self.height = dp(152) + else: + self.height = self.theme_cls.standard_increment + Clock.schedule_once(self.set_headline_font_style) + + def on_action_button(self, *args): + """ + Method for the button used for the :class:`~MDBottomAppBar` class. + """ + + def on_overflow_cls( + self, instance_toolbar, instance_overflow_cls: MDDropdownMenu + ) -> None: + """ + Called when the value of the :attr:`overflow_cls` attribute changes. + """ + + self.overflow_cls = instance_overflow_cls + + def on_md_bg_color(self, instance_toolbar, color_value: list) -> None: + """ + Called when the value of the :attr:`md_bg_color` attribute changes. + """ + + def on_md_bg_color(interval: Union[int, float]): + if self.type == "bottom": + self.md_bg_color = [0, 0, 0, 0] + else: + if self.set_bars_color: + set_bars_colors( + color_value, None, self.theme_cls.theme_style + ) + + Clock.schedule_once(on_md_bg_color) + + def on_left_action_items(self, instance_toolbar, items_value: list) -> None: + """ + Called when the value of the :attr:`left_action_items` attribute + changes. + """ + + def on_left_action_items(interval: Union[int, float]): + self.update_action_bar(self.ids.left_actions, items_value) + + Clock.schedule_once(on_left_action_items) + + def on_right_action_items( + self, instance_toolbar, items_value: list + ) -> None: + """ + Called when the value of the :attr:`right_action_items` attribute + changes. + """ + + def on_right_actions(interval: Union[int, float]): + self.update_action_bar(self.ids.right_actions, items_value) + + Clock.schedule_once(on_right_actions) + + def on_icon(self, instance_toolbar, icon_name: str) -> None: + """Called when the value of the :attr:`icon` attribute changes.""" + + self.action_button.icon = icon_name + + def on_icon_color(self, instance, icon_name: str) -> None: + """ + Called when the value of the :attr:`icon_color` attribute changes. + """ + + self.action_button.md_bg_color = icon_name + + def on_md_bg_bottom_color( + self, instance_toolbar, color_value: list + ) -> None: + """ + Called when the value of the :attr:`md_bg_bottom_color` attribute + changes. + """ + + set_bars_colors(None, color_value, self.theme_cls.theme_style) + + def on_anchor_title(self, instance_toolbar, anchor_value: str) -> None: + """ + Called when the value of the :attr:`anchor_title` attribute changes. + """ + + def on_anchor_title(interval: Union[int, float]): + self.ids.label_title.halign = anchor_value + + Clock.schedule_once(on_anchor_title) + + def on_mode(self, instance_toolbar, mode_value: str) -> None: + """Called when the value of the :attr:`made` attribute changes.""" + + if self.type == "top": + return + + def on_mode(interval: Union[int, float]): + def set_button_pos(*args): + self.action_button.x = x + self.action_button.y = y - self._rounded_rectangle_height / 2 + self.action_button._hard_shadow_size = (0, 0) + self.action_button._soft_shadow_size = (0, 0) + anim = Animation( + scale_value_x=1, scale_value_y=1, scale_value_z=1, d=0.05 + ) + anim.bind(on_complete=self.set_shadow) + anim.start(self.action_button) + + if mode_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 mode_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 mode_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 mode_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_value_x=0, scale_value_y=0, scale_value_z=0, d=0.1 + ) + anim.bind(on_complete=set_button_pos) + anim.start(self.action_button) + + Clock.schedule_once(on_mode) + + def set_md_bg_color(self, instance_toolbar, color_value: list) -> None: + if color_value == [1.0, 1.0, 1.0, 0.0]: + self.md_bg_color = self.theme_cls.primary_color + + def set_notch(self) -> None: + anim = Animation(d=0.1) + Animation( + notch_radius=self.action_button.width / 2 + dp(8), + d=0.1, + ) + anim.start(self) + + def set_shadow(self, *args) -> None: + self.action_button._elevation = self.action_button.elevation + + def get_default_overflow_cls(self) -> OverFlowMenu: + return OverFlowMenu(width_mult=4) + + def update_overflow_menu_items(self, action_button) -> None: + for data in self.overflow_cls.items: + if data["icon"] == action_button.icon: + self.overflow_cls.items.remove(data) + break + + def update_bar_height( + self, instance_theme_manager, material_style_value: str + ) -> None: + self.on_type_height(self, self.type_height) + self.update_anchor_title(material_style_value) + + def update_floating_radius(self, interval: Union[int, float]) -> None: + self.action_button.radius = self.action_button.width / 2 + + def update_anchor_title(self, material_style_value: str) -> str: + if material_style_value == "M2": + self.anchor_title = "left" + elif material_style_value == "M3" and self.type != "bottom": + self.anchor_title = "center" + elif material_style_value == "M3" and self.type == "bottom": + self.anchor_title = "left" + return self.anchor_title + + def update_action_bar( + self, instance_box_layout, action_bar_items: list + ) -> None: + instance_box_layout.clear_widgets() + new_width = 0 + + for item in action_bar_items: + new_width += dp(48) + if len(item) == 1: + item.append(lambda x: None) + if len(item) > 1 and not item[1]: + item[1] = lambda x: None + if len(item) == 2: + if type(item[1]) is str: + item.insert(1, lambda x: None) + else: + item.append("") + + instance_box_layout.add_widget( + ActionTopAppBarButton( + icon=item[0], + on_release=item[1], + tooltip_text=item[2], + overflow_text=item[3] if len(item) == 4 else "", + theme_text_color="Custom" + if not self.opposite_colors + else "Primary", + text_color=self.specific_text_color, + opposite_colors=self.opposite_colors, + ) + ) + + instance_box_layout.width = new_width + + def update_md_bg_color(self, *args) -> None: + self.md_bg_color = self.theme_cls._get_primary_color() + + def update_action_bar_text_colors(self, *args) -> None: + 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 remove_notch(self) -> None: + anim = Animation(d=0.1) + Animation(notch_radius=0, d=0.1) + anim.start(self) + + def remove_shadow(self) -> None: + self.action_button._elevation = 0 + + def _update_specific_text_color(self, instance, value): + if self.specific_text_color in ( + [0.0, 0.0, 0.0, 0.87], + [0.0, 0.0, 0.0, 1.0], + [1.0, 1.0, 1.0, 1.0], + ): + self.specific_text_color = text_colors[ + self.theme_cls.primary_palette + ][self.theme_cls.primary_hue] + + +class MDBottomAppBar(FloatLayout): + md_bg_color = ColorProperty([0, 0, 0, 0]) + """ + Color toolbar. + + :attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0]`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.size_hint_y = None + + def add_widget(self, widget, index=0, canvas=None): + if isinstance(widget, MDTopAppBar): + super().add_widget(widget) + return super().add_widget(widget.action_button) + + +class MDToolbar(MDTopAppBar): + """ + .. deprecated:: 1.0.0 + + Use :class:`~MDTopAppBar` class instead. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + Logger.warning( + "KivyMD: " + "The `MDToolbar` class has been deprecated. Use the `MDTopAppBar` " + "class instead." + ) diff --git a/sbapp/kivymd/uix/tooltip/__init__.py b/sbapp/kivymd/uix/tooltip/__init__.py new file mode 100644 index 0000000..7ee554f --- /dev/null +++ b/sbapp/kivymd/uix/tooltip/__init__.py @@ -0,0 +1 @@ +from .tooltip import MDTooltip, MDTooltipViewClass # NOQA F401 diff --git a/sbapp/kivymd/uix/tooltip/tooltip.kv b/sbapp/kivymd/uix/tooltip/tooltip.kv new file mode 100644 index 0000000..266bed5 --- /dev/null +++ b/sbapp/kivymd/uix/tooltip/tooltip.kv @@ -0,0 +1,38 @@ + + 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: root.tooltip_radius + Scale: + origin: self.center + x: root._scale_x + y: root._scale_y + canvas.after: + PopMatrix + + MDLabel: + id: label_tooltip + text: root.tooltip_text + size_hint: None, None + -text_size: None, None + size: self.texture_size + bold: True + theme_text_color: "Custom" + font_style: root.tooltip_font_style + markup: True + pos_hint: {"center_y": .5} + text_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) diff --git a/sbapp/kivymd/uix/tooltip/tooltip.py b/sbapp/kivymd/uix/tooltip/tooltip.py new file mode 100644 index 0000000..3c88333 --- /dev/null +++ b/sbapp/kivymd/uix/tooltip/tooltip.py @@ -0,0 +1,366 @@ +""" +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 = ''' + + + + MDScreen: + + 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") + +import os +from typing import Union + +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, + ColorProperty, + ListProperty, + NumericProperty, + OptionProperty, + StringProperty, +) +from kivy.uix.boxlayout import BoxLayout + +from kivymd import uix_path +from kivymd.font_definitions import theme_font_styles +from kivymd.material_resources import DEVICE_TYPE +from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import HoverBehavior, TouchBehavior + +with open( + os.path.join(uix_path, "tooltip", "tooltip.kv"), encoding="utf-8" +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDTooltip(ThemableBehavior, HoverBehavior, TouchBehavior): + tooltip_bg_color = ColorProperty(None) + """ + Tooltip background color in ``rgba`` format. + + :attr:`tooltip_bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + tooltip_text_color = ColorProperty(None) + """ + Tooltip text color in ``rgba`` format. + + :attr:`tooltip_text_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + tooltip_text = StringProperty() + """ + Tooltip text. + + :attr:`tooltip_text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + tooltip_font_style = OptionProperty("Caption", options=theme_font_styles) + """ + Tooltip font style. Available options are: `'H1'`, `'H2'`, `'H3'`, `'H4'`, + `'H5'`, `'H6'`, `'Subtitle1'`, `'Subtitle2'`, `'Body1'`, `'Body2'`, + `'Button'`, `'Caption'`, `'Overline'`, `'Icon'`. + + :attr:`tooltip_font_style` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'Caption'`. + """ + + tooltip_radius = ListProperty( + [ + dp(7), + ] + ) + """ + Corner radius values. + + :attr:`radius` is an :class:`~kivy.properties.ListProperty` + and defaults to `[dp(7),]`. + """ + + 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.NumericProperty` + and defaults to `0`. + """ + + shift_right = NumericProperty() + """ + Shifting the tooltip text to the right. + + .. versionadded:: 1.0.0 + + :attr:`shift_right` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + shift_left = NumericProperty() + """ + Shifting the tooltip text to the left. + + .. versionadded:: 1.0.0 + + :attr:`shift_left` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + _tooltip = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.register_event_type("on_show") + self.register_event_type("on_dismiss") + + 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: float, y: float) -> tuple: + """ + 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: Union[int, float]) -> None: + if not self._tooltip or self._tooltip.parent: + return + + Window.add_widget(self._tooltip) + pos = self.to_window(self.center_x, self.center_y) + + if not self.shift_right and not self.shift_left: + x = pos[0] - (self._tooltip.width / 2) + else: + if self.shift_right: + x = pos[0] - (self._tooltip.width / 2) + self.shift_right + if self.shift_left: + x = pos[0] - (self._tooltip.width / 2) - self.shift_left + + 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: Union[int, float]) -> None: + """Animation of opening tooltip on the screen.""" + + if self._tooltip: + ( + Animation(_scale_x=1, _scale_y=1, d=0.1) + + Animation(opacity=1, d=0.2) + ).start(self._tooltip) + self.dispatch("on_show") + + def animation_tooltip_dismiss(self, interval: Union[int, float]) -> None: + """ + .. versionadded:: 1.0.0 + + Animation of closing tooltip on the screen. + """ + + if self._tooltip: + anim = Animation(_scale_x=0, _scale_y=0, d=0.1) + Animation( + opacity=0, d=0.2 + ) + anim.bind(on_complete=self._on_dismiss_anim_complete) + anim.start(self._tooltip) + + def remove_tooltip(self, *args) -> None: + """Removes the tooltip widget from the screen.""" + + Window.remove_widget(self._tooltip) + + def on_long_touch(self, touch, *args) -> None: + if DEVICE_TYPE != "desktop": + self.on_enter(True) + + def on_enter(self, *args) -> None: + """ + 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": + if self.tooltip_text: + self._tooltip = MDTooltipViewClass( + tooltip_bg_color=self.tooltip_bg_color, + tooltip_text_color=self.tooltip_text_color, + tooltip_text=self.tooltip_text, + tooltip_font_style=self.tooltip_font_style, + tooltip_radius=self.tooltip_radius, + ) + Clock.schedule_once(self.display_tooltip, -1) + + def on_leave(self) -> None: + """ + See + :attr:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior.on_leave` + method in :class:`~kivymd.uix.behaviors.hover_behavior.HoverBehavior` + class. + """ + + if self._tooltip: + Clock.schedule_once(self.animation_tooltip_dismiss) + + def on_show(self) -> None: + """Default dismiss event handler.""" + + def on_dismiss(self) -> None: + """ + .. versionadded:: 1.0.0 + + Default dismiss event handler. + """ + + def _on_dismiss_anim_complete(self, *args): + self.dispatch("on_dismiss") + self.remove_tooltip() + self._tooltip = None + + +class MDTooltipViewClass(ThemableBehavior, BoxLayout): + tooltip_bg_color = ColorProperty(None) + """ + See :attr:`~MDTooltip.tooltip_bg_color`. + """ + + tooltip_text_color = ColorProperty(None) + """ + See :attr:`~MDTooltip.tooltip_text_color`. + """ + + tooltip_text = StringProperty() + """ + See :attr:`~MDTooltip.tooltip_text`. + """ + + tooltip_font_style = OptionProperty("Caption", options=theme_font_styles) + """ + See :attr:`~MDTooltip.tooltip_font_style`. + """ + + tooltip_radius = ListProperty() + """ + See :attr:`~MDTooltip.tooltip_radius`. + """ + + _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/sbapp/kivymd/uix/transition/__init__.py b/sbapp/kivymd/uix/transition/__init__.py new file mode 100644 index 0000000..f2c0a58 --- /dev/null +++ b/sbapp/kivymd/uix/transition/__init__.py @@ -0,0 +1,5 @@ +from .transition import ( # NOQA F401 + MDFadeSlideTransition, + MDSlideTransition, + MDSwapTransition, +) diff --git a/sbapp/kivymd/uix/transition/transition.py b/sbapp/kivymd/uix/transition/transition.py new file mode 100644 index 0000000..49bd657 --- /dev/null +++ b/sbapp/kivymd/uix/transition/transition.py @@ -0,0 +1,178 @@ +""" +Components/Transition +===================== + +.. rubric:: + A set of classes for implementing transitions between application screens. + +.. versionadded:: 1.0.0 + +Changing transitions +-------------------- + +You have multiple transitions available by default, such as: + +- :class:`MDFadeSlideTransition` + state one: the new screen closes the previous screen by lifting from the + bottom of the screen and changing from transparent to non-transparent; + + state two: the current screen goes down to the bottom of the screen, + passing from a non-transparent state to a transparent one, thus opening the + previous screen; + +.. note:: + You cannot control the direction of a slide using the direction attribute. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/transition-md-fade-slide-transition.gif + :align: center + +""" + +__all__ = ( + "MDFadeSlideTransition", + "MDSlideTransition", + "MDSwapTransition", + "MDTransitionBase", +) + +from kivy.animation import Animation, AnimationTransition +from kivy.uix.screenmanager import ( + ScreenManagerException, + SlideTransition, + SwapTransition, + TransitionBase, +) + +from kivymd.uix.screenmanager import MDScreenManager + + +class MDTransitionBase(TransitionBase): + _direction = "in" + hero_widget = None + hero_from_widget = None # kivymd.uix.hero.MDHeroFrom object + + def start(self, instance_screen_manager: MDScreenManager) -> None: + super().start(instance_screen_manager) + + {"in": self.animated_hero_in, "out": self.animated_hero_out}[ + self._direction + ]() + + def animated_hero_in(self) -> None: + if self.manager._heroes_data and self.manager.current_hero: + self.hero_from_widget = self.manager.get_hero_from_widget() + self._check_widget_properties() + self.hero_widget = self.hero_from_widget.children[0] + self.hero_from_widget.remove_widget(self.hero_widget) + + self.hero_widget.pos = self.screen_out.to_widget( + *self.hero_from_widget.to_window(*self.hero_from_widget.pos) + ) + self.hero_widget.size = self.hero_from_widget.size + self.manager.get_root_window().add_widget(self.hero_widget) + + Animation( + size=self.screen_in.hero_to.size, + d=self.duration, + pos=self.screen_in.hero_to.pos, + ).start(self.hero_widget) + self.hero_from_widget.dispatch( + "on_transform_in", self.hero_widget, self.duration + ) + + def animated_hero_out(self) -> None: + if self.manager._heroes_data and self.manager.current_hero: + self.screen_out.hero_to.remove_widget(self.hero_widget) + self.manager.get_root_window().add_widget(self.hero_widget) + + self.hero_from_widget.dispatch( + "on_transform_out", self.hero_widget, self.duration + ) + Animation( + pos=self.screen_in.to_widget( + *self.hero_from_widget.to_window(*self.hero_from_widget.pos) + ), + size=self.hero_from_widget.size, + d=self.duration, + ).start(self.hero_widget) + + def on_complete(self) -> None: + super().on_complete() + + if self._direction == "out": + self._direction = "in" + if self.manager._heroes_data and self.manager.current_hero: + self.manager.get_root_window().remove_widget(self.hero_widget) + self.hero_from_widget.add_widget(self.hero_widget) + else: + self._direction = "out" + if self.manager._heroes_data and self.manager.current_hero: + self.manager.get_root_window().remove_widget(self.hero_widget) + self.screen_in.hero_to.add_widget(self.hero_widget) + + def _check_widget_properties(self): + if not self.screen_in.hero_to: + raise Exception( + f"The `hero_to` attribute is not specified for screen {self.screen_in}" + ) + if len(self.hero_from_widget.children) > 1: + raise Exception( + f"{self.hero_from_widget.__class__} accept only one widget" + ) + + +class MDSwapTransition(SwapTransition, MDTransitionBase): + pass + + +class MDSlideTransition(SlideTransition, MDTransitionBase): + pass + + +class MDFadeSlideTransition(MDSlideTransition): + def start(self, instance_screen_manager: MDScreenManager) -> None: + if self.is_active: + raise ScreenManagerException("start() is called twice!") + + self.manager = instance_screen_manager + self._anim = Animation(d=self.duration, s=0) + self._anim.bind( + on_progress=self._on_progress, on_complete=self._on_complete + ) + + if self._direction == "in": + self.add_screen(self.screen_in) + self.animated_hero_in() + else: + self.animated_hero_out() + self.add_screen(self.screen_in) + self.add_screen(self.screen_out) + + self.screen_in.transition_progress = 0.0 + self.screen_in.transition_state = "in" + self.screen_out.transition_progress = 0.0 + self.screen_out.transition_state = "out" + self.screen_in.dispatch("on_pre_enter") + self.screen_out.dispatch("on_pre_leave") + + self.is_active = True + self._anim.start(self) + self.dispatch("on_progress", 0) + + if self._direction == "in": + self.screen_in.y = 0 + self.screen_in.opacity = 0 + + def on_progress(self, progression: float) -> None: + progression = AnimationTransition.out_quad(progression) + + if self._direction == "in": + self.screen_in.y = ( + self.manager.y + self.manager.height * progression + ) - self.screen_in.height + self.screen_in.opacity = progression + if self._direction == "out": + self.screen_out.y = ( + self.manager.y - self.manager.height * progression + ) + self.screen_out.opacity = 1 - progression diff --git a/sbapp/kivymd/uix/widget.py b/sbapp/kivymd/uix/widget.py new file mode 100644 index 0000000..23e4b80 --- /dev/null +++ b/sbapp/kivymd/uix/widget.py @@ -0,0 +1,45 @@ +""" +Components/Widget +================= + +:class:`~kivy.uix.widget.Widget` class equivalent. Simplifies working +with some widget properties. For example: + +Widget +------ + +.. code-block:: + + Widget: + size_hint: .5, None + height: self.width + + canvas: + Color: + rgba: app.theme_cls.primary_color + RoundedRectangle: + pos: self.pos + size: self.size + radius: [self.height / 2,] + +MDWidget +-------- + +.. code-block:: + + MDWidget: + size_hint: .5, None + height: self.width + radius: self.height / 2 + md_bg_color: app.theme_cls.primary_color +""" + +from kivymd.uix import MDAdaptiveWidget + + +class MDWidget(MDAdaptiveWidget): + """ + See :class:`~kivy.uix.Widget` class documentation for more information. + + .. versionadded:: 1.0.0 + """ diff --git a/sbapp/kivymd/utils/__init__.py b/sbapp/kivymd/utils/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/sbapp/kivymd/utils/asynckivy.py b/sbapp/kivymd/utils/asynckivy.py new file mode 100755 index 0000000..373ab0b --- /dev/null +++ b/sbapp/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/sbapp/kivymd/utils/fitimage.py b/sbapp/kivymd/utils/fitimage.py new file mode 100644 index 0000000..71d5fea --- /dev/null +++ b/sbapp/kivymd/utils/fitimage.py @@ -0,0 +1,19 @@ +""" +FitImage +======== + +.. note:: See :class:`~kivymd.uix.fitimage.FitImage` for more information +""" + +__all__ = ("FitImage",) + +from kivy import Logger + +from kivymd.uix.fitimage import FitImage + +Logger.warning( + "FitImage: Note!" + "\nIn the near future the `FitImage` widget will be moved to the " + "`kivymd.uix.fitimage` package.\nUse import of this widget like this:" + "`from kivymd.uix.fitimage import FitImage`." +) diff --git a/sbapp/kivymd/utils/fpsmonitor.py b/sbapp/kivymd/utils/fpsmonitor.py new file mode 100644 index 0000000..2284ba8 --- /dev/null +++ b/sbapp/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/sbapp/kivymd/utils/set_bars_colors.py b/sbapp/kivymd/utils/set_bars_colors.py new file mode 100644 index 0000000..0edf9cd --- /dev/null +++ b/sbapp/kivymd/utils/set_bars_colors.py @@ -0,0 +1,128 @@ +# The code is taken from AKivyMD project - +# https://github.com/kivymd-extensions/akivymd +# +# Source code - +# kivymd_extensions/akivymd/uix/statusbarcolor.py +# +# Author Sina Namadian - +# https://github.com/quitegreensky + +from typing import Union + +from kivy.utils import get_hex_from_color, platform + + +def set_bars_colors( + status_bar_color: Union[None, list], + navigation_bar_color: Union[None, list], + icons_color: str = "Light", +): + """ + Sets the color of the status of the StatusBar and NavigationBar. + + .. warning:: Works only on Android devices. + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/status-bar-color-light.png + :align: center + + .. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.utils.set_bars_colors import set_bars_colors + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "MDTopAppBar" + + MDBottomNavigation: + panel_color: app.theme_cls.primary_color + text_color_active: .2, .2, .2, 1 + text_color_normal: .9, .9, .9, 1 + use_text: False + + MDBottomNavigationItem: + icon: 'gmail' + + MDBottomNavigationItem: + icon: 'twitter' + + MDBottomNavigationItem: + icon: 'youtube' + ''' + + + class Test(MDApp): + def build(self): + self.set_bars_colors() + return Builder.load_string(KV) + + def set_bars_colors(self): + set_bars_colors( + self.theme_cls.primary_color, # status bar color + self.theme_cls.primary_color, # navigation bar color + "Light", # icons color of status bar + ) + + + Test().run() + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-bar-color.png + :align: center + + .. rubric:: Dark icon mode + + .. code-block:: python + + def set_bars_colors(self): + set_bars_colors( + self.theme_cls.primary_color, # status bar color + self.theme_cls.primary_color, # navigation bar color + "Dark", # icons color of status bar + ) + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/status-bar-color-dark.png + :align: center + + .. versionadded:: 1.0.0 + """ + + if platform == "android": + from android.runnable import run_on_ui_thread + from jnius import autoclass + + Color = autoclass("android.graphics.Color") + WindowManager = autoclass("android.view.WindowManager$LayoutParams") + activity = autoclass("org.kivy.android.PythonActivity").mActivity + View = autoclass("android.view.View") + + def statusbar(*args): + status_color = None + navigation_color = None + + if status_bar_color: + status_color = get_hex_from_color(status_bar_color)[:7] + if navigation_bar_color: + navigation_color = get_hex_from_color(navigation_bar_color)[:7] + window = activity.getWindow() + + if icons_color == "Dark": + window.getDecorView().setSystemUiVisibility( + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + ) + elif icons_color == "Light": + window.getDecorView().setSystemUiVisibility(0) + + window.clearFlags(WindowManager.FLAG_TRANSLUCENT_STATUS) + window.addFlags(WindowManager.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) + + if status_color: + window.setStatusBarColor(Color.parseColor(status_color)) + if navigation_color: + window.setNavigationBarColor(Color.parseColor(navigation_color)) + + return run_on_ui_thread(statusbar)() diff --git a/main.py b/sbapp/main.py similarity index 98% rename from main.py rename to sbapp/main.py index 711a520..8075ced 100644 --- a/main.py +++ b/sbapp/main.py @@ -3,9 +3,8 @@ import LXMF import time from kivy.logger import Logger, LOG_LEVELS -Logger.setLevel(LOG_LEVELS["error"]) - -from sideband.core import SidebandCore +# TODO: Reset +# Logger.setLevel(LOG_LEVELS["error"]) from kivymd.app import MDApp from kivy.core.window import Window @@ -13,11 +12,23 @@ from kivy.base import EventLoop from kivy.clock import Clock from kivy.lang.builder import Builder -from ui.layouts import root_layout -from ui.conversations import Conversations, MsgSync, NewConv -from ui.announces import Announces -from ui.messages import Messages, ts_format -from ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem +if RNS.vendor.platformutils.get_platform() == "android": + from sideband.core import SidebandCore + + from ui.layouts import root_layout + from ui.conversations import Conversations, MsgSync, NewConv + from ui.announces import Announces + from ui.messages import Messages, ts_format + from ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem + +else: + from .sideband.core import SidebandCore + + from .ui.layouts import root_layout + from .ui.conversations import Conversations, MsgSync, NewConv + from .ui.announces import Announces + from .ui.messages import Messages, ts_format + from .ui.helpers import ContentNavigationDrawer, DrawerList, IconListItem from kivy.metrics import dp from kivymd.uix.button import MDFlatButton @@ -824,4 +835,8 @@ Thank you very much for using Free Communications Systems. self.root.ids.screen_manager.current = "guide_screen" self.root.ids.nav_drawer.set_state("closed") -SidebandApp().run() +def run(): + SidebandApp().run() + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/sbapp/sideband/__init__.py b/sbapp/sideband/__init__.py new file mode 100644 index 0000000..f49e360 --- /dev/null +++ b/sbapp/sideband/__init__.py @@ -0,0 +1,5 @@ +import os +import glob + +modules = glob.glob(os.path.dirname(__file__)+"/*.py") +__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')] \ No newline at end of file diff --git a/sideband/core.py b/sbapp/sideband/core.py similarity index 100% rename from sideband/core.py rename to sbapp/sideband/core.py diff --git a/sbapp/ui/__init__.py b/sbapp/ui/__init__.py new file mode 100644 index 0000000..f49e360 --- /dev/null +++ b/sbapp/ui/__init__.py @@ -0,0 +1,5 @@ +import os +import glob + +modules = glob.glob(os.path.dirname(__file__)+"/*.py") +__all__ = [ os.path.basename(f)[:-3] for f in modules if not f.endswith('__init__.py')] \ No newline at end of file diff --git a/ui/announces.py b/sbapp/ui/announces.py similarity index 98% rename from ui/announces.py rename to sbapp/ui/announces.py index 5d70260..6a3de23 100644 --- a/ui/announces.py +++ b/sbapp/ui/announces.py @@ -12,7 +12,10 @@ from kivy.uix.boxlayout import BoxLayout from kivymd.uix.button import MDFlatButton from kivymd.uix.dialog import MDDialog -from ui.helpers import ts_format +if RNS.vendor.platformutils.get_platform() == "android": + from ui.helpers import ts_format +else: + from .helpers import ts_format class Announces(): def __init__(self, app): diff --git a/ui/conversations.py b/sbapp/ui/conversations.py similarity index 100% rename from ui/conversations.py rename to sbapp/ui/conversations.py diff --git a/ui/helpers.py b/sbapp/ui/helpers.py similarity index 100% rename from ui/helpers.py rename to sbapp/ui/helpers.py diff --git a/ui/layouts.py b/sbapp/ui/layouts.py similarity index 100% rename from ui/layouts.py rename to sbapp/ui/layouts.py diff --git a/ui/messages.py b/sbapp/ui/messages.py similarity index 95% rename from ui/messages.py rename to sbapp/ui/messages.py index 7402ae3..2ba57dd 100644 --- a/ui/messages.py +++ b/sbapp/ui/messages.py @@ -14,8 +14,12 @@ from kivy.uix.boxlayout import BoxLayout from kivymd.uix.button import MDFlatButton from kivymd.uix.dialog import MDDialog -from ui.helpers import ts_format, mdc -from ui.helpers import color_received, color_delivered, color_propagated, color_failed, color_unknown, intensity_msgs +if RNS.vendor.platformutils.get_platform() == "android": + from ui.helpers import ts_format, mdc + from ui.helpers import color_received, color_delivered, color_propagated, color_failed, color_unknown, intensity_msgs +else: + from .helpers import ts_format, mdc + from .helpers import color_received, color_delivered, color_propagated, color_failed, color_unknown, intensity_msgs class ListLXMessageCard(MDCard, RoundedRectangularElevationBehavior): text = StringProperty() diff --git a/setup.py b/setup.py index 695d583..16581ad 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,13 @@ import setuptools -from main.py import __version__ +__version__ = "0.1.6" +__variant__ = "beta" with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( - name="sideband", + name="sbapp", version=__version__, author="Mark Qvist", author_email="mark@unsigned.io", @@ -22,9 +23,9 @@ setuptools.setup( ], entry_points= { 'console_scripts': [ - 'sideband=main:main', + 'sideband=sbapp:main.run', ] }, - install_requires=['lxmf'], + install_requires=['rns>=0.3.9', 'lxmf>=0.1.7', 'kivy==2.1.0'], python_requires='>=3.6', )