Restructured repository
|
@ -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
|
||||||
buildozer.spec
|
buildozer.spec
|
||||||
requirements.txt
|
requirements.txt
|
||||||
|
|
44
Makefile
|
@ -1,35 +1,21 @@
|
||||||
all: prepare debug
|
devapk:
|
||||||
|
make -C sbapp devapk
|
||||||
|
|
||||||
prepare: activate
|
apk:
|
||||||
|
make -C sbapp apk
|
||||||
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
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
adb install bin/sideband-0.1.6-arm64-v8a-debug.apk
|
make -C sbapp install
|
||||||
|
|
||||||
install-release:
|
|
||||||
adb install bin/sideband-0.1.6-arm64-v8a-release.apk
|
|
||||||
|
|
||||||
console:
|
console:
|
||||||
(adb logcat | grep python)
|
make -C sbapp conole
|
||||||
|
|
||||||
getrns:
|
clean:
|
||||||
(rm ./RNS -r;cp -rv ../Reticulum/RNS ./;rm ./RNS/Utilities/RNS;rm ./RNS/__pycache__ -r)
|
@echo Cleaning...
|
||||||
|
-rm -r ./build
|
||||||
|
-rm -r ./dist
|
||||||
|
|
||||||
|
build_wheel:
|
||||||
|
python3 setup.py sdist bdist_wheel
|
||||||
|
|
||||||
|
release: build_wheel apk
|
|
@ -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)
|
|
@ -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')]
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
@ -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 <http://kivy.org/#home>`_
|
||||||
|
a framework for cross-platform, touch-enabled graphical applications.
|
||||||
|
The project's goal is to approximate Google's `Material Design spec
|
||||||
|
<https://material.io/design/introduction>`_ as close as possible without
|
||||||
|
sacrificing ease of use or application performance.
|
||||||
|
|
||||||
|
This library is a fork of the `KivyMD project
|
||||||
|
<https://gitlab.com/kivymd/KivyMD>`_ the author of which stopped supporting
|
||||||
|
this project three years ago. We found the strength and brought this project
|
||||||
|
to a new level. Currently we're in **beta** status, so things are changing
|
||||||
|
all the time and we cannot promise any kind of API stability.
|
||||||
|
However it is safe to vendor now and make use of what's currently available.
|
||||||
|
|
||||||
|
Join the project! Just fork the project, branch out and submit a pull request
|
||||||
|
when your patch is ready. If any changes are necessary, we'll guide you
|
||||||
|
through the steps that need to be done via PR comments or access to your for
|
||||||
|
may be requested to outright submit them. If you wish to become a project
|
||||||
|
developer (permission to create branches on the project without forking for
|
||||||
|
easier collaboration), have at least one PR approved and ask for it.
|
||||||
|
If you contribute regularly to the project the role may be offered to you
|
||||||
|
without asking too.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
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
|
|
@ -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'
|
|
@ -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)
|
|
@ -0,0 +1,954 @@
|
||||||
|
"""
|
||||||
|
Themes/Color Definitions
|
||||||
|
========================
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
`Material Design spec, The color system <https://material.io/design/color/the-color-system.html>`_
|
||||||
|
|
||||||
|
`Material Design spec, The color tool <https://material.io/resources/color/#!/?view.left=0&view.right=0>`_
|
||||||
|
|
||||||
|
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
|
||||||
|
<https://material.io/design/color/the-color-system.html>`_.
|
||||||
|
|
||||||
|
To demonstrate the shades of the palette, you can run the following code:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from kivy.lang import Builder
|
||||||
|
from kivy.properties import ListProperty, StringProperty
|
||||||
|
|
||||||
|
from kivymd.color_definitions import colors
|
||||||
|
from kivymd.uix.tab import MDTabsBase
|
||||||
|
from kivymd.uix.boxlayout import MDBoxLayout
|
||||||
|
|
||||||
|
demo = '''
|
||||||
|
<Root@MDBoxLayout>
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
<ItemColor>
|
||||||
|
size_hint_y: None
|
||||||
|
height: "42dp"
|
||||||
|
|
||||||
|
MDLabel:
|
||||||
|
text: root.text
|
||||||
|
halign: "center"
|
||||||
|
|
||||||
|
|
||||||
|
<Tab>
|
||||||
|
'''
|
||||||
|
|
||||||
|
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."""
|
|
@ -0,0 +1,4 @@
|
||||||
|
"""
|
||||||
|
Effects
|
||||||
|
=======
|
||||||
|
"""
|
|
@ -0,0 +1 @@
|
||||||
|
from .fadingedge import FadingEdgeEffect
|
|
@ -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)
|
|
@ -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.
|
|
@ -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.
|
|
@ -0,0 +1 @@
|
||||||
|
from .roulettescroll import RouletteScrollEffect
|
|
@ -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")
|
|
@ -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.
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
from .stiffscroll import StiffScrollEffect
|
|
@ -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)
|
|
@ -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")
|
|
@ -0,0 +1,69 @@
|
||||||
|
"""
|
||||||
|
Themes/Font Definitions
|
||||||
|
=======================
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
`Material Design spec, The type system <https://material.io/design/typography/the-type-system.html>`_
|
||||||
|
"""
|
||||||
|
|
||||||
|
from kivy.core.text import LabelBase
|
||||||
|
|
||||||
|
from kivymd import fonts_path
|
||||||
|
|
||||||
|
fonts = [
|
||||||
|
{
|
||||||
|
"name": "Roboto",
|
||||||
|
"fn_regular": fonts_path + "Roboto-Regular.ttf",
|
||||||
|
"fn_bold": fonts_path + "Roboto-Bold.ttf",
|
||||||
|
"fn_italic": fonts_path + "Roboto-Italic.ttf",
|
||||||
|
"fn_bolditalic": fonts_path + "Roboto-BoldItalic.ttf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RobotoThin",
|
||||||
|
"fn_regular": fonts_path + "Roboto-Thin.ttf",
|
||||||
|
"fn_italic": fonts_path + "Roboto-ThinItalic.ttf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RobotoLight",
|
||||||
|
"fn_regular": fonts_path + "Roboto-Light.ttf",
|
||||||
|
"fn_italic": fonts_path + "Roboto-LightItalic.ttf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RobotoMedium",
|
||||||
|
"fn_regular": fonts_path + "Roboto-Medium.ttf",
|
||||||
|
"fn_italic": fonts_path + "Roboto-MediumItalic.ttf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RobotoBlack",
|
||||||
|
"fn_regular": fonts_path + "Roboto-Black.ttf",
|
||||||
|
"fn_italic": fonts_path + "Roboto-BlackItalic.ttf",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Icons",
|
||||||
|
"fn_regular": fonts_path + "materialdesignicons-webfont.ttf",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
for font in fonts:
|
||||||
|
LabelBase.register(**font)
|
||||||
|
|
||||||
|
theme_font_styles = [
|
||||||
|
"H1",
|
||||||
|
"H2",
|
||||||
|
"H3",
|
||||||
|
"H4",
|
||||||
|
"H5",
|
||||||
|
"H6",
|
||||||
|
"Subtitle1",
|
||||||
|
"Subtitle2",
|
||||||
|
"Body1",
|
||||||
|
"Body2",
|
||||||
|
"Button",
|
||||||
|
"Caption",
|
||||||
|
"Overline",
|
||||||
|
"Icon",
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/font-styles-2.png
|
||||||
|
"""
|
After Width: | Height: | Size: 553 B |
After Width: | Height: | Size: 147 B |
After Width: | Height: | Size: 147 B |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 29 KiB |
After Width: | Height: | Size: 147 B |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 31 KiB |
After Width: | Height: | Size: 21 KiB |
|
@ -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]}}
|
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 43 KiB |
|
@ -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]}}
|
After Width: | Height: | Size: 30 KiB |
After Width: | Height: | Size: 32 KiB |
After Width: | Height: | Size: 28 KiB |
|
@ -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]}}
|
After Width: | Height: | Size: 147 B |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 40 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -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]}}
|
After Width: | Height: | Size: 156 B |
After Width: | Height: | Size: 147 B |
|
@ -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)
|
|
@ -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)
|
|
@ -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)
|
|
@ -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"))
|
|
@ -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()
|
|
@ -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
|
|
@ -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"
|
||||||
|
)
|
|
@ -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.
|
|
@ -0,0 +1,41 @@
|
||||||
|
KivyToast
|
||||||
|
========
|
||||||
|
|
||||||
|
A package for working with messages like Toast on Android. It is intended for use in applications written using the Kivy framework.
|
||||||
|
|
||||||
|
This package is an improved version of the package https://github.com/knappador/kivy-toaster in which human toasts are written, written on Kivy.
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/HeaTTheatR/KivyToast/master/Screenshot.png" align="center"/>
|
||||||
|
|
||||||
|
The package modules are written using the framework for cross-platform development of <Kivy>.
|
||||||
|
Information about the <Kivy> framework is available at http://kivy.org.
|
||||||
|
|
||||||
|
An example of usage (note that with this import the native implementation of toasts will be used for the Android platform and implementation on Kivy for others:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from toast import toast
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
# And then in the code, toasts are available
|
||||||
|
# by calling the toast function:
|
||||||
|
toast ('Your message')
|
||||||
|
```
|
||||||
|
|
||||||
|
To force the Kivy implementation on the Android platform, use the import of the form:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from toast.kivytoast import toast
|
||||||
|
```
|
||||||
|
|
||||||
|
PROGRAMMING LANGUAGE
|
||||||
|
--------------------
|
||||||
|
Python 2.7 +
|
||||||
|
|
||||||
|
DEPENDENCE
|
||||||
|
----------
|
||||||
|
The [Kivy] framework (http://kivy.org/docs/installation/installation.html)
|
||||||
|
|
||||||
|
LICENSE
|
||||||
|
-------
|
||||||
|
MIT
|
|
@ -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
|
|
@ -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
|
|
@ -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()
|
|
@ -0,0 +1,3 @@
|
||||||
|
__all__ = ("toast",)
|
||||||
|
|
||||||
|
from .kivytoast import toast
|
|
@ -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(
|
||||||
|
"""
|
||||||
|
<Toast>:
|
||||||
|
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)
|
|
@ -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()
|
|
@ -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 <https://kivymd.readthedocs.io/en/latest/api/kivymd/tools/patterns/create_project/#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)
|
|
@ -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())
|
|
@ -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],
|
||||||
|
)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
|
@ -0,0 +1,3 @@
|
||||||
|
%s
|
||||||
|
def get_view(self) -> %s:
|
||||||
|
return self.view
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,134 @@
|
||||||
|
"""
|
||||||
|
Restdb.io API Wrapper
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
This package is an API Wrapper for the website `restdb.io <https://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
|
|
@ -0,0 +1 @@
|
||||||
|
%s
|
|
@ -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.
|
||||||
|
"""
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}
|