Restructured repository

This commit is contained in:
Mark Qvist 2022-07-07 22:16:10 +02:00
parent 46269dd82b
commit 0de4c12c17
274 changed files with 51617 additions and 45 deletions

16
.gitignore vendored
View File

@ -1,3 +1,19 @@
sbapp/.buildozer
sbapp/buildozer.spec
sbapp/requirements.txt
sbapp/venv
sbapp/bin
sbapp/app_storage
sbapp/RNS
sbapp/LXMF
sbapp/precompiled
sbapp/*.DS_Store
sbapp/*.pyc
sbapp/build
sbapp/dist
sbapp/docs/build
sbapp/sideband*.egg-info
.buildozer
buildozer.spec
requirements.txt

View File

@ -1,35 +1,21 @@
all: prepare debug
devapk:
make -C sbapp devapk
prepare: activate
clean:
buildozer android clean
activate:
(. venv/bin/activate)
(mv setup.py setup.disabled)
debug:
buildozer android debug
release:
buildozer android release
postbuild:
(mv setup.disabled setup.py)
apk: prepare release postbuild
devapk: prepare debug postbuild
apk:
make -C sbapp apk
install:
adb install bin/sideband-0.1.6-arm64-v8a-debug.apk
install-release:
adb install bin/sideband-0.1.6-arm64-v8a-release.apk
make -C sbapp install
console:
(adb logcat | grep python)
make -C sbapp conole
getrns:
(rm ./RNS -r;cp -rv ../Reticulum/RNS ./;rm ./RNS/Utilities/RNS;rm ./RNS/__pycache__ -r)
clean:
@echo Cleaning...
-rm -r ./build
-rm -r ./dist
build_wheel:
python3 setup.py sdist bdist_wheel
release: build_wheel apk

38
sbapp/Makefile Normal file
View File

@ -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)

5
sbapp/__init__.py Normal file
View File

@ -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')]

View File

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 43 KiB

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

71
sbapp/kivymd/__init__.py Normal file
View File

@ -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

5
sbapp/kivymd/_version.py Normal file
View File

@ -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'

133
sbapp/kivymd/app.py Normal file
View File

@ -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)

954
sbapp/kivymd/color_definitions.py Executable file
View 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."""

View File

@ -0,0 +1,4 @@
"""
Effects
=======
"""

View File

@ -0,0 +1 @@
from .fadingedge import FadingEdgeEffect

View File

@ -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 dont 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)

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1 @@
from .roulettescroll import RouletteScrollEffect

View File

@ -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")

View File

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

View File

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

View File

@ -0,0 +1 @@
from .stiffscroll import StiffScrollEffect

View File

@ -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)

View File

@ -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")

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

7121
sbapp/kivymd/icon_definitions.py Executable file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

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

BIN
sbapp/kivymd/images/red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

View File

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

View File

@ -0,0 +1,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)

View File

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

View File

@ -0,0 +1,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"))

View File

@ -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()

View File

@ -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

1230
sbapp/kivymd/theming.py Executable file

File diff suppressed because it is too large Load Diff

View File

@ -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"
)

21
sbapp/kivymd/toast/LICENSE Executable file
View File

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

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

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

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

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

View File

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

View File

@ -0,0 +1,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()

View File

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

View File

@ -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)

View File

View File

@ -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()

View File

View File

@ -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)

View File

View File

@ -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())

View File

@ -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],
)
),
)
)

View File

@ -0,0 +1,3 @@
%s
def get_view(self) -> %s:
return self.view

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
%s

View File

@ -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.
"""

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,
},
}

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