From 67a8f61af8bdd2583ef8ed24d6ad80d7e39ae677 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Mon, 10 Jul 2023 02:49:58 +0200 Subject: [PATCH] Updated build system for Kivy 2.2.1 --- .gitignore | 3 +- sbapp/Makefile | 2 + sbapp/buildozer.spec | 7 +- sbapp/kivymd/__init__.py | 8 +- sbapp/kivymd/app.py | 4 +- .../kivymd/data/glsl/elevation/elevation.frag | 51 - sbapp/kivymd/data/glsl/elevation/header.frag | 10 - sbapp/kivymd/data/glsl/elevation/main.frag | 10 - sbapp/kivymd/effects/fadingedge/fadingedge.py | 3 + sbapp/kivymd/factory_registers.py | 3 + .../fonts/materialdesignicons-webfont.ttf | Bin 1243500 -> 1261792 bytes sbapp/kivymd/icon_definitions.py | 199 +- sbapp/kivymd/material_resources.py | 26 + sbapp/kivymd/tests/base_test.py | 9 - .../pyinstaller/test_pyinstaller_packaging.py | 94 - sbapp/kivymd/tests/test_app.py | 21 - sbapp/kivymd/tests/test_backdrop.py | 24 - sbapp/kivymd/tests/test_bottom_navigation.py | 32 - sbapp/kivymd/tests/test_card.py | 25 - sbapp/kivymd/tests/test_chip.py | 16 - sbapp/kivymd/tests/test_create_project.py | 15 - sbapp/kivymd/tests/test_fitimage.py | 24 - sbapp/kivymd/tests/test_font_definitions.py | 16 - sbapp/kivymd/tests/test_icon_definitions.py | 10 - sbapp/kivymd/tests/test_imagelist.py | 39 - sbapp/kivymd/tests/test_list.py | 67 - sbapp/kivymd/tests/test_navigationdrawer.py | 94 - sbapp/kivymd/tests/test_tab.py | 14 - sbapp/kivymd/tests/test_textfield.py | 72 - sbapp/kivymd/theming.py | 88 +- sbapp/kivymd/toast/kivytoast/kivytoast.py | 2 +- .../packaging/pyinstaller/hook-kivymd.py | 12 - sbapp/kivymd/tools/patterns/create_project.py | 3 +- sbapp/kivymd/uix/__init__.py | 6 + sbapp/kivymd/uix/anchorlayout.py | 5 +- sbapp/kivymd/uix/backdrop/backdrop.py | 31 +- sbapp/kivymd/uix/banner/banner.py | 7 + sbapp/kivymd/uix/behaviors/__init__.py | 5 + .../uix/behaviors/backgroundcolor_behavior.py | 44 +- sbapp/kivymd/uix/behaviors/elevation.py | 431 ++--- sbapp/kivymd/uix/behaviors/focus_behavior.py | 15 +- sbapp/kivymd/uix/behaviors/hover_behavior.py | 7 +- sbapp/kivymd/uix/behaviors/magic_behavior.py | 1 - sbapp/kivymd/uix/behaviors/motion_behavior.py | 287 +++ sbapp/kivymd/uix/behaviors/ripple_behavior.py | 14 +- sbapp/kivymd/uix/behaviors/rotate_behavior.py | 4 + sbapp/kivymd/uix/behaviors/scale_behavior.py | 25 +- sbapp/kivymd/uix/behaviors/touch_behavior.py | 14 +- .../uix/bottomnavigation/bottomnavigation.py | 40 +- sbapp/kivymd/uix/bottomsheet/__init__.py | 5 +- sbapp/kivymd/uix/bottomsheet/bottomsheet.kv | 97 +- sbapp/kivymd/uix/bottomsheet/bottomsheet.py | 1412 +++++++++++---- sbapp/kivymd/uix/boxlayout.py | 5 +- sbapp/kivymd/uix/button/button.kv | 10 +- sbapp/kivymd/uix/button/button.py | 179 +- sbapp/kivymd/uix/card/card.py | 53 +- sbapp/kivymd/uix/carousel.py | 3 +- sbapp/kivymd/uix/chip/__init__.py | 2 +- sbapp/kivymd/uix/chip/chip.kv | 122 +- sbapp/kivymd/uix/chip/chip.py | 1471 ++++++++++----- sbapp/kivymd/uix/circularlayout.py | 1 - sbapp/kivymd/uix/datatables/datatables.kv | 7 + sbapp/kivymd/uix/datatables/datatables.py | 148 +- sbapp/kivymd/uix/dialog/dialog.kv | 2 +- sbapp/kivymd/uix/dialog/dialog.py | 50 +- sbapp/kivymd/uix/dropdownitem/dropdownitem.py | 11 + .../uix/expansionpanel/expansionpanel.py | 29 +- sbapp/kivymd/uix/filemanager/filemanager.kv | 4 +- sbapp/kivymd/uix/filemanager/filemanager.py | 11 +- sbapp/kivymd/uix/fitimage/fitimage.py | 8 + sbapp/kivymd/uix/floatlayout.py | 5 +- sbapp/kivymd/uix/gridlayout.py | 5 +- sbapp/kivymd/uix/imagelist/imagelist.kv | 1 + sbapp/kivymd/uix/imagelist/imagelist.py | 14 +- sbapp/kivymd/uix/label/label.kv | 13 +- sbapp/kivymd/uix/label/label.py | 534 +++++- sbapp/kivymd/uix/list/list.py | 200 +- sbapp/kivymd/uix/menu/menu.kv | 511 +++++- sbapp/kivymd/uix/menu/menu.py | 1608 ++++++++++------- .../uix/navigationdrawer/navigationdrawer.py | 27 +- .../uix/navigationrail/navigationrail.py | 118 +- .../uix/pickers/colorpicker/colorpicker.py | 18 +- .../uix/pickers/datepicker/datepicker.kv | 173 +- .../uix/pickers/datepicker/datepicker.py | 551 +++--- .../uix/pickers/timepicker/timepicker.py | 3 +- sbapp/kivymd/uix/progressbar/progressbar.kv | 6 +- sbapp/kivymd/uix/progressbar/progressbar.py | 24 +- sbapp/kivymd/uix/recyclegridlayout.py | 3 +- sbapp/kivymd/uix/recycleview.py | 6 +- .../kivymd/uix/refreshlayout/refreshlayout.kv | 4 +- .../kivymd/uix/refreshlayout/refreshlayout.py | 125 +- sbapp/kivymd/uix/relativelayout.py | 5 +- sbapp/kivymd/uix/screen.py | 3 +- sbapp/kivymd/uix/segmentedbutton/__init__.py | 4 + .../uix/segmentedbutton/segmentedbutton.kv | 32 + .../uix/segmentedbutton/segmentedbutton.py | 653 +++++++ .../uix/segmentedcontrol/segmentedcontrol.kv | 6 +- .../uix/segmentedcontrol/segmentedcontrol.py | 14 +- sbapp/kivymd/uix/selection/selection.py | 19 +- .../uix/selectioncontrol/selectioncontrol.py | 351 +++- sbapp/kivymd/uix/slider/slider.py | 20 +- sbapp/kivymd/uix/sliverappbar/sliverappbar.py | 35 +- sbapp/kivymd/uix/snackbar/__init__.py | 7 +- sbapp/kivymd/uix/snackbar/snackbar.kv | 52 +- sbapp/kivymd/uix/snackbar/snackbar.py | 823 ++++----- sbapp/kivymd/uix/spinner/spinner.py | 6 +- sbapp/kivymd/uix/stacklayout.py | 5 +- sbapp/kivymd/uix/swiper/swiper.py | 14 +- sbapp/kivymd/uix/tab/tab.py | 18 +- sbapp/kivymd/uix/textfield/textfield.kv | 166 +- sbapp/kivymd/uix/textfield/textfield.py | 294 ++- sbapp/kivymd/uix/toolbar/__init__.py | 8 +- sbapp/kivymd/uix/toolbar/toolbar.py | 1034 ++++++++++- sbapp/kivymd/uix/tooltip/tooltip.py | 23 +- sbapp/kivymd/uix/transition/transition.py | 1 - sbapp/kivymd/uix/widget.py | 3 +- sbapp/kivymd/utils/fpsmonitor.py | 7 +- sbapp/main.py | 4 +- sbapp/patches/AndroidManifest.tmpl.xml | 6 +- sbapp/patches/p4a_build.py | 1064 +++++++++++ sbapp/services/sidebandservice.py | 2 +- sbapp/ui/announces.py | 12 +- sbapp/ui/conversations.py | 19 +- sbapp/ui/helpers.py | 2 +- sbapp/ui/messages.py | 12 +- setup.py | 4 +- 126 files changed, 9967 insertions(+), 4279 deletions(-) delete mode 100644 sbapp/kivymd/data/glsl/elevation/elevation.frag delete mode 100644 sbapp/kivymd/data/glsl/elevation/header.frag delete mode 100644 sbapp/kivymd/data/glsl/elevation/main.frag delete mode 100644 sbapp/kivymd/tests/base_test.py delete mode 100644 sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py delete mode 100644 sbapp/kivymd/tests/test_app.py delete mode 100644 sbapp/kivymd/tests/test_backdrop.py delete mode 100644 sbapp/kivymd/tests/test_bottom_navigation.py delete mode 100644 sbapp/kivymd/tests/test_card.py delete mode 100644 sbapp/kivymd/tests/test_chip.py delete mode 100644 sbapp/kivymd/tests/test_create_project.py delete mode 100644 sbapp/kivymd/tests/test_fitimage.py delete mode 100644 sbapp/kivymd/tests/test_font_definitions.py delete mode 100644 sbapp/kivymd/tests/test_icon_definitions.py delete mode 100644 sbapp/kivymd/tests/test_imagelist.py delete mode 100644 sbapp/kivymd/tests/test_list.py delete mode 100644 sbapp/kivymd/tests/test_navigationdrawer.py delete mode 100644 sbapp/kivymd/tests/test_tab.py delete mode 100644 sbapp/kivymd/tests/test_textfield.py create mode 100644 sbapp/kivymd/uix/behaviors/motion_behavior.py mode change 100755 => 100644 sbapp/kivymd/uix/behaviors/ripple_behavior.py mode change 100755 => 100644 sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py mode change 100755 => 100644 sbapp/kivymd/uix/bottomsheet/bottomsheet.py mode change 100755 => 100644 sbapp/kivymd/uix/imagelist/imagelist.py create mode 100644 sbapp/kivymd/uix/segmentedbutton/__init__.py create mode 100644 sbapp/kivymd/uix/segmentedbutton/segmentedbutton.kv create mode 100644 sbapp/kivymd/uix/segmentedbutton/segmentedbutton.py create mode 100644 sbapp/patches/p4a_build.py diff --git a/.gitignore b/.gitignore index 241691d..cce6603 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +sbapp/kivymd_working sbapp/.buildozer sbapp/requirements.txt sbapp/venv @@ -29,4 +30,4 @@ build dist docs/build sideband*.egg-info -sbapp*.egg-info \ No newline at end of file +sbapp*.egg-info diff --git a/sbapp/Makefile b/sbapp/Makefile index 18ba2a3..da6841f 100644 --- a/sbapp/Makefile +++ b/sbapp/Makefile @@ -30,12 +30,14 @@ patchsdl: cp patches/PythonService.java .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/src/main/java/org/kivy/android/PythonService.java injectxml: + # mkdir /home/markqvist/.local/lib/python3.11/site-packages/pythonforandroid/bootstraps/sdl2/build/src/main/xml # Inject XML on arm64-v8a mkdir -p .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/src/main/res/xml mkdir -p .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/templates cp patches/device_filter.xml .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/src/main/res/xml/ cp patches/file_paths.xml .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/src/main/res/xml/ cp patches/AndroidManifest.tmpl.xml .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/templates/ + cp patches/p4a_build.py .buildozer/android/platform/build-arm64-v8a_armeabi-v7a/dists/sideband/build.py debug: buildozer android debug diff --git a/sbapp/buildozer.spec b/sbapp/buildozer.spec index f9f8483..6d76205 100644 --- a/sbapp/buildozer.spec +++ b/sbapp/buildozer.spec @@ -12,9 +12,10 @@ version.regex = __version__ = ['"](.*)['"] version.filename = %(source.dir)s/main.py android.numeric_version = 20230204 -requirements = python3==3.9.5,hostpython3==3.9.5,cryptography,cffi,pycparser,kivy==2.1.0,pygments,sdl2,sdl2_ttf==2.0.15,pillow,qrcode==7.3.1,netifaces,libbz2,pydenticon,usb4a,usbserial4a +#requirements = python3==3.9.5,hostpython3==3.9.5,cryptography,cffi,pycparser,kivy==2.2.1,pygments,sdl2,sdl2_ttf==2.0.15,pillow,qrcode==7.3.1,netifaces,libbz2,pydenticon,usb4a,usbserial4a +requirements = kivy==2.2.1,libbz2,pillow,qrcode==7.3.1,usb4a,usbserial4a + p4a.local_recipes = ../Others/python-for-android/pythonforandroid/recipes -requirements.source.kivymd = ../../Others/KivyMD-master icon.filename = %(source.dir)s/assets/icon.png presplash.filename = %(source.dir)s/assets/presplash_small.png @@ -27,7 +28,7 @@ fullscreen = 0 android.permissions = INTERNET,POST_NOTIFICATIONS,WAKE_LOCK,FOREGROUND_SERVICE,CHANGE_WIFI_MULTICAST_STATE,BLUETOOTH_CONNECT android.api = 30 android.minapi = 24 -android.ndk = 23b +android.ndk = 25b android.skip_update = False android.accept_sdk_license = True android.release_artifact = apk diff --git a/sbapp/kivymd/__init__.py b/sbapp/kivymd/__init__.py index fa6cb27..bb05e0f 100644 --- a/sbapp/kivymd/__init__.py +++ b/sbapp/kivymd/__init__.py @@ -26,11 +26,12 @@ import os import kivy from kivy.logger import Logger -__version__ = "1.1.0.dev0" +__version__ = "1.2.0.dev0" """KivyMD version.""" release = False -kivy.require("2.0.0") +if "READTHEDOCS" not in os.environ: + kivy.require("2.2.0") try: from kivymd._version import __date__, __hash__, __short_hash__ @@ -49,9 +50,6 @@ images_path = os.path.join(path, f"images{os.sep}") uix_path = os.path.join(path, "uix") """Path to uix directory.""" -glsl_path = os.path.join(path, "data", "glsl") -"""Path to glsl directory.""" - _log_message = ( "KivyMD:" + (" Release" if release else "") diff --git a/sbapp/kivymd/app.py b/sbapp/kivymd/app.py index dfe3b27..bdad688 100644 --- a/sbapp/kivymd/app.py +++ b/sbapp/kivymd/app.py @@ -54,7 +54,7 @@ from kivymd.theming import ThemeManager class FpsMonitoring: """Implements a monitor to display the current FPS in the toolbar.""" - def fps_monitor_start(self) -> None: + def fps_monitor_start(self, anchor: str = "top") -> None: """Adds a monitor to the main application window.""" def add_monitor(*args): @@ -62,7 +62,7 @@ class FpsMonitoring: from kivymd.utils.fpsmonitor import FpsMonitor - monitor = FpsMonitor() + monitor = FpsMonitor(anchor=anchor) monitor.start() Window.add_widget(monitor) diff --git a/sbapp/kivymd/data/glsl/elevation/elevation.frag b/sbapp/kivymd/data/glsl/elevation/elevation.frag deleted file mode 100644 index 03f042a..0000000 --- a/sbapp/kivymd/data/glsl/elevation/elevation.frag +++ /dev/null @@ -1,51 +0,0 @@ -/* -The shader code has been refactored for the KivyMD library. -You can find the original code of this shaders at the links: - -https://www.shadertoy.com/view/WtdSDs -https://www.shadertoy.com/view/fsdyzB - -Additional thanks to iq for optimizing conditional block for individual -corner radius: -https://iquilezles.org/articles/distfunctions -*/ - -// For lower opengl version - -float custom_smoothstep(float a, float b, float x) { - float t = clamp((x - a) / (b - a), 0.0, 1.0); - return t * t * (3.0 - 2.0 * t); -} - -float roundedBoxSDF(vec2 centerPosition, vec2 size, vec4 radius) { - radius.xy = (centerPosition.x > 0.0) ? radius.xy : radius.zw; - radius.x = (centerPosition.y > 0.0) ? radius.x : radius.y; - - vec2 q = abs(centerPosition) - (size - shadow_softness) + radius.x; - return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius.x; -} - -void mainImage(out vec4 fragColor, in vec2 fragCoord) { - // Smooth the result (free antialiasing). - float edge0 = 0.0; - float smoothedAlpha = 1.0 - custom_smoothstep(0.0, edge0, 1.0); - // Get the resultant shape. - vec4 quadColor = mix( - vec4( - shadow_color[0], - shadow_color[1], - shadow_color[2], - 0.0 - ), - shadow_color, - smoothedAlpha - ); - // Apply a drop shadow effect. - float shadowDistance = roundedBoxSDF( - fragCoord.xy - mouse.xy - (size / 2.0), size / 2.0, shadow_radius - ); - float shadowAlpha = 1.0 - custom_smoothstep( - -shadow_softness, shadow_softness, shadowDistance - ); - fragColor = mix(quadColor, shadow_color, shadowAlpha - smoothedAlpha); -} diff --git a/sbapp/kivymd/data/glsl/elevation/header.frag b/sbapp/kivymd/data/glsl/elevation/header.frag deleted file mode 100644 index c09ce5c..0000000 --- a/sbapp/kivymd/data/glsl/elevation/header.frag +++ /dev/null @@ -1,10 +0,0 @@ -#ifdef GL_FRAGMENT_PRECISION_HIGH - precision highp float; -#endif - -uniform vec4 resolution; -uniform vec4 mouse; -uniform vec2 size; -uniform vec4 shadow_radius; -uniform float shadow_softness; -uniform vec4 shadow_color; diff --git a/sbapp/kivymd/data/glsl/elevation/main.frag b/sbapp/kivymd/data/glsl/elevation/main.frag deleted file mode 100644 index d9b8712..0000000 --- a/sbapp/kivymd/data/glsl/elevation/main.frag +++ /dev/null @@ -1,10 +0,0 @@ -vec2 gfc(in vec4 fc) { - vec2 canvas_pos = resolution.zw; - vec2 uv = fc.xy; - uv.y -= canvas_pos.y; - return uv; -} - -void main(void) { - mainImage(gl_FragColor, gfc(gl_FragCoord)); -} diff --git a/sbapp/kivymd/effects/fadingedge/fadingedge.py b/sbapp/kivymd/effects/fadingedge/fadingedge.py index 1af776a..106d3c8 100644 --- a/sbapp/kivymd/effects/fadingedge/fadingedge.py +++ b/sbapp/kivymd/effects/fadingedge/fadingedge.py @@ -164,6 +164,9 @@ class FadingEdgeEffect(ThemableBehavior): index, ), ) + self.update_canvas( + self, self.size, rectangle_top, rectangle_bottom, i + ) def update_canvas( self, diff --git a/sbapp/kivymd/factory_registers.py b/sbapp/kivymd/factory_registers.py index 1a20b01..4497bfe 100644 --- a/sbapp/kivymd/factory_registers.py +++ b/sbapp/kivymd/factory_registers.py @@ -5,6 +5,8 @@ Register KivyMD widgets to use without import. from kivy.factory import Factory register = Factory.register +register("MDSegmentedButton", module="kivymd.uix.segmentedbutton") +register("MDSegmentedButtonItem", module="kivymd.uix.segmentedbutton") register("MDScrollView", module="kivymd.uix.scrollview") register("MDRecycleView", module="kivymd.uix.recycleview") register("MDResponsiveLayout", module="kivymd.uix.responsivelayout") @@ -37,6 +39,7 @@ register("FitImage", module="kivymd.uix.fitimage") register("MDBackdrop", module="kivymd.uix.backdrop") register("MDBanner", module="kivymd.uix.banner") register("MDTooltip", module="kivymd.uix.tooltip") +register("MDBottomSheet", module="kivymd.uix.bottomsheet") register("MDBottomNavigation", module="kivymd.uix.bottomnavigation") register("MDBottomNavigationItem", module="kivymd.uix.bottomnavigation") register("MDToggleButton", module="kivymd.uix.behaviors.toggle_behavior") diff --git a/sbapp/kivymd/fonts/materialdesignicons-webfont.ttf b/sbapp/kivymd/fonts/materialdesignicons-webfont.ttf index d685510874af238852c0c9fec81b9c5b0f870fce..53061f16f158bed0620f197b83cb303fb9708a53 100644 GIT binary patch delta 47616 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
Z3dg6`-`Ziam9;5QzD*0XtIxr#M&&RO* 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 delta 31378 zcmYh?4OnI4-}nE|dVbu55JCtcgb+eQ2q9#I&=5jK2qAU|A%qZO`Vm5i2_YtgkP$)% zA%u_-Lb%^^-^c$r{?~QAT5Fx>_i49|nYH&e*Tkg&`P4)zzebZd}2B8u1y67oBlZ^*LXDqSfq7$?UE<^PJsl?_B$y zYOUsu$XL-XgV2Hh2H4bp|NVP}_3+A-|NsB2M{%z4%qg!fxp6vuE$d<96;^yw{$DIV z*Xvrkfl=WuUA}gTx%5%H%v$={IwSVY8~CY6S-OE=YA@Ypt=e$uaYbq7(rx@I`~RNZ z{I8KhIveMK0T?k|%bNCE2)3`<>A=#U(?3tAwr}*O&(h~*f zmzHLsTk0*xr1T_(o@9upvO&_*sc4k?GEmR2Jr`ZlvlM@}4%5h##uP zkn}q7uQR~w{n8utVDHT`v|~aVX6U!lK;B#IEiX*+gT%KD@xKN$=Tcl1AAZ9hctkmOk)Ng)!;F0yIlw#EsGTBd$Lp@1q&%<8*XNpV03U_C85c zU_1|l(x+w8XKCQod|veb{(MbTN?!>5(wD7RdE~eCRW`W(YC@Xie6mscItOFYH>`hK zAx))&`0vZ5A5zgPP1~ph15S@~|9>Ru$9Cx_n*78dKQ%}{XGk-J;1$jA8~KHpUpk}} ztnmj@D`uo$Df%mWvkW`S@i*3g8k48*Na|P&;{uFqDe{%j;3ds9wfcyV9 zP5x$^Kg*e@#gGgm8%>yzVe(g%#oquH*Vd>EI|rRI z9O9ia8D4=5KTAeXBO_!jEXJUWi2g=2B_mD&f7QeTG7>%t(SmUqt5A5AJ{hYPfn!Po z`em%f0ISu5{MBLtJiSN8`V6>!F*x6Vm<@>EU_!=*C1}BjjEodCV_e2Y*(gJ& zjEyrvuT7}23H6eju($~WWRj3sBxBPou(nwS+A%3(bJjMmL63|qie+_y!dv8^6pfgb zv1K_1WNeiQhT5uEMmF);wKBGjLEbjSGPW(kyo{U*C5;>sw#x%UY+op22O96tFC&+t zJCe8)gYC5P*e+vdhS-_CycSH!*af>*VNS+weKPVXp3nL2G~T0J#-7yLb4Er%7RF^P z$OE|x$lE&~t=#`YA2i#iUdFyfn3S6fLG%@et2Q zhCZlE#=)8Bk#Pv|hfuGCV@aKiLmA@Gb{U6pJggG^G7e{uQsPUK6e*=yDTNkNcwv)_ zBU3@*k%KbI2r8p-83oEFWE@3-qiAw8#gAtFXc`|wv13|gl-nr5h>T;aFe~G@GBkkP zhKs|ID9nu{J8 zr=_AFQ!-9Z!?28Ma;q7tnucd|%UIluAsJ_;U_?d@HO}JvEY{Ah<^G@DE8`p+9BZ3o zoXZgBj>$ML69k_}gY#x(oZpCX8Fe(RtCn$rPyqHXq~L{B=)$~=i;6*mdiLvkWL!+n z#q3>@4u(n++mM5P?tjCKj7uqUSw0xz@;nU6Xe79?7UMFmC`X@+D=Bnk114o$Mch^G zGMZ@K#Gp+yyt-b-HEC#(aV^cSrFJv9%`|PEl5t%TsB=B>*Ast3vVtG2AmE0T3lo@= z(UJp-wQzl77HYu$jq@^YDg%SwoC1p9%&@J6VE-0g`7L8IZfyqFw`HRUT;Db-qm7~3 z$i3Z1Ex(D|X?8~=Mr7PckvmChuSP#;c2^Z>a5shSrpVm`GM13Mq!i>W;ktwCj$#nk zF(uyIfb)Ae-ZLrVUWV+<1dZ>@mvMg+hGaaD#{GYwo)dz)@=%Rg84p%~<3qgahx%kZ zoP`EZyt@uFG9Iav@u% z8)b}f92=GK5k)`Z`r~mKpX7sLpU~iw85!dxGCs}5h>XuTepZN9jLG<%^UrJ12Vy6( zP=i4kU$Fi~J6Qj+2$M3tVu-IMx&M>Bd{UT`@pUJrWqgwkihM)xw{4i0G1Ub2zN6rG z6#PCFtsw6Q@}|j|=KRNg89$|=7(+6Cu9q>>%%_M#jLP`6K*p?O{AQyLgWUh$3HXD> zKWH*n3a;l!o}=-fd1wTM|H?!ICS?3wi8dMYG@751@o$VGbjbLRTK~24(IZB^Og`tB zMgs<9nw*scQN3TPspN>|UK?-_ghC&6%k4j|5 zeBO!KPjF1Ize*uGF)nk}YA`@bHfCh5R)k)e$<+y7y%JqA*GK_@YmCTDtpWqB$zW^t z$y|%VYu934X4;_4b;{8obKPRlWIbx67s^~e3oSA?utENYH0Mo-xltadxzVW1joI6{ z3=>M4n^0hrA(@$}C_+1iW%5SE+?1l5GWBK*wpj`KWp18>dXSSvOcsT*n$QD^Z$ZqK z9JeAbI}HrEHOH-4&&j};%GPfc?_4= zi+P#5P<&Tnb{&wpTdB|PJ9_n^ieB{KKSL>q|Tb5>>n!xZqM3#Mc) z;Q4PZs6vm-y<*JC+`ADpD$D_m_F>X}NZvOU6yKM<{aP_0v#3iZZ&1wrYcVeKfO3q= zJg@@971v-|=0Ws0h=vDK^WYAdhh(9F`(IKZv!qw%q3K}butHGi@Cuow#bELyQqTel zEM)qHU6_^0n-sH5r~-o@H7N7wRE)?xh6cxwdkk^qyomA{na6TGmbl}pLA~Qsx&Ox# zRM8`I5mTPfj7ga%c7ni42B>7Ml2>vPO-|~Ud2%*LK83idI+>^P0!|&1c^U(sHYoG- zT-1Q`YVxb=(SZ?6$vh(k-7*(5_~Jh9|CxoLSPczoXmnPq%(L?`AoH9ew9Bl`!H~>z zE72qKykVK=*MgY3OjMu`Gcqs8!l=v(CuCj}qXpEhr#^2)%!?akUgCrJB=1Db1}+-9 zWM0~YA(@wvbQ#x|+cFzdK%+(qUXcc3t{~=0&aY(sO4hIP!N6B>eHFtsk=Mj^Q$0px zUY!93yqer=sCP|+%xgLF=EO`kx5~V(5EQz;3KYM7O6Cpe=#bgMUJJjj8@ax5Lgq~r zzNug4&Eqm#3(za`mQ2)u+*?a9EAuw4+sb9$o`*@9cTnrjE}89vGVkh<$@>U%3Ge^R z4hnXV@IN;B*2lazOJ-*(`1RbEgBh9kQ-E(l%&tPP{$M5;;vtR?F~Gw#>n66l3WJ!I z`G^nJAL+r2%*WC}Y!9`2h~J<{JWSkM zbuyPTz;fy>@0IyBO-D-5E%P1Xlkbeke792OdlY_;>E0WcIm-2DD+Xk~PZPe~Fh8h~ z`C*34v0}{2{HQ_Z$E<%$qmQR$enPQN3Nb2koI2w@GCyqw*Pqe&Gjcx92LtgQ!JHs= zqL=&sMUKobx@CSz&{qYZ@K;=aH70Y?25XZPoScyPb(_p@8ZjvI+g6!Vm6(+IU7^hH zGcYUjhYFd~BvqApftj%}H{3iu- zGXJeaazf^RRbb=4Sy?Ja7p7$yIcUVZEVCGcvaC|H%d&mcVp5h<0^;2Qw1G8`T(3u# zPaN+Vte^wb3F#4)p%bIBk}-wj5m^Zq6D+Qh4FXdrkTN7|wOp{y`vhx^m1vNfj}{Ee zS~CW@YmUiUs{-}1)@E((ZdqyRV2E`}(JyOV^4D#WwO$&inVzSll}_UNG+Up^HlXMR z)3P=sk#8}qj5e^pQ3)upF|Tmr2C%k?4fZoBo;fUQ(@c!Z+Kl67eX=%B0nM@)CX4*6 zE=8 zE8mCseQPn!{ok)h)_zSGmQ|Dr0*feCG$CvMLJ+urD@Zz^OV)ugD1Km%tm0f*2WNxx zgK2sQ=ZDnG;yr~`(kJWCURj3`bJ(yfzVWaQC$7{7Yo){;kOC@Wwo-7N(aLnU4?mB#}Hpmqw)rf$~u;U$C7*Olq}v@SjTnAIz9zCsK*${t0)5X zD+XjOicy6o?*Af+oIvmiZL&^e<3xt3B)GC$)=3mPi6Ks+z{%iS5UZ*QGqO(21i7bi zJdN1XiRBv*tD1P;WLRhP%353|>&yc5%c{vhC0MVSm30>DXC(v{l z+A$=nmO`}*Q`;cxT+YwU2Sv}N;dvA~Z%WqrC1}Q&tU4N8K>mdSc^8g=niuuRsxJgN z7Z-rMOQ@MlGJUdNRs%&ESiH1U)@7;a1lN~mgJCY8mDNarD}3Z)K-QJzvaTZjs%cqG zbs*>JVlc=x6&R9rZ5BAcwhau^On!4dYS793ZzlM9bfbxWhHTN(6L_HN4p@ol__wgFkUGvMvrvhJt{`*#k@YHycy7x8y7 z?A_&Pk+r0n`@dvTR!16`yo1I6<)8}$-a`T2kXZM&$?8l;hphYRFedB%QCSaUqDNL& ziL3`H`XG%SDh2xwGeq}FeX<@&k@aY{tjByb%IdLE1)l%b#1s4PdCWwqfj5emA-yi&#?DQudHYD(Jkw_G+F)iAouwiaD1T>gR+)U ze_06_Y@kini{!k-0LhnzWW7wGmkA!E$>4-6-gH>6bjo_wmi1bUK3PL~;MKp*_3I?R z;iFO3n_R!yDr-0$DHd$YkqFdINLK*k}OE$mC1II~%CP!s`9iu_kHyN0e^)35TX{Z22 zzRSg+tnZ2Wepc2GJ+h`V(SiwCKQi=>wdj+@ml4*_kNA+gtBfHi2Gnh*Z`+o^N1 z*QCi>=@^o|HqF+q1wm;fr8S`+Q?l2om%VN(*y9Tadp&Z~iCv#T*Kd)%L5wlk8iM#`&0)y$N}nOv=uzk-ceik{?`b)+KxMEYKi}0$XID0u*+*n@{}pB>R91rmCzWAR_Q_nIJS+Q@df8Prnq{AwigwvN z`|Z=nJ3SA*va1_ppV1+EG1rSZo|z5yYtm((wGvIw;`;0u)IBGe!jEy;wH30@W#Qa8 z+2=K3Q1HE+W33^LlDrTqXOGZ0;#FlQc^9VOn+r z>kY(RS|j^1_AW0#9R}EMOq1P6;VX*3!j)`ZN$^!Pyo$nCvDZ|FA=y{wphNaG>|N6* z`&tIPc2agTxy^MT@47bG*Jq&zGqP_W=Z0a~Emg8_w2_Z?**BG8p8J0@1^FVuZk>>Q zON;DVi_tIpHeShX-I$f#)+qb-SoR&&vhSqeovgQKgIu2J_Fe4X)rWD}cc+0mOK7&F z3G=c$>Mx&P01$bNy77wSOaW!MiVI*|Y8xa_~!`J(qSL(Ur6;Fy{zXU$af$l*PKvld0x z8k4hj0lMX+5uer~XPp=nT$kgz&6tz3UJWMXq%+w1B^Z~pL7kip8D>NFHtdm;QHp*! z8)bt(|IWr0n3A&z8=DNt$t*&roK3SZB4;xiY&I=t^I6D%2;7T8d-Y*b&fY>ED$s#3IfdyUu8`}(0nEtRCl$r0M<<5m?90IW(tN)( z)Pc1ka*G;3Q{GoN`!`~o`+q>DoC9clz_^?Pi$G#AiNypI&&oNd0PUETb1=WwgS+J% zB9wytk_yn^&^%D*(0MtB6=Oio;VGcb;WRC!b}7drsI{V%oYMwgac#Yvb4%r%mjm|B8&IJTq(2i+2 z7c%vQU2-l;2Sw^L(J$v>VlHmQyqrsDn#{tioQ6I*m-4DE1w4FPC9Z&LC@p?7c$X zD;1cK^D6auf8e}Ijn^tMB!~A1NoR6y8<=dCFVW$-s=QSM_C&klk+}t@3Z$oHu(JKe8{in!y2$S z#wK4dIAb$%J|f}c9L&r4q+ZTAg~w~AANGZ&jW!!RAO4rbOkv6ScY*qKaupaj|Mq2 z6rJgk^GhK*;&6Epq;$(0}^dE+=yeudRz+f6XdOujZr1tRS90T4Ks36sxd5gH5#tQ_39+7J|=gKGEB)$ZI-*H zPz)NZm4!aJYiDCnZdw{zFe{hu3fy%#uFLhh47pwjsFB_&cYR*K`s8g;l;p>}+zlJ# zW>6%9qztb4(!kw_f*W((gy2m$&uqn<+)WG7EqAkgP-OEM)pE0leRPOevsFk||0Xr~hZvAcXoUvijmYYX> z-n86Zion*cc^Htp8-4Ow%csol1#f!=2WNr8hm?W6k|w!_vUX?} zM&%xsiUzrdr=c1&E3E~4N0iE4m;zqFkv4|pmbJ@0iv6Q2X3WW5MDPjhpIC%(xs^?FPm0kh_vCU+$vuUJRSbSAO;024v@yA-r=kS& za;xj)p23i3jL2PFEB8!l*QB6Z?pb_A;GV_F*#w+JKrIuWTO;>89}VcjwA}OSF(bDw z1H@dA4;o#_fETvNy(k8;^#vfmo}7z0UQEs<40_2pziR3wC*(FT(4~2DFSC&xklWZM zmnWyox9x5dK~3{=uV(FwQ$qy@8@P%*k!(lY1j^H&W*&3f@GW zo0{a_oQhVtt(jo&76!PrPVQ~R7?#^shA9TRJzehYOnN(ucW{0O1MskPd04u4j>>JP zczc)JyL{Axrgv9>f=fy^hf?G|#8eM8VL%3*;_esbB8fb(oVoQZM(N7@cz8%||09=iPt5RUqd3h-%@>XMQ^)hh1Mu)uA98_TC`LMh-IbU-? z-dY);;M$!SmzQQE50z*IF=^CTCsW?KDHxNt9>XNl2~4jA$?KP*Tiymf+U0FXvkiOY zWt7X?C>4G3Hm;VpiO?x8ll4rBZc3reh~11qHyf0*Z}7gFpY?)^qZEXtX_B`qF}sqp8@c&e7?HR8oV-1#u}7P{J*mHEOHy6|fd#|z7BIwKJ}A1^ zn7qB~g~_o0acikcVM-=;uLuYG1S40 z@(!WyA?@-?IF@kDlhiwu9)~4I!-G0R^DmN@=njfoV@A=%*Z=~VrNizaT#cKW&tQz!+K4>yt8P0 z){wljb1)(AoEmwxX`uPJ*&y!RF7E$%vApv}<(*F;k6Dk$tXJ2DNqH9#bOGlV(BMMi zF3bn(7ZF#_b$yq-i|f!Q?~-C(4f_ooFKw51d78Y&VR=_}%WGo2iM*>>zotmuwRvch z*KBkDo9pCVS1Ip$Hm>iJcS9-~<+a#ol6PYk2)-#Dy!xBVz`(5)7?a0mQ18}EOv}5i z7IX62XnZ^Sw~xrXqX4|9JFCDj?E>}hqR(APg6`^)cejs0c}v(>LeP>)c^xrG>gWKC z{+EIxQ1qTOuy;=zXn1dvyiV3S$-6HXt@7@t(fuXhRo~A54{-hf>kstG>ms&`UdgU5 zc@JiSrVo+u5JexFlK1eKyzXXskCcN3kG9EstPu0^dKkK=U*6*sdz{!OaxoxpX(fi` z^%j6@9>v~MD~-x~nw+P}?Q47b2Q;Is`osFo*$R@0z>h6)Ej7( z_aaSSBIe~jFvuWH2HAUsg0D=Dlouurd~cJ@9jp=Y@`Ik^4=*$ue^8Jf43GC<+1F&N3qdL zko$fvy5xNjqaM`$Farbf#^^D|+DFv*xEd?}{C`5iCmr&}^HGMC=M?<34(xwcD(`cl zRvwRKZ=xAA`hx2()6gRCt8A=1j>((MMVq{@ZSaD=@j>i2?ee~*&bQO@rWoQo&XeCU z>Gy2@P%3Y_M&6Gdn3DHX2}b4pTmg#C6k|Z%F9qPY^2><473BTO`7Ez$me}7o|Gih< zAH0x12Ib8$#Gjo1IVq3tu)V*jKTiygVDFzodH=R>|No`ge^qFeuWIzkH)1r%H+_uB zw`b)$o$}pW^vd^g&?etc0dfA6{GbHg@F4{8c!v!uqQD zNq$f`B^4~Du(?_>S}`ntb@tYvL28ZsHAm&IH7kGZ5&3CD^4FosdID?dSicJW@;4Zh zzac|x*oO)E8&zUT{>JU{Hz`4j{7hmq=j3miY?i+n(``O4KdVdr7Ik3KEeYl`roUAV zIM2?;$|H@oE&xTgo|L~$Ax7nITMv#odGdL3`aC)P?Rw>JpNUraJFvC`$J`9Cz9acN zQa`y+Q0Xcj2fFcFiV7*{g{sM+u z&?|p0Uew;1=#*dBCVwB!_nDNxZ!_lP?^i3os9Z^Zf63o}Q2qfV9+(4~6*q#QgJ^KD zk1F)bKO`NM=myP7O62n>^$#QFFn&9SO~^l-8i&_mTz+XWI^`b`qgDRGd{FmD1}sa% zAou^MQq0Idnxtb0JZ4IMc`g{Dd{F+enW)B){NpHgJn_d9Q^DRMiY%&^e?l9mablDF zN`~TT>Yr3D|70KJox*w*wX0g>pGv(`n=vQDKO+nM@)xtW zcv?P>R{zW{`FtJh*Nn(ND;pF#+eRy9ims=@mCwV04!$NmNB7?*z` zdl$~jzo>`%UtcNzVl-6Azmx`-Wyrrg1q59_B)^fwMuM-P(3PxT*(d)h2Dpl%P2@GP zcXgfoYZ&^PY5CWZ*US*jWnk#*YUE#Eh$;CzRs9<{wlv7Ukyn2c|3t*Ui2^)Q{hM3l z^GVfjW#bkSZsGh^j<*iUzpWfI@^3GYe+NZ*ocecGpcyo6ACrGq1{yFS|8C-!q=55| zBJ{}rAA9%2;QZcF`JFaOx&NIsxR0Rw8RC8lJ;3Gzz4E&<&@TT$jt_GEP%dcr(3Jd# z8SLRM`Q4f5lmAGS{6}--KStBXIPb{@wH~j=jQl4?i=4XX3%)nM*-^44|0Di0>yu~LE}FJhMyyDZcP54t(cMjS2Y;+Z{D%`e^c=9Df$19 z^e;{R<@!I){~J)CN{lHm%E6Hjq=7l2z{*!(XMh-|4IFto1};ah3_S|`QUyU8x-g|6 zEX1gSr~sV`VqZZruIFS=8XQxLF|J_E zW(8{zzt*IJwW~oMA3TG!J_YMA&^q%9)+L80WsuH#Is@>e3^uUAKM@Kxpuh$c+OSeV z28}YN6>QX@VB=H}yh)n^9+yETF`IJSbXdV=1sGJYc`^8HWR+t=!4}lmg5#F83bqo6 z$!=7zbtd{1Y}2V=+kC7%4k^gt`5)wTDA+C?Eef{hxP7~V9dbcHZjFK+8DvMA?o_W} z=K`?5^Q?kA2HPc8u&WQwcb!tOTcv`0uJg+WQf*_TqGOD&PZWaLEk6LSheok4cgii+)ne`rxo1Mui(yV3@d2w zQE*odSiiec!IByU9qC|z{}n2@havA_fO{JibQUPMkL&xI!P@<~ApQX#-3q$c>tg-E zQp_rNs6xTRKKK7&8g&!!2p(ngF&aEJsi22}dS(>x2n`;eSMWqFI4-SM(3^FLWqa z)}>%zTEUAHf2kZadYPgxvo_eL;FVm^^i`Vj2n}B2_!`$kG)4o5Z;Da0x^I-)# z6^z-a1Nk2np-sWZtbI(Aj|UZeQh;s+JY9oNvlV>C`e(BWKBw2`GYTfypXlKJf03c! z3j)84QG)>mUkRn?#H4~r2AS+p@O8d|Z(0<5OYFBapYp-+JFdTLSMYr{Mil%|hIs`) zHY@n4T*1#J3TCDi{6g-EOw1_wH648Z4}PWLuS_;e!&x?dt5@)Q3feKJ;Exdnb2Rxg z2Ym|uV&K1cHGi@H*SLbe3&8q(8G04`!?6E!EBH4b^9uf}!K6afU|ylosnASSXtgVJ zDw6ydSLoJZMxj@Q359+cMid71m{S;bDU5~`#!U(n*%(u}N+Z}?wN7D53fNnXp<~pw1S=Cbwkz zt+>c8Rk$@%Zq0<-P+(hrMLZ(IoEosUUA4mPQ_!z)hcbn^oac5a+%X5E3U_K$$kQ?0 znWj6pD9oc~-mt=5sJjctU0L6iI=d0a*Rf%K33&e(=1(i!o#5RE6z+jNXMu&!obAc{TeLDJ&wcs1>sc_wQABKpKcS&_|uZVjF!54=Pu9 za5_d69@3_;gnuF!mQeiAVvH+1EFVJ(4=)7Qr3_Zu4GJHThX$~}kirXj1q&&%khmkW zPy=d}rK1Tm3XduW@keKa*kc&vm}-zyUW6pYkEPjhJ}7WJ$BI}XkI-;Yqe32^;feDK zc~picw}U}WDFb;`8Q_(iT7qeXr%fq5o$Kleg=g^FTimJe%qE33wF=K7=IkPc=VU3Y z#ko0{;r^f3q40cOSzQ{sFpN2c7f}2H8eT}_3prktiZ+GyB-ghnyqNfl%g~Aug_l&J zpMu0E>%brlF_$n^br?%`UG1ud0y(jWlSa=oJKAQHg$q zSCY(!&+y7wg;(XG9t_)50QRr;QHCCc*QB6T;kDGgmRij=8Wdi~@j7~3Uk>WuP|W?m zfg&v=-bjHPDRfgR2)JoL;mw(-!=%Di3f+`-`LvQXju>7c;_LkhcCe2_*w9>a%v6h54# zu$$uD^9mm!@sU=AkCrQZjK)1axPF{Qk5ll85`{}`bSdnm@sl|iQTP<=PjxDMngO0B z_i1AL3cy6CRP_bF&Kj3GV0o`Bd~Pd?5ur3YT%cjQC}93I}R2 ztnft&zDTo|sxhYUWpZEUdXN|LN*aiNH5;P}U&}zH!Xe)Og+m0rUW@^SZ{#U_vr*yj zyu#%ae4FAU?7c&gcVkQ{d~ZnMXs^Qe^TGKCrI=OtVY9-qEDS6Bh`f*H6n@OG<2hjX zPpR{nj~WpFd6U8kzW)y=S`>ak;uj=-$>LWG@fFE@xD3B;SI84D{FbCChWM^r;rAt& zQusqXI8GOWQp;cS+|-$?$w zO5qgC+p=dpt zq|-2+0oG4JJ*E_Gn5`(IM$twbHyTy6aWPoWq;MwZn{wQ=9CL~`BbTRKwE4WEtO7J+ zTG19cXu_nTElbg(XsdJ#D9Wx+D%!eE(KcD2@V2dra!Npv?b;P>U#4h>Objc^ZB(=) zg?Czs^PLMplRO&aO)A=j*j;I`8^w3)Qk36J<}8w_@I8ljG_fiNKPo) zi$Z(RVDD;0g;f|+v`?L)eJQ+eucG}(+K;^=u8TZI91qO~FYGXZwd7$ViVkP-@D4?#OkPT%BRKM;i;iHpg`)eKeLrsxdz&gf9InDxaoiq2%OhVzy}3@B=; zRK$NbjBXZ)zqu20iugztwX%OpE|PpMrJ{Ria4)g~y2*@P)Y zJY=G$CKWy1pr|hfInVSedbUT=b2NIcLs37?`|0<5t)dr*d7+y7|H6c#Wo#_#Ry2@~ zE=4bL{bHY@muT=(o1&NVFsEpcMz2gO;sFx9#vns@y;IQ}4EttI(U&oZ|B^wzs#i40-sF&?ugfr{=o{)yWh(kE znWgA^0)CiR^y8qSpGFkT5c~^;SF|Ylm4>sUihdhY^artXC7{rs1t90IE=7OWDVitt zA7cI;SF8-hMv-E(U9r`y*dA8wOeps9(5cw3Qyf$%4hs|~BNihT<8;LdtdfZ)%qm{B z82ySnF8iKOD~+ZFF% zqYf0#W!l_1#XB`8n< ztat%+7cj_Pm5TRHMLlK}7d9#0hoSaO@?$@y-H&EPg_u#iKLrk;=m7-s_j+8Mk730J zG5tXlJDBT(i9dwALmI)*C8Zcud?+!8(fF`t#fPVY!Af({ruc{y(0C#H3wsqGSuUr*M@P$=Vzgs z`+q(O=ksdnXk16ZI)X1qM-{p;r})BrG=XLp#h@7vl=z|^#r3$jQSl|LUD65Sd0Q7J zslz9rxS;~n;W-muS`KPl#&CR37+;p`Q_QEI`0`1`S5zy$GEH$4*G&XAjVQjFp{}tp zp!iw_;Gq-q4_0CxI`MVcpz(DRimzwD>)XNp4F%{>+(KSU4cNc2Qt?evd^3BkS)lJN zB@B6MtoT-%-O9pk6u50(aa*LS5D87>dce2;s2AbT(-rW?xn_IM`OL0dY zW)%Og0PNkvE5A1loOkA9QZXNN;s>fQthkG~t_JRZ*O=l5xp=Ty@k7Fh;)hEWcl&5p z{0MuGQ25bS#gAp8NpTNxJr#-{C+2a6c%o48(niI-B^XfrBsotL)3*{0`xx?>A;r&9 z_c;db&rtk4xzEpV|6k}(ysSv^07YJ;z>5=#U#dlq;+F|}xk2$DUWvi+)eHJJALj}bpMtN0^+TOXHTMDZtSXi_|$4w`%_aQuwx&+0I$ z`12~vE1uxDG%>9BiyFmW5|{k4P4QQGpz&lDDD-s>c!l3k;G1^E-?GNjDxRY0)QsZq z$oalS@ee-O|6xw?bS1_W|5y!i8vn=P4p_U zN(x#qp~R{sAU7oqjhI$qwMxt>v3i>lYfvY(TZuI@!Evnuj482pAv%;uOQ!OpRf%=d z(WS(?Y^+P*dL*njsYH6866=>?UWpAFmDrHD4H+mSMx_!PvA=P>5}OPuu_UcIG#-b3J-6qeLFZU7D5HmE7F~hRUbz?wv~PLEfI!C@28m z|0N2>lvq#+8tm1i#NPQBP@=E`?Cq0l@et(rj>W>&x;#6v$mJjw%C$E~=>LdYYlq<2A=8I{>C!EBY4NBCM{GZ0o1}w_!%H!vq zfti7sdzgXwaDe$@7zSsMk6{KzK%6KssJO8kvFt8&jhbC6Ze!FWCLtMYOtp!PXc0`K zElE&OOp0mDkZ4od7$u2mX~e|s+TC>5O{sCSrgghEG1C7#pkRhPPey+8&U-)3IrrXk z&$%!2@CeZG9yDCLh{QS=vu=>Yy|DbgQzVeR#QnQTY=F=WT_ljK!~?qk>K>c|)`R0D z9?Avy{Lm1IW_)hm1cpiAS9jv!UabEkX#5BSN71n zxbDXNo>~$=MBR_{Bsy{JME_@oNIbg|TqW^bABnCtB=(|yZztA&A3E$q<-RST8w>zg zxE~Gn2LXh4CjoRmuz|!u2t|?-JoAuo5Tr}oj~6cXcIjd zA#t)3!1{p|0D|9y@;3)aywwO`#M@B#b{n`%;#4g_`*(uiIEh~_0+&eq3Q7hc3y)di z*SI2~iFcv!^b`Q2@uij+(t~^eW6q!+Uus3gdtD^nN8$Upp6w^`uf^aPiQjA`@o(!% zoI|H`he-SuM*Q|1iGSZj;y-ZzK{n_n@jDp%pBm6e;=htW8;SGhN&NRB03{#7m=8~p z_&v&pdrAD^0}>Y+0InYyvHl;eBylkax=8$w5kT?BxPA;p|EmM&d?}a2CotrbS`vSh zfuKKOWPfS_!z3=R0GCPpUk!;rH9B(COz z=v5M*A0u&X4T+HgaGscU6XW@fr7OgWZerykaGW>^*Q6oh+x9+*@g&l)NqM7y^k1~ij7N&O+kQ4^HyR@ z73d2S((r z0OyFkD=Ere2+eOMo(h3eTZw&j#C~-4uLfs{3u-_w@w9BPmADXfh3f$X6rrx@7;!O- zE{2klDgYTJ9}ovH`XJf{At#8>p$)`kFtQAN%b>Fy<>i;7#NlS*>3ZUd0x&>aiH21W zT-8lHBOB}{uGRsxtA?PN7l|WRh;OL{E5H^2Aqi07hwUKE1obrH{RAg)72T{rQ98Zb!wH&A#xge*(~=)(|71@!y{!{)Y|3jT+EPjKs#@=*Rjmfs%h* zM7*>L3=rRmj(7GEFKZ!Q4nvkh@d|vtYazHqjAuChXS82A1*`yvz*XXJ<^o(-L1|M2 zpx^2);(rMeBf0Unb`#$X8FzOQuQ7u4#P{IufA}6)zP6uu-6jA5>x;ozVq`kL7eemC z{e8QL@2>)vi8r)@OT-UAz=IgYgBOV($_8lv5M(vO*oVUaeIIcGw14CZM!FiHzNHzQ zBHoDp8wauecoySFH6Q}EfC1u7bzm#;V?hArj|~!k2lty%zWEUG76{$~L0e$RcN;-J z@#Fab*C#NLC!la^3m7K;-co>>cybCrpC_*pZ-bm|C~HOh=T->WLBx1AV`M*WLx;8x zh=^>R#LpIk zZsO-$)$~)Pt zNjtB7yt_Jpn z(OepGnab^w$;gslf>AkzsIQjFC$MVO0mTd-+Sk`(n>oX|N^jBJXWHAF zV7d>x^^1;ba);bsvgaiK;z(0dAkkrZ7B614%8=lxvH9l|*N#NTHCSa|8Fg2A%LVhi z@|eGp%gzb-LsJbU*%Q6C&g2MD|f5pZHB#NQAw!g#om?q0O&lB;O(#`PN8xc1%_Kp1Hk^|9^5!ngia}lIFu$RZDKpyPa#CQ=c zXGpRyn@Uaa;T)AsawBS%p%6jTBL`5MWHeVwAveOkTdpV#!QWA+a<^QcC3x!S#HDIe zbk=mI(;7`rw>q8aR$Yqr+J)BkN@@$PiFTe|w#;5rvjo_eB^q-jUTcrn#!kJP zoIb=OBO~u%I@|H=Vxh7Q=d-FTyF7;~wHyTCP9`IQ<-{%Den*P&>s7w#7NaLJKjKNM z8JnyH(<&PJ!}-~kS@v0J;e}y8G7WxO;7ys+(zJOq^YEkBkMC+Z3S!g~^xqcOUz8An zDif`UFSKl=w@fx@$@-h4PUMDJa5A#eR#}$iSZx*`74kuO$K+9yQvEw&pwY_qZQ_71z zpEo5fMdcHEO{!L_(fJg)74C$y%Z`phcu;J%MV<^XYa`rD(mnVi9d5_6S zqDT?9Dor7Kh`F?R*t)~1%O2-5u(AcmNgGwj@zBwuu|?(Siw9*y6*>Ogw2h=Q8fMKZ`3<|ol#!W% zJSnL{x%pvJ&xH6>FEsUXqk4^SS{x3G(uS$oi- z`a=FQZa#ce8#{bdH$2~}k&lk}&vbW^p3K;>IXF7{sF3D%L_X8!D>GbD$Gsn>IXWIZ zsmjazSV5n!Le$Zz(>YmV&9(Y)%t}=`RjD0s9!;HUlNMe(<2Pj*w4AC+706Qt+13nB z$<_$kyjCLsgS6OMjsC1PcP#nZ$xHZU&!oZ0+tFEVFgH#Toj%tgSEi>wFnWnvw_6h; z(-Z?5SPci{cGh$$b9*M?s63YRHDtgh^HZe5In!W`P8J#EXfVD?V~-baCFrf4c<>l~ zz>5clo1QjC5^PDDa3rxA@f90;{D?j5);Dfc&ZfGKb6xs~P?Uv&MG`iC8P1Iz zQ}^nprX?pUs*;jgR4IxS9q3rHS6|>(N>xdkWDTY~1wSN@cr4az`=w>pDk|Ma@y+h=td6Kk(Nou0V|=i|QunE7k# diff --git a/sbapp/kivymd/icon_definitions.py b/sbapp/kivymd/icon_definitions.py index 3941eae..0b9425a 100755 --- a/sbapp/kivymd/icon_definitions.py +++ b/sbapp/kivymd/icon_definitions.py @@ -12,7 +12,7 @@ Themes/Icon Definitions List of icons from materialdesignicons.com. These expanded material design icons are maintained by Austin Andrews (Templarian on Github). -LAST UPDATED: Version 7.0.96 +LAST UPDATED: Version 7.1.96 To preview the icons and their names, you can use the following application: ---------------------------------------------------------------------------- @@ -242,6 +242,8 @@ md_icons = { "account-switch-outline": "\U000F04CB", "account-sync": "\U000F191B", "account-sync-outline": "\U000F191C", + "account-tag": "\U000F1C1B", + "account-tag-outline": "\U000F1C1C", "account-tie": "\U000F0CE3", "account-tie-hat": "\U000F1898", "account-tie-hat-outline": "\U000F1899", @@ -774,6 +776,7 @@ md_icons = { "audio-video": "\U000F093D", "audio-video-off": "\U000F11B6", "augmented-reality": "\U000F0850", + "aurora": "\U000F1BB9", "auto-download": "\U000F137E", "auto-fix": "\U000F0068", "auto-upload": "\U000F0069", @@ -852,6 +855,8 @@ md_icons = { "bandage": "\U000F0DAF", "bank": "\U000F0070", "bank-check": "\U000F1655", + "bank-circle": "\U000F1C03", + "bank-circle-outline": "\U000F1C04", "bank-minus": "\U000F0DB0", "bank-off": "\U000F1656", "bank-off-outline": "\U000F1657", @@ -1443,6 +1448,8 @@ md_icons = { "camera-image": "\U000F08CC", "camera-iris": "\U000F0104", "camera-lock": "\U000F1A14", + "camera-lock-open": "\U000F1C0D", + "camera-lock-open-outline": "\U000F1C0E", "camera-lock-outline": "\U000F1A15", "camera-marker": "\U000F19A7", "camera-marker-outline": "\U000F19A8", @@ -1707,6 +1714,7 @@ md_icons = { "chart-multiline": "\U000F08D4", "chart-multiple": "\U000F1213", "chart-pie": "\U000F012B", + "chart-pie-outline": "\U000F1BDF", "chart-ppf": "\U000F1380", "chart-sankey": "\U000F11DF", "chart-sankey-variant": "\U000F11E0", @@ -1978,22 +1986,53 @@ md_icons = { "closed-caption-outline": "\U000F0DBD", "cloud": "\U000F015F", "cloud-alert": "\U000F09E0", + "cloud-alert-outline": "\U000F1BE0", + "cloud-arrow-down": "\U000F1BE1", + "cloud-arrow-down-outline": "\U000F1BE2", + "cloud-arrow-left": "\U000F1BE3", + "cloud-arrow-left-outline": "\U000F1BE4", + "cloud-arrow-right": "\U000F1BE5", + "cloud-arrow-right-outline": "\U000F1BE6", + "cloud-arrow-up": "\U000F1BE7", + "cloud-arrow-up-outline": "\U000F1BE8", "cloud-braces": "\U000F07B5", - "cloud-check": "\U000F0160", - "cloud-check-outline": "\U000F12CC", + "cloud-cancel": "\U000F1BE9", + "cloud-cancel-outline": "\U000F1BEA", + "cloud-check": "\U000F1BEB", + "cloud-check-outline": "\U000F1BEC", + "cloud-check-variant": "\U000F0160", + "cloud-check-variant-outline": "\U000F12CC", "cloud-circle": "\U000F0161", + "cloud-circle-outline": "\U000F1BED", + "cloud-clock": "\U000F1BEE", + "cloud-clock-outline": "\U000F1BEF", + "cloud-cog": "\U000F1BF0", + "cloud-cog-outline": "\U000F1BF1", "cloud-download": "\U000F0162", "cloud-download-outline": "\U000F0B7D", "cloud-lock": "\U000F11F1", + "cloud-lock-open": "\U000F1BF2", + "cloud-lock-open-outline": "\U000F1BF3", "cloud-lock-outline": "\U000F11F2", + "cloud-minus": "\U000F1BF4", + "cloud-minus-outline": "\U000F1BF5", + "cloud-off": "\U000F1BF6", "cloud-off-outline": "\U000F0164", "cloud-outline": "\U000F0163", "cloud-percent": "\U000F1A35", "cloud-percent-outline": "\U000F1A36", + "cloud-plus": "\U000F1BF7", + "cloud-plus-outline": "\U000F1BF8", "cloud-print": "\U000F0165", "cloud-print-outline": "\U000F0166", "cloud-question": "\U000F0A39", - "cloud-refresh": "\U000F052A", + "cloud-question-outline": "\U000F1BF9", + "cloud-refresh": "\U000F1BFA", + "cloud-refresh-outline": "\U000F1BFB", + "cloud-refresh-variant": "\U000F052A", + "cloud-refresh-variant-outline": "\U000F1BFC", + "cloud-remove": "\U000F1BFD", + "cloud-remove-outline": "\U000F1BFE", "cloud-search": "\U000F0956", "cloud-search-outline": "\U000F0957", "cloud-sync": "\U000F063F", @@ -2311,6 +2350,7 @@ md_icons = { "currency-rub": "\U000F01B1", "currency-rupee": "\U000F1976", "currency-sign": "\U000F07BE", + "currency-thb": "\U000F1C05", "currency-try": "\U000F01B2", "currency-twd": "\U000F07BF", "currency-uah": "\U000F1B9B", @@ -2750,6 +2790,10 @@ md_icons = { "eye-check-outline": "\U000F0D05", "eye-circle": "\U000F0B94", "eye-circle-outline": "\U000F0B95", + "eye-lock": "\U000F1C06", + "eye-lock-open": "\U000F1C07", + "eye-lock-open-outline": "\U000F1C08", + "eye-lock-outline": "\U000F1C09", "eye-minus": "\U000F1026", "eye-minus-outline": "\U000F1027", "eye-off": "\U000F0209", @@ -2858,6 +2902,8 @@ md_icons = { "file-document": "\U000F0219", "file-document-alert": "\U000F1A97", "file-document-alert-outline": "\U000F1A98", + "file-document-arrow-right": "\U000F1C0F", + "file-document-arrow-right-outline": "\U000F1C10", "file-document-check": "\U000F1A99", "file-document-check-outline": "\U000F1A9A", "file-document-edit": "\U000F0DC8", @@ -3731,6 +3777,9 @@ md_icons = { "helicopter": "\U000F0AC2", "help": "\U000F02D6", "help-box": "\U000F078B", + "help-box-multiple": "\U000F1C0A", + "help-box-multiple-outline": "\U000F1C0B", + "help-box-outline": "\U000F1C0C", "help-circle": "\U000F02D7", "help-circle-outline": "\U000F0625", "help-network": "\U000F06F5", @@ -3907,6 +3956,7 @@ md_icons = { "image-filter-center-focus-strong-outline": "\U000F0F00", "image-filter-center-focus-weak": "\U000F02F2", "image-filter-drama": "\U000F02F3", + "image-filter-drama-outline": "\U000F1BFF", "image-filter-frames": "\U000F02F4", "image-filter-hdr": "\U000F02F5", "image-filter-none": "\U000F02F6", @@ -4021,6 +4071,7 @@ md_icons = { "keyboard-backspace": "\U000F030D", "keyboard-caps": "\U000F030E", "keyboard-close": "\U000F030F", + "keyboard-close-outline": "\U000F1C00", "keyboard-esc": "\U000F12B7", "keyboard-f1": "\U000F12AB", "keyboard-f10": "\U000F12B4", @@ -4263,6 +4314,12 @@ md_icons = { "lock-open-variant-outline": "\U000F0FC7", "lock-outline": "\U000F0341", "lock-pattern": "\U000F06EA", + "lock-percent": "\U000F1C12", + "lock-percent-open": "\U000F1C13", + "lock-percent-open-outline": "\U000F1C14", + "lock-percent-open-variant": "\U000F1C15", + "lock-percent-open-variant-outline": "\U000F1C16", + "lock-percent-outline": "\U000F1C17", "lock-plus": "\U000F05FB", "lock-plus-outline": "\U000F16B2", "lock-question": "\U000F08EF", @@ -5072,6 +5129,7 @@ md_icons = { "pencil-remove": "\U000F0DED", "pencil-remove-outline": "\U000F0DEE", "pencil-ruler": "\U000F1353", + "pencil-ruler-outline": "\U000F1C11", "penguin": "\U000F0EC0", "pentagon": "\U000F0701", "pentagon-outline": "\U000F0700", @@ -5309,6 +5367,41 @@ md_icons = { "printer-off-outline": "\U000F1785", "printer-outline": "\U000F1786", "printer-pos": "\U000F1057", + "printer-pos-alert": "\U000F1BBC", + "printer-pos-alert-outline": "\U000F1BBD", + "printer-pos-cancel": "\U000F1BBE", + "printer-pos-cancel-outline": "\U000F1BBF", + "printer-pos-check": "\U000F1BC0", + "printer-pos-check-outline": "\U000F1BC1", + "printer-pos-cog": "\U000F1BC2", + "printer-pos-cog-outline": "\U000F1BC3", + "printer-pos-edit": "\U000F1BC4", + "printer-pos-edit-outline": "\U000F1BC5", + "printer-pos-minus": "\U000F1BC6", + "printer-pos-minus-outline": "\U000F1BC7", + "printer-pos-network": "\U000F1BC8", + "printer-pos-network-outline": "\U000F1BC9", + "printer-pos-off": "\U000F1BCA", + "printer-pos-off-outline": "\U000F1BCB", + "printer-pos-outline": "\U000F1BCC", + "printer-pos-pause": "\U000F1BCD", + "printer-pos-pause-outline": "\U000F1BCE", + "printer-pos-play": "\U000F1BCF", + "printer-pos-play-outline": "\U000F1BD0", + "printer-pos-plus": "\U000F1BD1", + "printer-pos-plus-outline": "\U000F1BD2", + "printer-pos-refresh": "\U000F1BD3", + "printer-pos-refresh-outline": "\U000F1BD4", + "printer-pos-remove": "\U000F1BD5", + "printer-pos-remove-outline": "\U000F1BD6", + "printer-pos-star": "\U000F1BD7", + "printer-pos-star-outline": "\U000F1BD8", + "printer-pos-stop": "\U000F1BD9", + "printer-pos-stop-outline": "\U000F1BDA", + "printer-pos-sync": "\U000F1BDB", + "printer-pos-sync-outline": "\U000F1BDC", + "printer-pos-wrench": "\U000F1BDD", + "printer-pos-wrench-outline": "\U000F1BDE", "printer-search": "\U000F1457", "printer-settings": "\U000F0707", "printer-wireless": "\U000F0A0B", @@ -5497,7 +5590,10 @@ md_icons = { "remote-off": "\U000F0EC4", "remote-tv": "\U000F0EC5", "remote-tv-off": "\U000F0EC6", + "rename": "\U000F1C18", "rename-box": "\U000F0455", + "rename-box-outline": "\U000F1C19", + "rename-outline": "\U000F1C1A", "reorder-horizontal": "\U000F0688", "reorder-vertical": "\U000F0689", "repeat": "\U000F0456", @@ -5566,8 +5662,10 @@ md_icons = { "robot-outline": "\U000F167A", "robot-vacuum": "\U000F070D", "robot-vacuum-alert": "\U000F1B5D", + "robot-vacuum-off": "\U000F1C01", "robot-vacuum-variant": "\U000F0908", "robot-vacuum-variant-alert": "\U000F1B5E", + "robot-vacuum-variant-off": "\U000F1C02", "rocket": "\U000F0463", "rocket-launch": "\U000F14DE", "rocket-launch-outline": "\U000F14DF", @@ -6605,6 +6703,8 @@ md_icons = { "tooltip-outline": "\U000F0526", "tooltip-plus": "\U000F0BD6", "tooltip-plus-outline": "\U000F0527", + "tooltip-question": "\U000F1BBA", + "tooltip-question-outline": "\U000F1BBB", "tooltip-remove": "\U000F1560", "tooltip-remove-outline": "\U000F1561", "tooltip-text": "\U000F0528", @@ -7219,3 +7319,94 @@ md_icons = { "zodiac-virgo": "\U000F0A88", "blank": " ", } + + +if __name__ == "__main__": + from kivy.lang import Builder + from kivy.properties import StringProperty + from kivy.uix.screenmanager import Screen + + from kivymd.app import MDApp + from kivymd.uix.list import OneLineIconListItem + + Builder.load_string( + """ +#:import images_path kivymd.images_path + + + + + IconLeftWidget: + icon: root.icon + + + + + MDBoxLayout: + orientation: 'vertical' + spacing: dp(10) + padding: dp(20) + + MDBoxLayout: + adaptive_height: True + + MDIconButton: + icon: 'magnify' + + MDTextField: + id: search_field + hint_text: 'Search icon' + on_text: root.set_list_md_icons(self.text, True) + + RecycleView: + id: rv + key_viewclass: 'viewclass' + key_size: 'height' + + RecycleBoxLayout: + padding: dp(10) + default_size: None, dp(48) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: 'vertical' + """ + ) + + class CustomOneLineIconListItem(OneLineIconListItem): + icon = StringProperty() + + class PreviousMDIcons(Screen): + def set_list_md_icons(self, text="", search=False): + """Builds a list of icons for the screen MDIcons.""" + + def add_icon_item(name_icon): + self.ids.rv.data.append( + { + "viewclass": "CustomOneLineIconListItem", + "icon": name_icon, + "text": name_icon, + "callback": lambda x: x, + } + ) + + self.ids.rv.data = [] + for name_icon in md_icons.keys(): + if search: + if text in name_icon: + add_icon_item(name_icon) + else: + add_icon_item(name_icon) + + class MainApp(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = PreviousMDIcons() + + def build(self): + return self.screen + + def on_start(self): + self.screen.set_list_md_icons() + + MainApp().run() diff --git a/sbapp/kivymd/material_resources.py b/sbapp/kivymd/material_resources.py index 0a1b811..2eb6626 100755 --- a/sbapp/kivymd/material_resources.py +++ b/sbapp/kivymd/material_resources.py @@ -35,4 +35,30 @@ else: PORTRAIT_TOOLBAR_HEIGHT = STANDARD_INCREMENT LANDSCAPE_TOOLBAR_HEIGHT = STANDARD_INCREMENT +# Elevation. +SEGMENT_CONTROL_SEGMENT_SWITCH_ELEVATION = 1 +FILE_MANAGER_TOP_APP_BAR_ELEVATION = 1 +FLOATING_ACTION_BUTTON_M2_ELEVATION = 1 +FLOATING_ACTION_BUTTON_M3_ELEVATION = 0.5 +CARD_STYLE_ELEVATED_M3_ELEVATION = 0.5 +CARD_STYLE_OUTLINED_FILLED_M3_ELEVATION = 0 +DATA_TABLE_ELEVATION = 4 +DROP_DOWN_MENU_ELEVATION = 2 +TOP_APP_BAR_ELEVATION = 2 +SNACK_BAR_ELEVATION = 2 + +# Shadow softness. +RAISED_BUTTON_SOFTNESS = 4 +FLOATING_ACTION_BUTTON_M3_SOFTNESS = 0 +DATA_TABLE_SOFTNESS = 12 +DROP_DOWN_MENU_SOFTNESS = 6 + +# Shadow offset. +RAISED_BUTTON_OFFSET = (0, -2) +FLOATING_ACTION_BUTTON_M2_OFFSET = (0, -1) +FLOATING_ACTION_BUTTON_M3_OFFSET = (0, -2) +DATA_TABLE_OFFSET = (0, -2) +DROP_DOWN_MENU_OFFSET = (0, -2) +SNACK_BAR_OFFSET = (0, -2) + TOUCH_TARGET_HEIGHT = dp(48) diff --git a/sbapp/kivymd/tests/base_test.py b/sbapp/kivymd/tests/base_test.py deleted file mode 100644 index 9fa52ef..0000000 --- a/sbapp/kivymd/tests/base_test.py +++ /dev/null @@ -1,9 +0,0 @@ -from kivy.tests.common import GraphicUnitTest - -from kivymd.app import MDApp - - -class BaseTest(GraphicUnitTest): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.app = MDApp() # NOQA diff --git a/sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py b/sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py deleted file mode 100644 index 3b81fdf..0000000 --- a/sbapp/kivymd/tests/pyinstaller/test_pyinstaller_packaging.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -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 "logo" in images -assert "alpha_layer.png" in images -assert "black.png" in images -assert "blue.png" in images -assert "red.png" in images -assert "green.png" in images -assert "yellow.png" in images -assert "folder.png" in images -assert "transparent.png" in images -""" - ) - pyi_main.run( - [ - "--workpath", - str(workpath), - "--distpath", - str(distpath), - "--specpath", - str(tmp_path), - str(app), - ] - ) - subprocess.run([str(distpath / app_name / app_name)], check=True) - - -def test_widgets(tmp_path) -> None: - """Test that all widgets are accesible.""" - - app_name = "userapp" - workpath = tmp_path / "build" - distpath = tmp_path / "dist" - app = tmp_path / (app_name + ".py") - app.write_text( - """ -import os - -import kivymd # NOQA -__import__("kivymd.uix.label") -__import__("kivymd.uix.button") -__import__("kivymd.uix.list") -__import__("kivymd.uix.navigationdrawer") - -print(os.listdir(os.path.dirname(kivymd.uix.__path__[0]))) -""" - ) - pyi_main.run( - [ - "--workpath", - str(workpath), - "--distpath", - str(distpath), - "--specpath", - str(tmp_path), - str(app), - ] - ) - subprocess.run([str(distpath / app_name / app_name)], check=True) diff --git a/sbapp/kivymd/tests/test_app.py b/sbapp/kivymd/tests/test_app.py deleted file mode 100644 index 076e9dd..0000000 --- a/sbapp/kivymd/tests/test_app.py +++ /dev/null @@ -1,21 +0,0 @@ -from kivy import lang -from kivy.clock import Clock -from kivy.tests.common import GraphicUnitTest - -from kivymd.app import MDApp -from kivymd.theming import ThemeManager - - -class AppTest(GraphicUnitTest): - def test_start_raw_app(self): - lang._delayed_start = None - a = MDApp() - Clock.schedule_once(a.stop, 0.1) - a.run() - - def test_theme_manager_existance(self): - lang._delayed_start = None - a = MDApp() - Clock.schedule_once(a.stop, 0.1) - a.run() - assert isinstance(a.theme_cls, ThemeManager) diff --git a/sbapp/kivymd/tests/test_backdrop.py b/sbapp/kivymd/tests/test_backdrop.py deleted file mode 100644 index 85b1c85..0000000 --- a/sbapp/kivymd/tests/test_backdrop.py +++ /dev/null @@ -1,24 +0,0 @@ -from kivymd.tests.base_test import BaseTest - - -class BackdropTest(BaseTest): - def test_backdrop_raw_app(self): - from kivymd.uix.backdrop import MDBackdrop - from kivymd.uix.backdrop.backdrop import ( - MDBackdropBackLayer, - MDBackdropFrontLayer, - ) - from kivymd.uix.screen import MDScreen - from kivymd.uix.widget import MDWidget - - self.render( - MDScreen( - MDBackdrop( - MDBackdropBackLayer(MDWidget()), - MDBackdropFrontLayer(MDWidget()), - id="backdrop", - title="Example Backdrop", - header_text="Menu:", - ) - ) - ) diff --git a/sbapp/kivymd/tests/test_bottom_navigation.py b/sbapp/kivymd/tests/test_bottom_navigation.py deleted file mode 100644 index 9b3467c..0000000 --- a/sbapp/kivymd/tests/test_bottom_navigation.py +++ /dev/null @@ -1,32 +0,0 @@ -from kivymd.tests.base_test import BaseTest - - -class BottomNavigationTest(BaseTest): - def test_bottom_navigation_m3_style_raw_app(self): - from kivymd.uix.bottomnavigation import ( - MDBottomNavigation, - MDBottomNavigationItem, - ) - from kivymd.uix.screen import MDScreen - - self.app.theme_cls.material_style = "M3" - self.render( - MDScreen( - MDBottomNavigation( - MDBottomNavigationItem( - name="screen 1", - text="Mail", - icon="gmail", - ), - MDBottomNavigationItem( - name="screen 2", - text="Twitter", - icon="twitter", - badge_icon="numeric-10", - ), - panel_color="#eeeaea", - selected_color_background="#97ecf8", - text_color_active="red", - ) - ) - ) diff --git a/sbapp/kivymd/tests/test_card.py b/sbapp/kivymd/tests/test_card.py deleted file mode 100644 index 81dd0dd..0000000 --- a/sbapp/kivymd/tests/test_card.py +++ /dev/null @@ -1,25 +0,0 @@ -from kivymd.tests.base_test import BaseTest - - -class CardTest(BaseTest): - def test_card_m3_style_raw_app(self): - from kivymd.uix.behaviors import RoundedRectangularElevationBehavior - from kivymd.uix.card import MDCard - from kivymd.uix.screen import MDScreen - - class MD3Card(MDCard, RoundedRectangularElevationBehavior): - pass - - self.app.theme_cls.material_style = "M3" - self.render( - MDScreen( - MD3Card( - size_hint=(None, None), - pos_hint={"center_x": 0.5, "center_y": 0.5}, - size=("200dp", "100dp"), - line_color=(0.2, 0.2, 0.2, 0.8), - style="elevated", - md_bg_color="lightblue", - ) - ) - ) diff --git a/sbapp/kivymd/tests/test_chip.py b/sbapp/kivymd/tests/test_chip.py deleted file mode 100644 index 6f30f67..0000000 --- a/sbapp/kivymd/tests/test_chip.py +++ /dev/null @@ -1,16 +0,0 @@ -from kivymd.tests.base_test import BaseTest - - -class ChipTest(BaseTest): - def test_chip_raw_app(self): - from kivymd.uix.chip import MDChip - from kivymd.uix.screen import MDScreen - - self.render( - MDScreen( - MDChip( - text="Portland", - pos_hint={"center_x": 0.5, "center_y": 0.5}, - ) - ) - ) diff --git a/sbapp/kivymd/tests/test_create_project.py b/sbapp/kivymd/tests/test_create_project.py deleted file mode 100644 index 31265fa..0000000 --- a/sbapp/kivymd/tests/test_create_project.py +++ /dev/null @@ -1,15 +0,0 @@ -def test_create_project(): - import os - - os.system( - f"python3.10 -m kivymd.tools.patterns.create_project " - f"MVC " - f"{os.path.expanduser('~')} " - f"TestProject " - f"python3.10 " - f"stable " - f"--name_screen TestProjectScreen " - f"--name_database restdb " - f"--use_hotreload yes" - ) - assert os.path.exists(os.path.join(os.path.expanduser("~"), "TestProject")) diff --git a/sbapp/kivymd/tests/test_fitimage.py b/sbapp/kivymd/tests/test_fitimage.py deleted file mode 100644 index de159de..0000000 --- a/sbapp/kivymd/tests/test_fitimage.py +++ /dev/null @@ -1,24 +0,0 @@ -from kivymd.tests.base_test import BaseTest - - -class FitImageTest(BaseTest): - def test_fitimage_raw_app(self): - import os - - from kivymd import images_path - from kivymd.uix.fitimage import FitImage - from kivymd.uix.screen import MDScreen - - self.render( - MDScreen( - FitImage( - source=os.path.join( - images_path, "logo", "kivymd-icon-512.png" - ), - size_hint=(0.5, 0.5), - pos_hint={"center_x": 0.5, "center_y": 0.5}, - radius=[36, 36, 0, 0], - mipmap=True, - ) - ) - ) diff --git a/sbapp/kivymd/tests/test_font_definitions.py b/sbapp/kivymd/tests/test_font_definitions.py deleted file mode 100644 index adf8e75..0000000 --- a/sbapp/kivymd/tests/test_font_definitions.py +++ /dev/null @@ -1,16 +0,0 @@ -def test_fonts_registration(): - # This should register fonts: - from kivy.core.text import LabelBase - - import kivymd # NOQA - - fonts = [ - "Roboto", - "RobotoThin", - "RobotoLight", - "RobotoMedium", - "RobotoBlack", - "Icons", - ] - for font in fonts: - assert font in LabelBase._fonts.keys() diff --git a/sbapp/kivymd/tests/test_icon_definitions.py b/sbapp/kivymd/tests/test_icon_definitions.py deleted file mode 100644 index d1f6bab..0000000 --- a/sbapp/kivymd/tests/test_icon_definitions.py +++ /dev/null @@ -1,10 +0,0 @@ -def test_icons_have_size(): - from kivy.core.text import Label - - from kivymd.icon_definitions import md_icons - - lbl = Label(font_name="Icons") - for icon_name, icon_value in md_icons.items(): - assert len(icon_value) == 1 - lbl.refresh() - assert lbl.get_extents(icon_value) is not None diff --git a/sbapp/kivymd/tests/test_imagelist.py b/sbapp/kivymd/tests/test_imagelist.py deleted file mode 100644 index 4ca989e..0000000 --- a/sbapp/kivymd/tests/test_imagelist.py +++ /dev/null @@ -1,39 +0,0 @@ -from kivymd.tests.base_test import BaseTest - - -class ImageListTest(BaseTest): - def test_imagelist_raw_app(self): - import os - - from kivymd import images_path - from kivymd.uix.button import MDIconButton - from kivymd.uix.imagelist import MDSmartTile - from kivymd.uix.label import MDLabel - from kivymd.uix.screen import MDScreen - - self.render( - MDScreen( - MDSmartTile( - MDIconButton( - icon="heart-outline", - theme_icon_color="Custom", - icon_color="red", - pos_hint={"center_y": 0.5}, - ), - MDLabel( - text="Julia and Julie", - bold=True, - color="white", - ), - radius=24, - box_radius=[0, 0, 24, 24], - box_color="grey", - source=os.path.join( - images_path, "logo", "kivymd-icon-512.png" - ), - pos_hint={"center_x": 0.5, "center_y": 0.5}, - size_hint=(None, None), - size=("320dp", "320dp"), - ) - ) - ) diff --git a/sbapp/kivymd/tests/test_list.py b/sbapp/kivymd/tests/test_list.py deleted file mode 100644 index da5a045..0000000 --- a/sbapp/kivymd/tests/test_list.py +++ /dev/null @@ -1,67 +0,0 @@ -from kivymd.tests.base_test import BaseTest - - -class ListTest(BaseTest): - def test_list_raw_app(self): - import os - - from kivymd import images_path - from kivymd.uix.list import ( - IconLeftWidget, - IconRightWidget, - ImageLeftWidget, - IRightBodyTouch, - MDList, - OneLineAvatarIconListItem, - OneLineAvatarListItem, - OneLineIconListItem, - OneLineListItem, - ThreeLineListItem, - TwoLineListItem, - ) - from kivymd.uix.screen import MDScreen - from kivymd.uix.scrollview import MDScrollView - from kivymd.uix.selectioncontrol import MDCheckbox - - class RightCheckbox(IRightBodyTouch, MDCheckbox): - pass - - self.render( - MDScreen( - MDScrollView( - MDList( - OneLineListItem(text="Text"), - TwoLineListItem( - text="Text", secondary_text="secondary text" - ), - ThreeLineListItem( - text="Text", - secondary_text="secondary text", - tertiary_text="tertiary text", - ), - OneLineAvatarListItem( - ImageLeftWidget( - source=os.path.join( - images_path, "logo", "kivymd-icon-512.png" - ) - ), - text="Text", - ), - OneLineIconListItem( - IconLeftWidget(icon="plus"), - text="Text", - ), - OneLineAvatarIconListItem( - IconLeftWidget(icon="plus"), - IconRightWidget(icon="minus"), - text="Text", - ), - OneLineAvatarIconListItem( - IconLeftWidget(icon="plus"), - RightCheckbox(), - text="Text", - ), - ) - ) - ) - ) diff --git a/sbapp/kivymd/tests/test_navigationdrawer.py b/sbapp/kivymd/tests/test_navigationdrawer.py deleted file mode 100644 index 87039c2..0000000 --- a/sbapp/kivymd/tests/test_navigationdrawer.py +++ /dev/null @@ -1,94 +0,0 @@ -from kivymd.tests.base_test import BaseTest - - -class NavigationDrawerTest(BaseTest): - def test_navigationdrawer_raw_app(self): - from kivymd.uix.navigationdrawer import ( - MDNavigationDrawer, - MDNavigationDrawerDivider, - MDNavigationDrawerHeader, - MDNavigationDrawerItem, - MDNavigationDrawerLabel, - MDNavigationDrawerMenu, - MDNavigationLayout, - ) - from kivymd.uix.screen import MDScreen - from kivymd.uix.screenmanager import MDScreenManager - from kivymd.uix.toolbar import MDTopAppBar - - class DrawerClickableItem(MDNavigationDrawerItem): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.focus_color = "#e7e4c0" - self.unfocus_color = "#f7f4e7" - self.text_color = "#4a4939" - self.icon_color = "#4a4939" - self.ripple_color = "#c5bdd2" - self.selected_color = "#0c6c4d" - - class DrawerLabelItem(MDNavigationDrawerItem): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.bg_color = "#f7f4e7" - self.text_color = "#4a4939" - self.icon_color = "#4a4939" - _no_ripple_effect = True # NOQA - - self.app.theme_cls.material_style = "M3" - self.render( - MDNavigationLayout( - MDScreenManager( - MDScreen( - MDTopAppBar( - title="Navigation Drawer", - elevation=10, - pos_hint={"top": 1}, - md_bg_color="#e7e4c0", - specific_text_color="#4a4939", - left_action_items=[ - ["menu", lambda x: self.nav_drawer_open()] - ], - ) - ) - ), - MDNavigationDrawer( - MDNavigationDrawerMenu( - MDNavigationDrawerHeader( - title="Header title", - title_color="#4a4939", - text="Header text", - spacing="4dp", - padding=("12dp", 0, 0, "56dp"), - ), - MDNavigationDrawerLabel( - text="Mail", - ), - DrawerClickableItem( - icon="gmail", - right_text="+99", - text_right_color="#4a4939", - text="Inbox", - radius=24, - ), - DrawerClickableItem( - icon="send", - text="Outbox", - radius=24, - ), - MDNavigationDrawerDivider(), - MDNavigationDrawerLabel( - text="Labels", - ), - DrawerLabelItem( - icon="information-outline", - text="Label", - ), - DrawerLabelItem( - icon="information-outline", - text="Label", - ), - ), - id="nav_drawer", - ), - ) - ) diff --git a/sbapp/kivymd/tests/test_tab.py b/sbapp/kivymd/tests/test_tab.py deleted file mode 100644 index 1ad5920..0000000 --- a/sbapp/kivymd/tests/test_tab.py +++ /dev/null @@ -1,14 +0,0 @@ -from kivymd.tests.base_test import BaseTest - - -class TabTest(BaseTest): - def test_tab_raw_app(self): - from kivymd.uix.floatlayout import MDFloatLayout - from kivymd.uix.tab import MDTabs, MDTabsBase - - class Tab(MDFloatLayout, MDTabsBase): - pass - - tab = MDTabs() - tab.add_widget(Tab(title="Tab")) - self.render(tab) diff --git a/sbapp/kivymd/tests/test_textfield.py b/sbapp/kivymd/tests/test_textfield.py deleted file mode 100644 index b5654e1..0000000 --- a/sbapp/kivymd/tests/test_textfield.py +++ /dev/null @@ -1,72 +0,0 @@ -# from kivy.clock import Clock -# from kivy.uix.textinput import TextInput - -from kivymd.tests.base_test import BaseTest - - -class TextFieldTest(BaseTest): - def test_textfield_raw_app(self): - from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.uix.button import MDFlatButton - from kivymd.uix.screen import MDScreen - from kivymd.uix.textfield import MDTextField - - # def set_text(): - # for widget in self.screen.ids.box.children: - # if issubclass(widget.__class__, TextInput): - # widget.text = "Input text" - - self.render( - MDScreen( - MDBoxLayout( - MDTextField( - hint_text="Label", - helper_text="Error massage", - mode="rectangle", - max_text_length=5, - ), - MDTextField( - icon_left="git", - hint_text="Label", - helper_text="Error massage", - mode="rectangle", - ), - MDTextField( - icon_left="git", - hint_text="Label", - helper_text="Error massage", - mode="fill", - ), - MDTextField( - hint_text="Label", - helper_text="Error massage", - mode="fill", - ), - MDTextField( - hint_text="Label", - helper_text="Error massage", - ), - MDTextField( - icon_left="git", - hint_text="Label", - helper_text="Error massage", - ), - MDTextField( - hint_text="Round mode", - mode="round", - max_text_length=15, - helper_text="Massage", - ), - MDFlatButton( - text="SET TEXT", - pos_hint={"center_x": 0.5}, - ), - id="box", - orientation="vertical", - spacing="20dp", - adaptive_height=True, - size_hint_x=0.8, - pos_hint={"center_x": 0.5, "center_y": 0.5}, - ) - ) - ) diff --git a/sbapp/kivymd/theming.py b/sbapp/kivymd/theming.py index 7bc4116..0c89bcb 100755 --- a/sbapp/kivymd/theming.py +++ b/sbapp/kivymd/theming.py @@ -606,13 +606,16 @@ class ThemeManager(EventDispatcher): readonly. """ - material_style = OptionProperty("M2", options=["M2", "M3"]) + material_style = OptionProperty("M3", options=["M2", "M3"]) """ Material design style. Available options are: 'M2', 'M3'. .. versionadded:: 1.0.0 + .. versionchanged:: 1.2.0 + By default now `'M3'`. + .. seealso:: `Material Design 2 `_ and @@ -620,7 +623,7 @@ class ThemeManager(EventDispatcher): :attr:`material_style` is an :class:`~kivy.properties.OptionProperty` - and defaults to `'M2'`. + and defaults to `'M3'`. """ theme_style_switch_animation = BooleanProperty(False) @@ -647,9 +650,8 @@ class ThemeManager(EventDispatcher): padding: 0, 0, 0 , "36dp" size_hint: .5, .5 pos_hint: {"center_x": .5, "center_y": .5} - elevation: 4 - shadow_radius: 6 - shadow_offset: 0, 2 + elevation: 2 + shadow_offset: 0, -2 MDLabel: text: "Theme style - {}".format(app.theme_cls.theme_style) @@ -720,9 +722,8 @@ class ThemeManager(EventDispatcher): padding=(0, 0, 0, "36dp"), size_hint=(0.5, 0.5), pos_hint={"center_x": 0.5, "center_y": 0.5}, - elevation=4, - shadow_radius=6, - shadow_offset=(0, 2), + elevation=2, + shadow_offset=(0, -2), ) ) ) @@ -1665,25 +1666,60 @@ class ThemableBehavior(EventDispatcher): "https://github.com/kivymd/KivyMD/wiki/Modules-Material-App#exceptions" ) self.theme_cls = App.get_running_app().theme_cls + super().__init__(**kwargs) - # def dec_disabled(self, *args, **kwargs) -> None: - # callabacks = self.theme_cls.get_property_observers("theme_style") + # Fix circular imports. + from kivymd.uix.behaviors import CommonElevationBehavior + from kivymd.uix.label import MDLabel + from kivymd.uix.textfield import MDTextField - # for callaback in callabacks: - # try: - # if hasattr(callaback, "proxy") and hasattr( - # callaback.proxy, "theme_cls" - # ): - # for property_name in self.unbind_properties: - # self.theme_cls.unbind( - # **{ - # property_name: getattr( - # callaback.proxy, callaback.method_name - # ) - # } - # ) - # except ReferenceError: - # pass + self.common_elevation_behavior = CommonElevationBehavior + self.md_label = MDLabel + self.md_textfield = MDTextField - # super().dec_disabled(*args, **kwargs) + def remove_widget(self, widget) -> None: + if not hasattr(widget, "theme_cls"): + super().remove_widget(widget) + return + + callbacks = widget.theme_cls.get_property_observers("theme_style") + + for callback in callbacks: + try: + if hasattr(callback, "proxy") and hasattr( + callback.proxy, "theme_cls" + ): + if issubclass(widget.__class__, self.md_textfield): + widget.theme_cls.unbind( + **{ + "theme_style": getattr( + callback.proxy, callback.method_name + ) + } + ) + for property_name in self.unbind_properties: + if widget == callback.proxy: + widget.theme_cls.unbind( + **{ + property_name: getattr( + callback.proxy, callback.method_name + ) + } + ) + # KivyMD widgets may contain other MD widgets. + for children in widget.children: + if hasattr(children, "theme_cls"): + self.remove_widget(children) + except ReferenceError: + pass + + # Canceling a scheduled method call on_window_touch for MDLabel + # objects. + if ( + issubclass(widget.__class__, self.md_label) + and self.md_label.allow_selection + ): + Window.unbind(on_touch_down=widget.on_window_touch) + + super().remove_widget(widget) diff --git a/sbapp/kivymd/toast/kivytoast/kivytoast.py b/sbapp/kivymd/toast/kivytoast/kivytoast.py index 87645bc..c190666 100755 --- a/sbapp/kivymd/toast/kivytoast/kivytoast.py +++ b/sbapp/kivymd/toast/kivytoast/kivytoast.py @@ -82,7 +82,7 @@ class Toast(BaseDialog): def __init__(self, **kwargs): super().__init__(**kwargs) - self.label_toast = Label(size_hint=(None, None), opacity=0) + self.label_toast = Label(size_hint=(None, None), markup=True, opacity=0) self.label_toast.bind(texture_size=self.label_check_texture_size) self.add_widget(self.label_toast) diff --git a/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py b/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py index 040d103..d2cb9a5 100644 --- a/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py +++ b/sbapp/kivymd/tools/packaging/pyinstaller/hook-kivymd.py @@ -13,18 +13,6 @@ from pathlib import Path import kivymd datas = [ - # Add `.frag` files from the `kivymd/data/glsl/elevation` directory. - ( - str(Path(kivymd.glsl_path).joinpath("elevation")) + os.sep, - str( - Path("kivymd").joinpath( - str(Path(kivymd.glsl_path)).split(str(Path("kivymd")) + os.sep)[ - 1 - ] - + f"{os.sep}elevation" - ) - ), - ), # Add `.ttf` files from the `kivymd/fonts` directory. ( kivymd.fonts_path, diff --git a/sbapp/kivymd/tools/patterns/create_project.py b/sbapp/kivymd/tools/patterns/create_project.py index 64487b0..864ed55 100644 --- a/sbapp/kivymd/tools/patterns/create_project.py +++ b/sbapp/kivymd/tools/patterns/create_project.py @@ -381,13 +381,12 @@ class {name_screen}Controller: temp_base_screen = '''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): +class BaseScreenView(MDScreen, Observer): """ A base class that implements a visual representation of the model data. The view class must be inherited from this class. diff --git a/sbapp/kivymd/uix/__init__.py b/sbapp/kivymd/uix/__init__.py index 2d48cc1..9aec2dc 100755 --- a/sbapp/kivymd/uix/__init__.py +++ b/sbapp/kivymd/uix/__init__.py @@ -59,6 +59,8 @@ class MDAdaptiveWidget(SpecificBackgroundColorBehavior): else: if not isinstance(self, (FloatLayout, Screen)): self.bind(minimum_height=self.setter("height")) + if not self.children: + self.height = 0 def on_adaptive_width(self, md_widget, value: bool) -> None: self.size_hint_x = None @@ -71,6 +73,8 @@ class MDAdaptiveWidget(SpecificBackgroundColorBehavior): else: if not isinstance(self, (FloatLayout, Screen)): self.bind(minimum_width=self.setter("width")) + if not self.children: + self.width = 0 def on_adaptive_size(self, md_widget, value: bool) -> None: self.size_hint = (None, None) @@ -84,3 +88,5 @@ class MDAdaptiveWidget(SpecificBackgroundColorBehavior): else: if not isinstance(self, (FloatLayout, Screen)): self.bind(minimum_size=self.setter("size")) + if not self.children: + self.size = (0, 0) diff --git a/sbapp/kivymd/uix/anchorlayout.py b/sbapp/kivymd/uix/anchorlayout.py index 4e2be4a..fe43542 100644 --- a/sbapp/kivymd/uix/anchorlayout.py +++ b/sbapp/kivymd/uix/anchorlayout.py @@ -33,11 +33,14 @@ __all__ = ("MDAnchorLayout",) from kivy.uix.anchorlayout import AnchorLayout +from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior -class MDAnchorLayout(DeclarativeBehavior, AnchorLayout, MDAdaptiveWidget): +class MDAnchorLayout( + DeclarativeBehavior, ThemableBehavior, AnchorLayout, MDAdaptiveWidget +): """ Anchor layout class. For more information, see in the :class:`~kivy.uix.anchorlayout.AnchorLayout` class documentation. diff --git a/sbapp/kivymd/uix/backdrop/backdrop.py b/sbapp/kivymd/uix/backdrop/backdrop.py index 604ab13..276eb1c 100644 --- a/sbapp/kivymd/uix/backdrop/backdrop.py +++ b/sbapp/kivymd/uix/backdrop/backdrop.py @@ -201,7 +201,6 @@ from kivy.properties import ( from kivy.uix.boxlayout import BoxLayout from kivymd import uix_path -from kivymd.theming import ThemableBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.card import MDCard from kivymd.uix.floatlayout import MDFloatLayout @@ -214,8 +213,11 @@ with open( Builder.load_string(kv_file.read()) -class MDBackdrop(MDFloatLayout, ThemableBehavior): +class MDBackdrop(MDFloatLayout): """ + For more information, see in the + :class:`~kivymd.uix.floatlayout.MDFloatLayout` class documentation. + :Events: :attr:`on_open` When the front layer drops. @@ -277,7 +279,7 @@ class MDBackdrop(MDFloatLayout, ThemableBehavior): back_layer_color = ColorProperty(None) """ - Background color of back layer. + Background color of back layer in (r, g, b, a) or string format. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-back-layer-color.png :align: center @@ -288,7 +290,7 @@ class MDBackdrop(MDFloatLayout, ThemableBehavior): front_layer_color = ColorProperty(None) """ - Background color of front layer. + Background color of front layer in (r, g, b, a) or string format. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/backdrop-front-layer-color.png :align: center @@ -512,15 +514,30 @@ class MDBackdrop(MDFloatLayout, ThemableBehavior): class MDBackdropToolbar(MDTopAppBar): - """Implements a toolbar for back content.""" + """ + Implements a toolbar for back content. + + For more information, see in the + :class:`~kivymd.uix.toolbar.toolbar.MDTopAppBar` classes documentation. + """ class MDBackdropFrontLayer(MDBoxLayout): - """Container for front content.""" + """ + Container for front content. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` classes documentation. + """ class MDBackdropBackLayer(MDBoxLayout): - """Container for back content.""" + """ + Container for back content. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. + """ class _BackLayer(BoxLayout): diff --git a/sbapp/kivymd/uix/banner/banner.py b/sbapp/kivymd/uix/banner/banner.py index fb87221..534783c 100644 --- a/sbapp/kivymd/uix/banner/banner.py +++ b/sbapp/kivymd/uix/banner/banner.py @@ -177,6 +177,13 @@ with open( class MDBanner(MDCard): + """ + Banner class. + + For more information, see in the :class:`~kivymd.uix.card.MDCard` + class documentation. + """ + vertical_pad = NumericProperty(dp(68)) """ Indent the banner at the top of the screen. diff --git a/sbapp/kivymd/uix/behaviors/__init__.py b/sbapp/kivymd/uix/behaviors/__init__.py index e89ed10..d80d8c1 100755 --- a/sbapp/kivymd/uix/behaviors/__init__.py +++ b/sbapp/kivymd/uix/behaviors/__init__.py @@ -20,6 +20,11 @@ from .elevation import ( RectangularElevationBehavior, RoundedRectangularElevationBehavior, ) +from .motion_behavior import ( + MotionDialogBehavior, + MotionShackBehavior, + MotionDropDownMenuBehavior, +) from .magic_behavior import MagicBehavior from .ripple_behavior import CircularRippleBehavior, RectangularRippleBehavior from .rotate_behavior import RotateBehavior diff --git a/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py b/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py index 4c4649b..a24166d 100755 --- a/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py +++ b/sbapp/kivymd/uix/behaviors/backgroundcolor_behavior.py @@ -5,9 +5,9 @@ Behaviors/Background Color .. note:: The following classes are intended for in-house use of the library. """ -__all__ = ("BackgroundColorBehavior", "SpecificBackgroundColorBehavior") +from __future__ import annotations -from typing import List, Union +__all__ = ("BackgroundColorBehavior", "SpecificBackgroundColorBehavior") from kivy.animation import Animation from kivy.lang import Builder @@ -49,6 +49,8 @@ Builder.load_string( source: root.background Color: rgba: self.line_color if self.line_color else (0, 0, 0, 0) + # TODO: maybe we should use SmoothLine, + # but this should be tested on all widgets. Line: width: root.line_width rounded_rectangle: @@ -58,7 +60,6 @@ Builder.load_string( self.width, \ self.height, \ *self.radius, \ - 100, \ ] PopMatrix """, @@ -90,6 +91,8 @@ class BackgroundColorBehavior: and defaults to `[0, 0, 0, 0]`. """ + # FIXME: in this case, we will not be able to animate this property + # using the `Animation` class. md_bg_color = ColorProperty([1, 1, 1, 0]) """ The background color of the widget (:class:`~kivy.uix.widget.Widget`) @@ -154,12 +157,34 @@ class BackgroundColorBehavior: _background_y = NumericProperty(0) _background_origin = ReferenceListProperty(_background_x, _background_y) _md_bg_color = ColorProperty([0, 0, 0, 0]) + _origin_line_color = ColorProperty(None) + _origin_md_bg_color = ColorProperty(None) def __init__(self, **kwarg): super().__init__(**kwarg) - self.bind(pos=self.update_background_origin) + self.bind( + pos=self.update_background_origin, + disabled=self.restore_color_origin, + ) + + def restore_color_origin(self, instance_md_widget, value: bool) -> None: + """Called when the values of :attr:`disabled` change.""" + + if not value: + if self._origin_line_color: + self.line_color = self._origin_line_color + if self._origin_md_bg_color: + self.md_bg_color = self._origin_md_bg_color + + def on_line_color(self, instance_md_widget, value: list | str) -> None: + """Called when the values of :attr:`line_color` change.""" + + if not self.disabled: + self._origin_line_color = value + + def on_md_bg_color(self, instance_md_widget, color: list | str): + """Called when the values of :attr:`md_bg_color` change.""" - def on_md_bg_color(self, instance_md_widget, color: Union[list, str]): if ( hasattr(self, "theme_cls") and self.theme_cls.theme_style_switch_animation @@ -172,9 +197,12 @@ class BackgroundColorBehavior: else: self._md_bg_color = color - def update_background_origin( - self, instance_md_widget, pos: List[float] - ) -> None: + if not self.disabled: + self._origin_md_bg_color = color + + def update_background_origin(self, instance_md_widget, pos: list) -> None: + """Called when the values of :attr:`pos` change.""" + if self.background_origin: self._background_origin = self.background_origin else: diff --git a/sbapp/kivymd/uix/behaviors/elevation.py b/sbapp/kivymd/uix/behaviors/elevation.py index 615edac..f081d5c 100755 --- a/sbapp/kivymd/uix/behaviors/elevation.py +++ b/sbapp/kivymd/uix/behaviors/elevation.py @@ -41,8 +41,9 @@ For example, let's create a button with a rectangular elevation effect: # With elevation effect RectangularElevationButton: pos_hint: {"center_x": .5, "center_y": .6} - elevation: 4.5 - shadow_offset: 0, 6 + elevation: 4 + shadow_offset: 0, -6 + shadow_softness: 4 # Without elevation effect RectangularElevationButton: @@ -102,8 +103,9 @@ For example, let's create a button with a rectangular elevation effect: MDScreen( RectangularElevationButton( pos_hint={"center_x": .5, "center_y": .6}, - elevation=4.5, - shadow_offset=(0, 6), + elevation=4, + shadow_softness=4, + shadow_offset=(0, -6), ), RectangularElevationButton( pos_hint={"center_x": .5, "center_y": .4}, @@ -164,6 +166,7 @@ Similarly, create a circular button: CircularElevationButton: pos_hint: {"center_x": .5, "center_y": .6} elevation: 4 + shadow_softness: 4 ''' @@ -231,6 +234,7 @@ Similarly, create a circular button: CircularElevationButton( pos_hint={"center_x": .5, "center_y": .5}, elevation=4, + shadow_softness=4, ) ) ) @@ -266,7 +270,7 @@ Animating the elevation size_hint: None, None size: 100, 100 md_bg_color: 0, 0, 1, 1 - elevation: 4 + elevation: 2 radius: 18 ''' @@ -336,7 +340,7 @@ Animating the elevation size_hint=(None, None), size=(100, 100), md_bg_color="blue", - elevation=4, + elevation=2, radius=18, ) ) @@ -360,32 +364,62 @@ __all__ = ( "FakeCircularElevationBehavior", ) -import os - from kivy import Logger -from kivy.clock import Clock -from kivy.core.window import Window -from kivy.graphics import RenderContext, RoundedRectangle +from kivy.lang import Builder from kivy.properties import ( - AliasProperty, - BooleanProperty, BoundedNumericProperty, ColorProperty, ListProperty, NumericProperty, - ObjectProperty, VariableListProperty, ) from kivy.uix.widget import Widget -from kivymd import glsl_path -from kivymd.app import MDApp +Builder.load_string( + """ + + canvas.before: + PushMatrix + Scale: + x: self.scale_value_x + y: self.scale_value_y + z: self.scale_value_x + origin: + self.center \ + if not self.scale_value_center else \ + self.scale_value_center + Rotate: + angle: self.rotate_value_angle + axis: tuple(self.rotate_value_axis) + origin: self.center + Color: + rgba: + (0, 0, 0, 0) \ + if self.disabled or not self.elevation else \ + root.shadow_color + BoxShadow: + pos: self.pos + size: self.size + offset: root.shadow_offset + spread_radius: -(root.shadow_softness), -(root.shadow_softness) + blur_radius: root.elevation * 10 + border_radius: + (root.radius if hasattr(self, "radius") else [0, 0, 0, 0]) \ + if root.shadow_radius == [0.0, 0.0, 0.0, 0.0] else \ + root.shadow_radius + canvas.after: + PopMatrix +""" +) -# FIXME: Add shadow manipulation with canvas instructions such as -# PushMatrix and PopMatrix. class CommonElevationBehavior(Widget): - """Common base class for rectangular and circular elevation behavior.""" + """ + Common base class for rectangular and circular elevation behavior. + + For more information, see in the :class:`~kivy.uix.widget.Widget` + class documentation. + """ elevation = BoundedNumericProperty(0, min=0, errorvalue=0) """ @@ -418,9 +452,9 @@ class CommonElevationBehavior(Widget): radius: 12, 46, 12, 46 size_hint: .5, .3 pos_hint: {"center_x": .5, "center_y": .5} - elevation: 4 - shadow_softness: 8 - shadow_offset: (-2, 2) + elevation: 2 + shadow_softness: 4 + shadow_offset: (2, -2) ''' @@ -434,21 +468,11 @@ class CommonElevationBehavior(Widget): .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-radius.png :align: center - .. note:: - However, if you want to use this parameter, remember that the angle - values for the radius of the Kivy widgets and the radius for the shader - are different. - - .. code-block:: python - - shadow_radius = ['top-right', 'bot-right', 'top-left', 'bot-left'] - kivy_radius = ['top-left', 'top-right', 'bottom-right', 'bottom-left'] - :attr:`shadow_radius` is an :class:`~kivy.properties.VariableListProperty` and defaults to `[0, 0, 0, 0]`. """ - shadow_softness = NumericProperty(12) + shadow_softness = NumericProperty(0.0) """ Softness of the shadow. @@ -482,7 +506,9 @@ class CommonElevationBehavior(Widget): class RectangularElevationButton(CommonElevationBehavior, BackgroundColorBehavior): - md_bg_color = [0, 0, 1, 1] + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.md_bg_color = "blue" class Example(MDApp): @@ -499,7 +525,19 @@ class CommonElevationBehavior(Widget): and defaults to `12`. """ - shadow_offset = ListProperty((0, 2)) + shadow_softness_size = BoundedNumericProperty(2, min=2, deprecated=True) + """ + The value of the softness of the shadow. + + .. versionadded:: 1.1.0 + + .. deprecated:: 1.2.0 + + :attr:`shadow_softness_size` is an :class:`~kivy.properties.NumericProperty` + and defaults to `2`. + """ + + shadow_offset = ListProperty((0, 0)) """ Offset of the shadow. @@ -523,14 +561,16 @@ class CommonElevationBehavior(Widget): RectangularElevationButton: pos_hint: {"center_x": .5, "center_y": .5} elevation: 6 - shadow_radius: 18 - shadow_softness: 24 - shadow_offset: 12, 12 + shadow_radius: 6 + shadow_softness: 12 + shadow_offset: -12, -12 ''' class RectangularElevationButton(CommonElevationBehavior, BackgroundColorBehavior): - md_bg_color = [0, 0, 1, 1] + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.md_bg_color = "blue" class Example(MDApp): @@ -546,7 +586,7 @@ class CommonElevationBehavior(Widget): .. code-block:: kv RectangularElevationButton: - shadow_offset: -12, 12 + shadow_offset: 12, -12 .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-2.png :align: center @@ -554,7 +594,7 @@ class CommonElevationBehavior(Widget): .. code-block:: kv RectangularElevationButton: - shadow_offset: -12, -12 + shadow_offset: 12, 12 .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-3.png :align: center @@ -562,13 +602,13 @@ class CommonElevationBehavior(Widget): .. code-block:: kv RectangularElevationButton: - shadow_offset: 12, -12 + shadow_offset: -12, 12 .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadow-offset-4.png :align: center :attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty` - and defaults to `(0, 2)`. + and defaults to `(0, 0)`. """ shadow_color = ColorProperty([0, 0, 0, 0.6]) @@ -586,252 +626,75 @@ class CommonElevationBehavior(Widget): :align: center :attr:`shadow_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[0.4, 0.4, 0.4, 0.8]`. + and defaults to `[0, 0, 0, 0.6]`. + """ + + scale_value_x = NumericProperty(1) + """ + X-axis value. + + .. versionadded:: 1.2.0 + + :attr:`scale_value_x` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + scale_value_y = NumericProperty(1) + """ + Y-axis value. + + .. versionadded:: 1.2.0 + + :attr:`scale_value_y` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + scale_value_z = NumericProperty(1) + """ + Z-axis value. + + .. versionadded:: 1.2.0 + + :attr:`scale_value_z` is an :class:`~kivy.properties.NumericProperty` + and defaults to `1`. + """ + + scale_value_center = ListProperty() + """ + Origin of the scale. + + .. versionadded:: 1.2.0 + + The format of the origin can be either (x, y) or (x, y, z). + + :attr:`scale_value_center` is an :class:`~kivy.properties.NumericProperty` + and defaults to `[]`. + """ + + rotate_value_angle = NumericProperty(0) + """ + Property for getting/setting the angle of the rotation. + + .. versionadded:: 1.2.0 + + :attr:`rotate_value_angle` is an :class:`~kivy.properties.NumericProperty` + and defaults to `0`. + """ + + rotate_value_axis = ListProperty((0, 0, 1)) + """ + Property for getting/setting the axis of the rotation. + + .. versionadded:: 1.2.0 + + :attr:`rotate_value_axis` is an :class:`~kivy.properties.ListProperty` + and defaults to `(0, 0, 1)`. """ - _transition_ref = ObjectProperty() - _has_relative_position = BooleanProperty(defaultvalue=False) _elevation = 0 - _shadow_color = [0.0, 0.0, 0.0, 0.0] - - def _get_window_pos(self, *args): - window_pos = self.to_window(*self.pos) - # To list, so it can be compared to self.pos directly. - return [window_pos[0], window_pos[1]] - - def _set_window_pos(self, value): - self.window_pos = value - - window_pos = AliasProperty(_get_window_pos, _set_window_pos) - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - if hasattr(MDApp.get_running_app(), "shaders_disabled") and MDApp.get_running_app().shaders_disabled: - self.shaders_disabled = True - else: - self.shaders_disabled = False - - with self.canvas.before: - self.context = RenderContext(use_parent_projection=True) - with self.context: - if self.shaders_disabled: - self.rect = None - del self.rect - else: - self.rect = RoundedRectangle(pos=self.pos, size=self.size) - - self.after_init() - - def after_init(self, *args): - Clock.schedule_once(self.check_for_relative_behavior) - if not self.shaders_disabled: - Clock.schedule_once(self.set_shader_string) - Clock.schedule_once(lambda x: self.on_elevation(self, self.elevation)) - self.on_pos() - - def check_for_relative_behavior(self, *args) -> None: - """ - Checks if the widget has relative properties and if necessary - binds Window.on_draw and screen events to fix behavior - """ - - if self.pos != self.window_pos: - self._has_relative_position = True - - # Loops to check if its inside screenmanager or bottom_navigation. - widget = self - while True: - # Checks if has screen event function - # works for Screen and MDTab objects. - if hasattr(widget, "on_pre_enter"): - widget.bind(on_pre_enter=self.apply_correction) - widget.bind(on_pre_leave=self.apply_correction) - widget.bind(on_enter=self.reset_correction) - widget.bind(on_leave=self.reset_correction) - self._has_relative_position = True - - # Save refs to objects with transition property. - if hasattr(widget, "header"): # specific to bottom_nav - self._transition_ref = widget.header.panel - elif hasattr(widget, "manager"): # specific to screen - if widget.manager: # manager cant be None - self._transition_ref = widget.manager - break - - elif widget.parent and str(widget) != str(widget.parent): - widget = widget.parent - else: - break - - if self._has_relative_position: - Window.bind(on_draw=self.update_window_position) - - def apply_correction(self, *args): - if self._transition_ref: - transition = str(self._transition_ref.transition) - # Slide and Card transitions only need _has_relative_pos to be - # always on. - if ( - "SlideTransition" in transition - or "CardTransition" in transition - ): - self.context.use_parent_modelview = False - else: - self.context.use_parent_modelview = True - - def reset_correction(self, *args): - self.context.use_parent_modelview = False - self.update_window_position() - - def get_shader_string(self) -> str: - shader_string = "" - for name_file in ["header.frag", "elevation.frag", "main.frag"]: - with open( - os.path.join(glsl_path, "elevation", name_file), - encoding="utf-8", - ) as file: - shader_string += f"{file.read()}\n\n" - - return shader_string - - def set_shader_string(self, *args) -> None: - self.context["shadow_radius"] = list(map(float, self.shadow_radius)) - self.context["shadow_softness"] = float(self.shadow_softness) - self.context["shadow_color"] = list(map(float, self.shadow_color))[ - :-1 - ] + [float(self.opacity)] - self.context["pos"] = list(map(float, self.rect.pos)) - self.context.shader.fs = self.get_shader_string() - - def update_resolution(self) -> None: - self.context["resolution"] = (*self.rect.size, *self.rect.pos) - - def on_shadow_color(self, instance, value) -> None: - def on_shadow_color(*args): - self._shadow_color = list(map(float, value))[:-1] + [ - float(self.opacity) if not self.disabled else 0 - ] - self.context["shadow_color"] = self._shadow_color - - Clock.schedule_once(on_shadow_color) - - def on_shadow_radius(self, instance, value) -> None: - def on_shadow_radius(*args): - if hasattr(self, "context"): - self.context["shadow_radius"] = list(map(float, value)) - - Clock.schedule_once(on_shadow_radius) - - def on_shadow_softness(self, instance, value) -> None: - def on_shadow_softness(*args): - if hasattr(self, "context"): - self.context["shadow_softness"] = float(value) - - Clock.schedule_once(on_shadow_softness) def on_elevation(self, instance, value) -> None: - def on_elevation(*args): - if hasattr(self, "context"): - self._elevation = value - self.hide_elevation( - True if (value <= 0 or self.disabled) else False - ) - - Clock.schedule_once(on_elevation) - - def on_shadow_offset(self, instance, value) -> None: - self.on_size() - self.on_pos() - - def update_window_position(self, *args) -> None: - """ - This function is used only when the widget has relative position - properties. - """ - - self.on_pos() - - def on_pos(self, *args) -> None: - if not hasattr(self, "rect"): - return - - if ( - self._has_relative_position - and not self.context.use_parent_modelview - ): - pos = self.window_pos - else: - pos = self.pos - - self.rect.pos = [ - pos[0] - - ((self.rect.size[0] - self.width) / 2) - - self.shadow_offset[0], - pos[1] - - ((self.rect.size[1] - self.height) / 2) - - self.shadow_offset[1], - ] - - self.context["mouse"] = [self.rect.pos[0], 0.0, 0.0, 0.0] - self.context["pos"] = list(map(float, self.rect.pos)) - self.update_resolution() - - def on_size(self, *args) -> None: - if not hasattr(self, "rect"): - return - - # If the elevation value is 0, set the canvas size to zero. - # Because even with a zero elevation value, the shadow is displayed - # under the widget. This is visible if we change the scale - # of the widget. - width = self.size[0] if self.elevation else 0 - height = self.size[1] if self.elevation else 0 - self.rect.size = ( - width + (self._elevation * self.shadow_softness / 2), - height + (self._elevation * self.shadow_softness / 2), - ) - - self.context["mouse"] = [self.rect.pos[0], 0.0, 0.0, 0.0] - self.context["size"] = list(map(float, self.rect.size)) - self.update_resolution() - - def on_opacity(self, instance, value: int | float) -> None: - """ - Adjusts the transparency of the shadow according to the transparency - of the widget. - """ - - def on_opacity(*args): - self._shadow_color = list(map(float, self._shadow_color))[:-1] + [ - float(value) - ] - self.context["shadow_color"] = self._shadow_color - - super().on_opacity(instance, value) - Clock.schedule_once(on_opacity) - - def on_radius(self, instance, value) -> None: - self.shadow_radius = [value[1], value[2], value[0], value[3]] - - def on_disabled(self, instance, value) -> None: - if value: - self._elevation = 0 - self.hide_elevation(True) - else: - self.hide_elevation(False) - - def hide_elevation(self, hide: bool) -> None: - if hide: - self._elevation = -self.elevation - self._shadow_color = [0.0, 0.0, 0.0, 0.0] - else: - self._elevation = self.elevation - self._shadow_color = self.shadow_color[:-1] + [float(self.opacity)] - - self.on_shadow_color(self, self._shadow_color) - self.on_size() - self.on_pos() + self._elevation = value class RectangularElevationBehavior(CommonElevationBehavior): diff --git a/sbapp/kivymd/uix/behaviors/focus_behavior.py b/sbapp/kivymd/uix/behaviors/focus_behavior.py index b90f16a..33e00bd 100644 --- a/sbapp/kivymd/uix/behaviors/focus_behavior.py +++ b/sbapp/kivymd/uix/behaviors/focus_behavior.py @@ -15,8 +15,9 @@ Usage from kivy.lang import Builder from kivymd.app import MDApp - from kivymd.uix.behaviors import RectangularElevationBehavior, FocusBehavior + from kivymd.uix.behaviors import RectangularElevationBehavior from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.behaviors.focus_behavior import FocusBehavior KV = ''' MDScreen: @@ -72,6 +73,18 @@ from kivymd.uix.behaviors import HoverBehavior class FocusBehavior(HoverBehavior, ButtonBehavior): + """ + Focus behavior class. + + For more information, see in the :class:`~kivymd.uix.behavior.HoverBehavior` + and :class:`~kivy.uix.button.ButtonBehavior` classes documentation. + + :Events: + :attr:`on_enter` + Called when mouse enters the bbox of the widget AND the widget is visible + :attr:`on_leave` + Called when the mouse exits the widget AND the widget is visible + """ focus_behavior = BooleanProperty(True) """ diff --git a/sbapp/kivymd/uix/behaviors/hover_behavior.py b/sbapp/kivymd/uix/behaviors/hover_behavior.py index 40f519e..dbe1ce2 100644 --- a/sbapp/kivymd/uix/behaviors/hover_behavior.py +++ b/sbapp/kivymd/uix/behaviors/hover_behavior.py @@ -11,13 +11,13 @@ In `KV file`: .. code-block:: kv - + In `python file`: .. code-block:: python - class HoverItem(MDBoxLayout, ThemableBehavior, HoverBehavior): + class HoverItem(MDBoxLayout, HoverBehavior): '''Custom item implementing hover behavior.''' After creating a class, you must define two methods for it: @@ -38,7 +38,6 @@ the widget. from kivymd.app import MDApp from kivymd.uix.behaviors import HoverBehavior from kivymd.uix.boxlayout import MDBoxLayout - from kivymd.theming import ThemableBehavior KV = ''' Screen @@ -51,7 +50,7 @@ the widget. ''' - class HoverItem(MDBoxLayout, ThemableBehavior, HoverBehavior): + class HoverItem(MDBoxLayout, HoverBehavior): '''Custom item implementing hover behavior.''' def on_enter(self, *args): diff --git a/sbapp/kivymd/uix/behaviors/magic_behavior.py b/sbapp/kivymd/uix/behaviors/magic_behavior.py index 9e7f862..8f7e998 100644 --- a/sbapp/kivymd/uix/behaviors/magic_behavior.py +++ b/sbapp/kivymd/uix/behaviors/magic_behavior.py @@ -118,7 +118,6 @@ Builder.load_string( class MagicBehavior: - magic_speed = NumericProperty(1) """ Animation playback speed. diff --git a/sbapp/kivymd/uix/behaviors/motion_behavior.py b/sbapp/kivymd/uix/behaviors/motion_behavior.py new file mode 100644 index 0000000..45a8e4b --- /dev/null +++ b/sbapp/kivymd/uix/behaviors/motion_behavior.py @@ -0,0 +1,287 @@ +""" +Behaviors/Motion +================ + +.. rubric:: Use motion to make a UI expressive and easy to use. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/motion.png + :align: center + +.. versionadded:: 1.2.0 + +Classes of the `Motion` type implement the display behavior of widgets such +as dialogs, dropdown menu, snack bars, and so on. +""" + +__all__ = ( + "MotionBase", + "MotionDropDownMenuBehavior", + "MotionDialogBehavior", + "MotionShackBehavior", +) + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.core.window import Window +from kivy.properties import StringProperty, NumericProperty + +from kivymd.uix.behaviors.stencil_behavior import StencilBehavior + + +class MotionBase: + """Base class for widget display movement behavior.""" + + show_transition = StringProperty("linear") + """ + The type of transition of the widget opening. + + :attr:`show_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'linear'`. + """ + + show_duration = NumericProperty(0.2) + """ + Duration of widget display transition. + + :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + hide_transition = StringProperty("linear") + """ + The type of transition of the widget closing. + + :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'linear'`. + """ + + hide_duration = NumericProperty(0.2) + """ + Duration of widget closing transition. + + :attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + +class MotionDropDownMenuBehavior(MotionBase): + """ + Base class for the dropdown menu movement behavior. + + For more information, see in the :class:`~MotionBase` class documentation. + """ + + show_transition = StringProperty("out_back") + """ + The type of transition of the widget opening. + + :attr:`show_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_back'`. + """ + + show_duration = NumericProperty(0.4) + """ + Duration of widget display transition. + + :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + hide_transition = StringProperty("out_cubic") + """ + The type of transition of the widget closing. + + :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_cubic'`. + """ + + _scale_x = NumericProperty(None) + """ + Default X-axis scaling values. + + :attr:`_scale_x` is a :class:`~kivy.properties.NumericProperty` + and defaults to `None`. + """ + + _scale_y = NumericProperty(None) + """ + Default Y-axis scaling values. + + :attr:`_scale_y` is a :class:`~kivy.properties.NumericProperty` + and defaults to `None`. + """ + + _opacity = NumericProperty(None) + """ + Menu transparency values. + + :attr:`_opacity` is a :class:`~kivy.properties.NumericProperty` + and defaults to `None`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.set_scale() + # self.set_opacity() + + def set_opacity(self) -> None: + self._opacity = 0 + + def set_scale(self) -> None: + self._scale_x = 0 + self._scale_y = 0 + + def on_dismiss(self) -> None: + Window.remove_widget(self) + # anim = Animation( + # _scale_x=0, + # _scale_y=0, + # # _opacity=0, + # duration=self.hide_duration, + # transition=self.hide_transition, + # ) + # anim.bind(on_complete=lambda *args: Window.remove_widget(self)) + # anim.start(self) + + def on_open(self, *args): + pass + anim = Animation( + _scale_y=1, + # _opacity=1, + duration=0.0, + transition=self.show_transition, + ) + anim &= Animation( + _scale_x=1, + duration=0.0, + transition="out_quad", + ) + anim.start(self) + + def on__opacity(self, instance, value): + self.opacity = value + + def on__scale_x(self, instance, value): + self.scale_value_x = value + + def on__scale_y(self, instance, value): + self.scale_value_y = value + + +class MotionDialogBehavior(MotionBase): + """ + Base class for dialog movement behavior. + + For more information, see in the :class:`~MotionBase` class documentation. + """ + + show_duration = NumericProperty(0.0) + """ + Duration of widget display transition. + + :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.1`. + """ + + scale_x = NumericProperty(1.0) + """ + Default X-axis scaling values. + + :attr:`scale_x` is a :class:`~kivy.properties.NumericProperty` + and defaults to `1.5`. + """ + + scale_y = NumericProperty(1.0) + """ + Default Y-axis scaling values. + + :attr:`scale_y` is a :class:`~kivy.properties.NumericProperty` + and defaults to `1.5`. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.set_default_values() + + def set_default_values(self): + """Sets default scaled and transparency values.""" + + self.scale_value_x = self.scale_x + self.scale_value_y = self.scale_y + self.opacity = 0 + + def on_dismiss(self, *args): + """Called when a dialog closed.""" + + self.set_default_values() + + def on_open(self, *args): + """Called when a dialog opened.""" + + Animation( + opacity=1, + scale_value_x=1, + scale_value_y=1, + t=self.show_transition, + d=self.show_duration, + ).start(self) + + +class MotionShackBehavior(StencilBehavior, MotionBase): + """ + The base class for the behavior of the movement of snack bars. + + For more information, see in the + :class:`~MotionBase` class and + :class:`~kivy.uix.behaviors.stencil_behavior.StencilBehavior` class + documentation. + """ + + _interval = 0 + _height = 0 + + def on_dismiss(self, *args): + """Called when a snackbar closed.""" + + def remove_snackbar(*args): + Window.parent.remove_widget(self) + self.height = self._height + self.dispatch("on_dismiss") + + Clock.unschedule(self._wait_interval) + anim = Animation( + opacity=0, + height=0, + t=self.hide_transition, + d=self.hide_duration, + ) + anim.bind(on_complete=remove_snackbar) + anim.start(self) + + def on_open(self, *args): + """Called when a snackbar opened.""" + + def open(*args): + self._height = self.height + self.height = 0 + anim = Animation( + opacity=1, + height=self._height, + t=self.show_transition, + d=self.show_duration, + ) + anim.bind( + on_complete=lambda *args: Clock.schedule_interval( + self._wait_interval, 1 + ) + ) + anim.start(self) + + Clock.schedule_once(open) + self.dispatch("on_open") + + def _wait_interval(self, interval): + self._interval += interval + if self._interval > self.duration: + self.dismiss() + self._interval = 0 diff --git a/sbapp/kivymd/uix/behaviors/ripple_behavior.py b/sbapp/kivymd/uix/behaviors/ripple_behavior.py old mode 100755 new mode 100644 index 1271878..fddcef7 --- a/sbapp/kivymd/uix/behaviors/ripple_behavior.py +++ b/sbapp/kivymd/uix/behaviors/ripple_behavior.py @@ -413,7 +413,12 @@ class CommonRipple: class RectangularRippleBehavior(CommonRipple): - """Class implements a rectangular ripple effect.""" + """ + Class implements a rectangular ripple effect. + + For more information, see in the :class:`~kivymd.uix.behavior.CommonRipple` + class documentation. + """ ripple_scale = NumericProperty(2.75) """ @@ -472,7 +477,12 @@ class RectangularRippleBehavior(CommonRipple): class CircularRippleBehavior(CommonRipple): - """Class implements a circular ripple effect.""" + """ + Class implements a circular ripple effect. + + For more information, see in the :class:`~kivymd.uix.behavior.CommonRipple` + class documentation. + """ ripple_scale = NumericProperty(1) """ diff --git a/sbapp/kivymd/uix/behaviors/rotate_behavior.py b/sbapp/kivymd/uix/behaviors/rotate_behavior.py index c80b879..c3f6fdf 100644 --- a/sbapp/kivymd/uix/behaviors/rotate_behavior.py +++ b/sbapp/kivymd/uix/behaviors/rotate_behavior.py @@ -91,6 +91,10 @@ KivyMD Test().run() + +.. warning:: Do not use `RotateBehavior` class with classes that inherited` + from `CommonElevationBehavior` class. `CommonElevationBehavior` classes + by default contains attributes for rotate widget. """ __all__ = ("RotateBehavior",) diff --git a/sbapp/kivymd/uix/behaviors/scale_behavior.py b/sbapp/kivymd/uix/behaviors/scale_behavior.py index bfc1e03..85fb7b4 100644 --- a/sbapp/kivymd/uix/behaviors/scale_behavior.py +++ b/sbapp/kivymd/uix/behaviors/scale_behavior.py @@ -105,12 +105,16 @@ KivyMD Test().run() + +.. warning:: Do not use `ScaleBehavior` class with classes that inherited` + from `CommonElevationBehavior` class. `CommonElevationBehavior` classes + by default contains attributes for scale widget. """ __all__ = ("ScaleBehavior",) from kivy.lang import Builder -from kivy.properties import NumericProperty +from kivy.properties import ListProperty, NumericProperty Builder.load_string( """ @@ -120,8 +124,11 @@ Builder.load_string( Scale: x: self.scale_value_x y: self.scale_value_y - z: self.scale_value_x - origin: self.center + z: self.scale_value_z + origin: + self.center \ + if not self.scale_value_center else \ + self.scale_value_center canvas.after: PopMatrix """ @@ -154,3 +161,15 @@ class ScaleBehavior: :attr:`scale_value_z` is an :class:`~kivy.properties.NumericProperty` and defaults to `1`. """ + + scale_value_center = ListProperty() + """ + Origin of the scale. + + .. versionadded:: 1.2.0 + + The format of the origin can be either (x, y) or (x, y, z). + + :attr:`scale_value_center` is an :class:`~kivy.properties.NumericProperty` + and defaults to `[]`. + """ diff --git a/sbapp/kivymd/uix/behaviors/touch_behavior.py b/sbapp/kivymd/uix/behaviors/touch_behavior.py index 49b54fc..8aca7bf 100644 --- a/sbapp/kivymd/uix/behaviors/touch_behavior.py +++ b/sbapp/kivymd/uix/behaviors/touch_behavior.py @@ -22,7 +22,7 @@ Usage from kivymd.uix.button import MDRaisedButton KV = ''' - Screen: + MDScreen: MyButton: text: "PRESS ME" @@ -74,9 +74,10 @@ class TouchBehavior: def create_clock(self, widget, touch, *args): if self.collide_point(touch.x, touch.y): - callback = partial(self.on_long_touch, touch) - Clock.schedule_once(callback, self.duration_long_touch) - touch.ud["event"] = callback + if "event" not in touch.ud: + callback = partial(self.on_long_touch, touch) + Clock.schedule_once(callback, self.duration_long_touch) + touch.ud["event"] = callback if touch.is_double_tap: self.on_double_tap(touch, *args) @@ -85,10 +86,9 @@ class TouchBehavior: def delete_clock(self, widget, touch, *args): if self.collide_point(touch.x, touch.y): - try: + if "event" in touch.ud: Clock.unschedule(touch.ud["event"]) - except KeyError: - pass + del touch.ud["event"] def on_long_touch(self, touch, *args): """Called when the widget is pressed for a long time.""" diff --git a/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py b/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py old mode 100755 new mode 100644 index d99f54b..9b764d4 --- a/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py +++ b/sbapp/kivymd/uix/bottomnavigation/bottomnavigation.py @@ -274,12 +274,19 @@ with open( Builder.load_string(kv_file.read()) -class MDBottomNavigationHeader( - ThemableBehavior, ButtonBehavior, MDAnchorLayout -): +class MDBottomNavigationHeader(ButtonBehavior, MDAnchorLayout): + """ + Bottom navigation header class. + + For more information, see in the + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.anchorlayout.MDAnchorLayout` + classes documentation. + """ + panel_color = ColorProperty([1, 1, 1, 0]) """ - Panel color of bottom navigation. + Panel color of bottom navigation in (r, g, b, a) or string format. :attr:`panel_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[1, 1, 1, 0]`. @@ -307,7 +314,8 @@ class MDBottomNavigationHeader( text_color_normal = ColorProperty([1, 1, 1, 1]) """ - Text color of the label when it is not selected. + Text color in (r, g, b, a) or string format of the label when it is not + selected. :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` and defaults to `[1, 1, 1, 1]`. @@ -315,7 +323,7 @@ class MDBottomNavigationHeader( text_color_active = ColorProperty([1, 1, 1, 1]) """ - Text color of the label when it is selected. + Text color in (r, g, b, a) or string format of the label when it is selected. :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty` and defaults to `[1, 1, 1, 1]`. @@ -323,7 +331,8 @@ class MDBottomNavigationHeader( selected_color_background = ColorProperty(None) """ - The background color of the highlighted item when using Material Design v3. + The background color in (r, g, b, a) or string format of the highlighted + item when using Material Design v3. .. versionadded:: 1.0.0 @@ -384,10 +393,13 @@ class MDBottomNavigationHeader( ) -class MDTab(MDScreen, ThemableBehavior): +class MDTab(MDScreen): """ A tab is simply a screen with meta information that defines the content that goes in the tab header. + + For more information, see in the + :class:`~kivymd.uix.screen.MDScreen` class documentation. """ __events__ = ( @@ -524,6 +536,10 @@ class TabbedPanelBase( A class that contains all variables a :class:`~kivy.properties.TabPannel` must have. It is here so I (zingballyhoo) don't get mad about the :class:`~kivy.properties.TabbedPannels` not being DRY. + + For more information, see in the :class:`~kivymd.theming.ThemableBehavior` + and :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior` + and :class:`~kivy.uix.boxlayout.BoxLayout` classes documentation. """ current = StringProperty(None) @@ -555,6 +571,10 @@ class MDBottomNavigation(DeclarativeBehavior, TabbedPanelBase): A bottom navigation that is implemented by delegating all items to a :class:`~kivy.uix.screenmanager.ScreenManager`. + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~TabbedPanelBase` classes documentation. + :Events: :attr:`on_switch_tabs` Called when switching tabs. Returns the object of the tab to be @@ -856,7 +876,5 @@ class MDBottomNavigation(DeclarativeBehavior, TabbedPanelBase): return bottom_navigation_item -class MDBottomNavigationBar( - ThemableBehavior, CommonElevationBehavior, MDFloatLayout -): +class MDBottomNavigationBar(CommonElevationBehavior, MDFloatLayout): pass diff --git a/sbapp/kivymd/uix/bottomsheet/__init__.py b/sbapp/kivymd/uix/bottomsheet/__init__.py index 439811d..dec1c23 100644 --- a/sbapp/kivymd/uix/bottomsheet/__init__.py +++ b/sbapp/kivymd/uix/bottomsheet/__init__.py @@ -1,7 +1,10 @@ # NOQA F401 from .bottomsheet import ( - GridBottomSheetItem, MDBottomSheet, + MDBottomSheetContent, + MDBottomSheetDragHandle, + MDBottomSheetDragHandleButton, + MDBottomSheetDragHandleTitle, MDCustomBottomSheet, MDGridBottomSheet, MDListBottomSheet, diff --git a/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv b/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv index a840629..6d63e1c 100644 --- a/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv +++ b/sbapp/kivymd/uix/bottomsheet/bottomsheet.kv @@ -1,73 +1,42 @@ -#:import Window kivy.core.window.Window + + size_hint_y: None + height: self.minimum_height - + + orientation: "vertical" + size_hint_y: None + height: self.minimum_height + padding: "16dp", "8dp", "16dp", "16dp" - MDGridLayout: - id: box_sheet_list - cols: 1 - adaptive_height: True - padding: 0, 0, 0, "96dp" + BottomSheetDragHandle: + md_bg_color: + app.theme_cls.disabled_hint_text_color \ + if not root.drag_handle_color else \ + root.drag_handle_color + size_hint: None, None + size: "32dp", "4dp" + radius: 4 + pos_hint: {"center_x": .5} + + BottomSheetDragHandleContainer: + id: header_container + size_hint_y: None + height: self.minimum_height - md_bg_color: root.value_transparent - _upper_padding: _upper_padding - _gl_content: _gl_content - _position_content: Window.height + orientation: "vertical" + md_bg_color: root.bg_color if root.bg_color else app.theme_cls.bg_darkest + radius: 16, 16, 0, 0 + padding: 0, "8dp", 0, 0 MDBoxLayout: - orientation: "vertical" - padding: 0, 1, 0, 0 + id: drag_handle_container + size_hint_y: None + height: self.minimum_height - BsPadding: - id: _upper_padding - size_hint_y: None - height: root.height - min(root.width * 9 / 16, root._gl_content.height) - on_release: root.dismiss() - - BottomSheetContent: - id: _gl_content - size_hint_y: None - cols: 1 - md_bg_color: 0, 0, 0, 0 - - canvas: - Color: - rgba: root.theme_cls.bg_normal if not root.bg_color else root.bg_color - RoundedRectangle: - pos: self.pos - size: self.size - radius: - [ - (root.radius, root.radius) if root.radius_from == "top_left" or root.radius_from == "top" else (0, 0), - (root.radius, root.radius) if root.radius_from == "top_right" or root.radius_from == "top" else (0, 0), - (root.radius, root.radius) if root.radius_from == "bottom_right" or root.radius_from == "bottom" else (0, 0), - (root.radius, root.radius) if root.radius_from == "bottom_left" or root.radius_from == "bottom" else (0, 0) - ] - - - - theme_text_color: "Primary" - pos_hint: {"center_x": .5, "center_y": .5} - - - - orientation: "vertical" - padding: 0, dp(24), 0, 0 - size_hint_y: None - size: dp(64), dp(96) - - AnchorLayout: - anchor_x: "center" - - MDIconButton: - icon: root.source - user_font_size: root.icon_size - on_release: root.dispatch("on_release") - - MDLabel: - font_style: "Caption" - theme_text_color: "Secondary" - text: root.caption - halign: "center" + MDBoxLayout: + id: container + size_hint_y: None + height: self.minimum_height \ No newline at end of file diff --git a/sbapp/kivymd/uix/bottomsheet/bottomsheet.py b/sbapp/kivymd/uix/bottomsheet/bottomsheet.py old mode 100755 new mode 100644 index 522f31b..4961ff0 --- a/sbapp/kivymd/uix/bottomsheet/bottomsheet.py +++ b/sbapp/kivymd/uix/bottomsheet/bottomsheet.py @@ -4,232 +4,564 @@ Components/BottomSheet .. seealso:: - `Material Design spec, Sheets: bottom `_ + `Material Design spec, Sheets: bottom `_ .. rubric:: Bottom sheets are surfaces containing supplementary content that are anchored to the bottom of the screen. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet.png :align: center -Two classes are available to you :class:`~MDListBottomSheet` and :class:`~MDGridBottomSheet` -for standard bottom sheets dialogs: - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/grid-list-bottomsheets.png - :align: center - -Usage :class:`~MDListBottomSheet` -================================= - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.toast import toast - from kivymd.uix.bottomsheet import MDListBottomSheet - from kivymd.app import MDApp - - KV = ''' - MDScreen: - - MDTopAppBar: - title: "Example BottomSheet" - pos_hint: {"top": 1} - elevation: 4 - - MDRaisedButton: - text: "Open list bottom sheet" - on_release: app.show_example_list_bottom_sheet() - pos_hint: {"center_x": .5, "center_y": .5} - ''' - - - class Example(MDApp): - def build(self): - return Builder.load_string(KV) - - def callback_for_menu_items(self, *args): - toast(args[0]) - - def show_example_list_bottom_sheet(self): - bottom_sheet_menu = MDListBottomSheet() - for i in range(1, 11): - bottom_sheet_menu.add_item( - f"Standart Item {i}", - lambda x, y=i: self.callback_for_menu_items( - f"Standart Item {y}" - ), - ) - bottom_sheet_menu.open() - - - Example().run() - -The :attr:`~MDListBottomSheet.add_item` method of the :class:`~MDListBottomSheet` -class takes the following arguments: - -``text`` - element text; - -``callback`` - function that will be called when clicking on an item; - -There is also an optional argument ``icon``, -which will be used as an icon to the left of the item: - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/icon-list-bottomsheets.png - :align: center - -.. rubric:: Using the :class:`~MDGridBottomSheet` class is similar - to using the :class:`~MDListBottomSheet` class: - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.toast import toast - from kivymd.uix.bottomsheet import MDGridBottomSheet - from kivymd.app import MDApp - - KV = ''' - MDScreen: - - MDTopAppBar: - title: 'Example BottomSheet' - pos_hint: {"top": 1} - elevation: 4 - - MDRaisedButton: - text: "Open grid bottom sheet" - on_release: app.show_example_grid_bottom_sheet() - pos_hint: {"center_x": .5, "center_y": .5} - ''' - - - class Example(MDApp): - def build(self): - return Builder.load_string(KV) - - def callback_for_menu_items(self, *args): - toast(args[0]) - - def show_example_grid_bottom_sheet(self): - bottom_sheet_menu = MDGridBottomSheet() - data = { - "Facebook": "facebook-box", - "YouTube": "youtube", - "Twitter": "twitter-box", - "Da Cloud": "cloud-upload", - "Camera": "camera", - } - for item in data.items(): - bottom_sheet_menu.add_item( - item[0], - lambda x, y=item[0]: self.callback_for_menu_items(y), - icon_src=item[1], - ) - bottom_sheet_menu.open() - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/grid-bottomsheet.png - :align: center - -.. rubric:: You can use custom content for bottom sheet dialogs: - -.. code-block:: python - - from kivy.lang import Builder - from kivy.factory import Factory - - from kivymd.uix.bottomsheet import MDCustomBottomSheet - from kivymd.app import MDApp - - KV = ''' - - on_press: app.custom_sheet.dismiss() - icon: "" - - IconLeftWidget: - icon: root.icon - - - : - orientation: "vertical" - size_hint_y: None - height: "400dp" - - MDTopAppBar: - title: 'Custom bottom sheet:' - - ScrollView: - - MDGridLayout: - cols: 1 - adaptive_height: True - - ItemForCustomBottomSheet: - icon: "page-previous" - text: "Preview" - - ItemForCustomBottomSheet: - icon: "exit-to-app" - text: "Exit" - - - MDScreen: - - MDTopAppBar: - title: 'Example BottomSheet' - pos_hint: {"top": 1} - elevation: 4 - - MDRaisedButton: - text: "Open custom bottom sheet" - on_release: app.show_example_custom_bottom_sheet() - pos_hint: {"center_x": .5, "center_y": .5} - ''' - - - class Example(MDApp): - custom_sheet = None - - def build(self): - return Builder.load_string(KV) - - def show_example_custom_bottom_sheet(self): - self.custom_sheet = MDCustomBottomSheet(screen=Factory.ContentCustomSheet()) - self.custom_sheet.open() - - - Example().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/custom-bottomsheet.png - :align: center - -.. note:: When you use the :attr:`~MDCustomBottomSheet` class, you must specify - the height of the user-defined content exactly, otherwise ``dp(100)`` - heights will be used for your ``ContentCustomSheet`` class: +Usage +===== .. code-block:: kv - : - orientation: "vertical" - size_hint_y: None - height: "400dp" + MDScreen: + + [ Content screen ] + + MDBottomSheet: + +The bottom sheet has two types: + +- Standard_ +- Modal_ + +.. Standard: +Standard +-------- + +`Standard bottom sheets `_ +co-exist with the screen’s main UI region and allow for simultaneously viewing +and interacting with both regions, especially when the main UI region is +frequently scrolled or panned. + +Use a standard bottom sheet to display content that complements the screen’s +primary content, such as an audio player in a music app. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-standard.png + :align: center + +Standard bottom sheets are elevated above the main UI region so their +visibility is not affected by panning or scrolling. + +Standard bottom sheet example +----------------------------- + +.. tabs:: + + .. tab:: Declarative KV style + + .. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDBoxLayout: + orientation: "vertical" + padding: "12dp" + adaptive_height: True + pos_hint: {"top": 1} + + MDSmartTile: + id: smart_tile + source: "https://picsum.photos/id/70/3011/2000" + radius: 16 + box_radius: [0, 0, 16, 16] + size_hint_y: None + height: "240dp" + on_release: + bottom_sheet.open() \\ + if bottom_sheet.state == "close" else \\ + bottom_sheet.dismiss() + + MDLabel: + bold: True + color: 1, 1, 1, 1 + text: + "Tap to open the bottom sheet" \\ + if bottom_sheet.state == "close" else \\ + "Tap to close the bottom sheet" + + MDBottomSheet: + id: bottom_sheet + type: "standard" + bg_color: "grey" + default_opening_height: smart_tile.y - dp(12) + size_hint_y: None + height: root.height - (smart_tile.height + dp(24)) + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + + .. tab:: Declarative python style + + .. code-block:: python + + from kivy.clock import Clock + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.bottomsheet import MDBottomSheet + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.imagelist import MDSmartTile + from kivymd.uix.label import MDLabel + from kivymd.uix.screen import MDScreen + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return MDScreen( + MDBoxLayout( + MDSmartTile( + MDLabel( + id="tile_label", + text="Tap to open the bottom sheet", + bold=True, + color=(1, 1, 1, 1), + ), + id="smart_tile", + source="https://picsum.photos/id/70/3011/2000", + radius=16, + box_radius=[0, 0, 16, 16], + size_hint_y=None, + height="240dp", + ), + id="box", + orientation="vertical", + padding="12dp", + pos_hint={"top": 1}, + adaptive_height=True, + ), + MDBottomSheet( + id="bottom_sheet", + size_hint_y=None, + type="standard", + bg_color="grey", + ), + ) + + def open_bottom_sheet(self, *args): + bottom_sheet = self.root.ids.bottom_sheet + smart_tile = self.root.ids.box.ids.smart_tile + tile_label = smart_tile.ids.tile_label + bottom_sheet.open() if bottom_sheet.state == "close" else bottom_sheet.dismiss() + tile_label.text = ( + "Tap to open the bottom sheet" + if bottom_sheet.state == "close" + else "Tap to close the bottom sheet" + ) + + def on_start(self): + def on_start(*args): + bottom_sheet = self.root.ids.bottom_sheet + smart_tile = self.root.ids.box.ids.smart_tile + bottom_sheet.default_opening_height = smart_tile.y - dp(12) + bottom_sheet.height = self.root.height - ( + smart_tile.height + dp(24) + ) + smart_tile.bind(on_release=lambda x: self.open_bottom_sheet()) + + Clock.schedule_once(on_start, 1.2) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-standard-example.gif + :align: center + +.. Modal: +Modal +----- + +Like dialogs, `modal bottom sheets `_ +appear in front of app content, disabling all other app functionality when +they appear, and remaining on screen until confirmed, dismissed, or a required +action has been taken. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-modal.png + :align: center + +Modal bottom sheet example +-------------------------- + +.. tabs:: + + .. tab:: Declarative KV style + + .. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDBoxLayout: + orientation: "vertical" + padding: "12dp" + adaptive_height: True + pos_hint: {"top": 1} + + MDSmartTile: + id: smart_tile + source: "https://picsum.photos/id/70/3011/2000" + radius: 16 + box_radius: [0, 0, 16, 16] + size_hint_y: None + height: "240dp" + on_release: bottom_sheet.open() + + MDLabel: + bold: True + color: 1, 1, 1, 1 + text: "Tap to open the modal bottom sheet" + + MDBottomSheet: + id: bottom_sheet + bg_color: "grey" + default_opening_height: smart_tile.y - dp(12) + size_hint_y: None + height: root.height - (smart_tile.height + dp(24)) + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + + .. tab:: Declarative python style + + .. code-block:: python + + from kivy.clock import Clock + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.bottomsheet import MDBottomSheet + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.imagelist import MDSmartTile + from kivymd.uix.label import MDLabel + from kivymd.uix.screen import MDScreen + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return MDScreen( + MDBoxLayout( + MDSmartTile( + MDLabel( + id="tile_label", + text="Tap to open the modal bottom sheet", + bold=True, + color=(1, 1, 1, 1), + ), + id="smart_tile", + source="https://picsum.photos/id/70/3011/2000", + radius=16, + box_radius=[0, 0, 16, 16], + size_hint_y=None, + height="240dp", + ), + id="box", + orientation="vertical", + padding="12dp", + pos_hint={"top": 1}, + adaptive_height=True, + ), + MDBottomSheet( + id="bottom_sheet", + size_hint_y=None, + bg_color="grey", + ), + ) + + def open_bottom_sheet(self, *args): + bottom_sheet = self.root.ids.bottom_sheet + bottom_sheet.open() + + def on_start(self): + def on_start(*args): + bottom_sheet = self.root.ids.bottom_sheet + smart_tile = self.root.ids.box.ids.smart_tile + bottom_sheet.default_opening_height = smart_tile.y - dp(12) + bottom_sheet.height = self.root.height - ( + smart_tile.height + dp(24) + ) + smart_tile.bind(on_release=lambda x: self.open_bottom_sheet()) + + Clock.schedule_once(on_start, 1.2) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-modal-example.gif + :align: center + +Tapping the scrim dismisses a modal bottom sheet. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-modal-tapping.png + :align: center + +Custom positioning +------------------ + +The optional drag handle provides an affordance for custom sheet height, +or for a quick toggle through preset heights. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-drag-handle.png + :align: center + +.. code-block:: kv + + MDBottomSheet: + + MDBottomSheetDragHandle: + +By default, when you drag and then release the drag handle, the bottom sheet +will be closed or expand to the full screen, depending on whether you released +the drag handle closer to the top or to the bottom of the screen: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-drag-handle.gif + :align: center + +In order to manually adjust the height of the bottom sheet with the drag handle, +set the `auto_positioning` parameter to `False`: + +.. code-block:: kv + + MDBottomSheet: + auto_positioning: False + + MDBottomSheetDragHandle: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-drag-handle-auto-positioning.gif + :align: center + +Add elements to :class:`~MDBottomSheetDragHandleTitle` class +------------------------------------------------------------ + +.. code-block:: kv + + MDBottomSheet: + + MDBottomSheetDragHandle: + + MDBottomSheetDragHandleTitle: + text: "MDBottomSheet" + adaptive_height: True + font_style: "H6" + pos_hint: {"center_y": .5} + + MDBottomSheetDragHandleButton: + icon: "close" + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-drag-handle-elements.png + :align: center + +Add custom content to :class:`~MDBottomSheet` class +--------------------------------------------------- + +To add custom content to the bottom sheet, use the +:class:`~MDBottomSheetContent` class: + +.. code-block:: kv + + MDBottomSheet: + bg_color: "darkgrey" + type: "standard" + max_opening_height: self.height + default_opening_height: self.max_opening_height + adaptive_height: True + + MDBottomSheetDragHandle: + drag_handle_color: "grey" + + MDBottomSheetContent: + padding: "16dp" + + MDLabel: + text: "Content" + halign: "center" + font_style: "H5" + adaptive_height: True + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-content.png + :align: center + +A practical example with standard bottom sheet +---------------------------------------------- + +(A double tap on the map to open the bottom sheet) + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import StringProperty, ObjectProperty, BooleanProperty + from kivy_garden.mapview import MapView + + from kivymd.app import MDApp + from kivymd.uix.behaviors import TouchBehavior + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.utils import asynckivy + + KV = ''' + #:import MapSource kivy_garden.mapview.MapSource + #:import asynckivy kivymd.utils.asynckivy + + + + orientation: "vertical" + adaptive_height: True + spacing: "8dp" + + MDIconButton: + id: icon + icon: root.icon + md_bg_color: "#EDF1F9" if not root.selected else app.theme_cls.primary_color + pos_hint: {"center_x": .5} + theme_icon_color: "Custom" + icon_color: "white" if root.selected else "black" + on_release: app.set_active_element(root, root.title.lower()) + + MDLabel: + font_size: "14sp" + text: root.title + pos_hint: {"center_x": .5} + halign: "center" + adaptive_height: True + + + MDScreen: + + CustomMapView: + bottom_sheet: bottom_sheet + map_source: MapSource(url=app.map_sources[app.current_map]) + lat: 46.5124 + lon: 47.9812 + zoom: 12 + + MDBottomSheet: + id: bottom_sheet + elevation: 2 + shadow_softness: 6 + bg_color: "white" + type: "standard" + max_opening_height: self.height + default_opening_height: self.max_opening_height + adaptive_height: True + on_open: asynckivy.start(app.generate_content()) + + MDBottomSheetDragHandle: + drag_handle_color: "grey" + + MDBottomSheetDragHandleTitle: + text: "Select type map" + adaptive_height: True + bold: True + pos_hint: {"center_y": .5} + + MDBottomSheetDragHandleButton: + icon: "close" + _no_ripple_effect: True + on_release: bottom_sheet.dismiss() + + MDBottomSheetContent: + id: content_container + padding: 0, 0, 0, "16dp" + ''' + + + class TypeMapElement(MDBoxLayout): + selected = BooleanProperty(False) + icon = StringProperty() + title = StringProperty() + + + class CustomMapView(MapView, TouchBehavior): + bottom_sheet = ObjectProperty() + + def on_double_tap(self, touch, *args): + if self.bottom_sheet: + self.bottom_sheet.open() + + + class Example(MDApp): + map_sources = { + "street": "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}", + "sputnik": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}", + "hybrid": "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}", + } + current_map = StringProperty("street") + + async def generate_content(self): + icons = { + "street": "google-street-view", + "sputnik": "space-station", + "hybrid": "map-legend", + } + if not self.root.ids.content_container.children: + for i, title in enumerate(self.map_sources.keys()): + await asynckivy.sleep(0) + self.root.ids.content_container.add_widget( + TypeMapElement( + title=title.capitalize(), + icon=icons[title], + selected=not i, + ) + ) + + def set_active_element(self, instance, type_map): + for element in self.root.ids.content_container.children: + if instance == element: + element.selected = True + self.current_map = type_map + else: + element.selected = False + + def build(self): + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-sheet-real-example.gif + :align: center -.. note:: The height of the bottom sheet dialog will never exceed half - the height of the screen! """ __all__ = ( - "MDGridBottomSheet", - "GridBottomSheetItem", - "MDListBottomSheet", "MDCustomBottomSheet", + "MDGridBottomSheet", + "MDListBottomSheet", "MDBottomSheet", + "MDBottomSheetContent", + "MDBottomSheetDragHandle", + "MDBottomSheetDragHandleTitle", + "MDBottomSheetDragHandleButton", ) import os +from kivy import Logger from kivy.animation import Animation from kivy.clock import Clock from kivy.core.window import Window @@ -243,18 +575,15 @@ from kivy.properties import ( OptionProperty, StringProperty, ) -from kivy.uix.behaviors import ButtonBehavior -from kivy.uix.boxlayout import BoxLayout -from kivy.uix.floatlayout import FloatLayout -from kivy.uix.gridlayout import GridLayout -from kivy.uix.modalview import ModalView -from kivy.uix.scrollview import ScrollView +from kivy.uix.screenmanager import Screen -from kivymd import images_path, uix_path -from kivymd.theming import ThemableBehavior -from kivymd.uix.behaviors import BackgroundColorBehavior -from kivymd.uix.label import MDIcon -from kivymd.uix.list import ILeftBody, OneLineIconListItem, OneLineListItem +from kivymd import uix_path +from kivymd.uix.behaviors import CommonElevationBehavior, TouchBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.button import MDIconButton +from kivymd.uix.label import MDLabel +from kivymd.uix.screen import MDScreen +from kivymd.uix.widget import MDWidget with open( os.path.join(uix_path, "bottomsheet", "bottomsheet.kv"), @@ -263,25 +592,206 @@ with open( Builder.load_string(kv_file.read()) -class SheetList(ScrollView): +class BottomSheetDragHandle(MDWidget): pass -class BsPadding(ButtonBehavior, FloatLayout): +class BottomSheetDragHandleContainer(MDBoxLayout): pass -class BottomSheetContent(BackgroundColorBehavior, GridLayout): - pass +class BottomSheetScrimLayer(MDWidget): + """ + Implements a transparency layer to shade the parent widget + on which the bottom sheet is displayed. + """ -class MDBottomSheet(ThemableBehavior, ModalView): - background = f"{images_path}transparent.png" - """Private attribute.""" +class MDBottomSheetContent(MDBoxLayout): + """ + Implements a container for custom content for the :class:`~MDBottomSheet` + class + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. + + .. versionadded:: 1.2.0 + """ + + +class MDBottomSheetDragHandleButton(MDIconButton): + """ + Implements a close button (or other functionality) for the + :class:`~MDBottomSheetDragHandle` container. + + For more information, see in the + :class:`~kivymd.uix.button.MDIconButton` class documentation. + + .. versionadded:: 1.2.0 + """ + + +class MDBottomSheetDragHandleTitle(MDLabel): + """ + Implements a header for the :class:`~MDBottomSheetDragHandle` container. + + For more information, see in the + :class:`~kivymd.uix.label.MDLabel` class documentation. + + .. versionadded:: 1.2.0 + """ + + +class MDBottomSheetDragHandle(MDBoxLayout): + """ + Implements a container that can place the header of the bottom sheet + and the close button. Also implements the event of dragging the + bottom sheet on the parent screen. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. + + .. versionadded:: 1.2.0 + """ + + drag_handle_color = ColorProperty(None) + """ + Color of drag handle element in (r, g, b, a) or string format. + + .. code-block:: kv + + MDBottomSheet: + + MDBottomSheetDragHandle: + drag_handle_color: "white" + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-sheet-drag-handle-color.png + :align: center + + :attr:`drag_handle_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + def add_widget(self, widget, *args, **kwargs): + if isinstance( + widget, + (MDBottomSheetDragHandleTitle, MDBottomSheetDragHandleButton), + ): + self.ids.header_container.add_widget(widget) + elif isinstance( + widget, + (BottomSheetDragHandleContainer, BottomSheetDragHandle), + ): + return super().add_widget(widget) + + +class MDBottomSheet(MDBoxLayout, CommonElevationBehavior, TouchBehavior): + """ + Bottom sheet class. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` and + :class:`~kivymd.uix.behaviors.touch_behavior.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.touch_behavior.TouchBehavior` + classes documentation. + + :Events: + `on_open` + Event when opening the bottom sheet. + `on_close` + Event when closing the bottom sheet. + `on_progress` + Bottom sheet opening/closing progress event. + """ + + auto_dismiss = BooleanProperty(True) + """ + This property determines if the view is automatically + dismissed when the user clicks outside it. + + .. versionadded:: 1.2.0 + + :attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + type = OptionProperty("modal", options=["modal", "standard"]) + """ + Type sheet. There are two types of bottom sheets: standard and modal. + Available options are: `'modal'`, `'standard'`. + + .. versionadded:: 1.2.0 + + :attr:`type` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'modal`. + """ + + auto_positioning = BooleanProperty(True) + """ + Close or expand the bottom menu automatically when you release the + drag handle. + + .. versionadded:: 1.2.0 + + :attr:`auto_positioning` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + max_opening_height = NumericProperty(None, allownone=True) + """ + The maximum height a that the bottom sheet can be opened using the + drag handle. + + .. versionadded:: 1.2.0 + + .. code-block:: kv + + MDBottomSheet: + max_opening_height: "300dp" + + MDBottomSheetDragHandle: + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-max-opening-height.gif + :align: center + + :attr:`max_opening_height` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `None`. + """ + + opening_transition = StringProperty("out_cubic") + """ + The name of the animation transition type to use when animating to + the :attr:`state` `'open'`. + + .. versionadded:: 1.2.0 + + :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_cubic'`. + """ + + closing_transition = StringProperty("out_sine") + """The name of the animation transition type to use when animating to + the :attr:`state` 'close'. + + .. versionadded:: 1.2.0 + + :attr:`closing_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_sine'`. + """ + + default_opening_height = NumericProperty(dp(200)) + """ + Default opening height of the bottom sheet. + + .. versionadded:: 1.2.0 + + :attr:`default_opening_height` is an :class:`~kivy.properties.NumericProperty` + and defaults to `dp(100)`. + """ duration_opening = NumericProperty(0.15) """ - The duration of the bottom sheet dialog opening animation. + The duration of the bottom sheet opening animation. :attr:`duration_opening` is an :class:`~kivy.properties.NumericProperty` and defaults to `0.15`. @@ -295,12 +805,41 @@ class MDBottomSheet(ThemableBehavior, ModalView): and defaults to `0.15`. """ - radius = NumericProperty(25) + animation = BooleanProperty(True) """ - The value of the rounding of the corners of the dialog. + Whether to use animation for opening and closing of the bottom sheet + or not. - :attr:`radius` is an :class:`~kivy.properties.NumericProperty` - and defaults to `25`. + :attr:`animation` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + state = OptionProperty("close", options=["close", "open"]) + """ + Menu state. Available options are: `'close'`, `'open'`. + + .. versionadded:: 1.2.0 + + :attr:`state` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'close'`. + """ + + scrim_layer_color = ColorProperty([0, 0, 0, 1]) + """ + Color for scrim in (r, g, b, a) or string format. + + .. versionadded:: 1.2.0 + + :attr:`scrim_layer_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 1]`. + """ + + bg_color = ColorProperty(None) + """ + Background color of bottom sheet in (r, g, b, a) or string format. + + :attr:`bg_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. """ radius_from = OptionProperty( @@ -314,196 +853,311 @@ class MDBottomSheet(ThemableBehavior, ModalView): "bottom", ], allownone=True, + deprecated=True, ) """ Sets which corners to cut from the dialog. Available options are: - (`"top_left"`, `"top_right"`, `"top"`, `"bottom_right"`, `"bottom_left"`, `"bottom"`). + `"top_left"`, `"top_right"`, `"top"`, `"bottom_right"`, `"bottom_left"`, + `"bottom"`. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottomsheet-radius-from.png - :align: center + .. deprecated:: 1.2.0 + Use :attr:`radius` instead. :attr:`radius_from` is an :class:`~kivy.properties.OptionProperty` and defaults to `None`. """ - animation = BooleanProperty(False) + value_transparent = ColorProperty([0, 0, 0, 0.8], deprecated=True) """ - Whether to use animation for opening and closing of the bottomsheet or not. + Background color in (r, g, b, a) or string format transparency value when + opening a dialog. - :attr:`animation` is an :class:`~kivy.properties.BooleanProperty` - and defaults to `False`. - """ - - bg_color = ColorProperty(None) - """ - Dialog background color in ``rgba`` format. - - :attr:`bg_color` is an :class:`~kivy.properties.ColorProperty` - and defaults to `[]`. - """ - - value_transparent = ColorProperty([0, 0, 0, 0.8]) - """ - Background transparency value when opening a dialog. + .. deprecated:: 1.2.0 :attr:`value_transparent` is an :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0.8]`. """ - _upper_padding = ObjectProperty() - _gl_content = ObjectProperty() - _position_content = NumericProperty() + _diff_between_touch_height_sheet = 0 + _alpha_channel_value = 0 + # Menu state: + # - value 'down' - menu is captured; + # - value 'none' - menu is not captured; + _state = OptionProperty("none", options=["none", "down"]) + # There was a touch to the bottom sheet. + _touch_sheet = False + # kivymd.uix.bottomsheet.bottomsheet.BottomSheetScrimLayer object. + _scrim_layer = ObjectProperty(None, allownone=True) - def open(self, *args): - super().open(*args) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.y = -Window.height # start bottom sheet position + Clock.schedule_once(self.check_parent) + Clock.schedule_once(self.check_max_opening_height) + Clock.schedule_once(self.add_scrim_layer) + self.register_event_type("on_open") + self.register_event_type("on_close") + self.register_event_type("on_progress") - def add_widget(self, widget, index=0, canvas=None): - super().add_widget(widget, index, canvas) + def on_progress(self, *args) -> None: + """Bottom sheet opening/closing progress event.""" - def dismiss(self, *args, **kwargs): - def dismiss(*args): - self.dispatch("on_pre_dismiss") - self._gl_content.clear_widgets() - self._real_remove_widget() - self.dispatch("on_dismiss") + def on_open(self, *args) -> None: + """Event when opening the bottom sheet.""" - if self.animation: - a = Animation(height=0, d=self.duration_closing) - a.bind(on_complete=dismiss) - a.start(self._gl_content) + def on_close(self, *args) -> None: + """Event when closing the bottom sheet.""" + + def on_long_touch(self, touch, *args): + if self.ids.drag_handle_container.collide_point(touch.x, touch.y): + self._state = "down" + + def on_touch_down(self, touch): + if self.type == "standard": + super().on_touch_down(touch) + + if self.collide_point(touch.x, touch.y): + self._touch_sheet = not self._touch_sheet + if self.type == "standard": + return True + elif self.type == "modal": + return super().on_touch_down(touch) + + def on_touch_up(self, touch): + self._diff_between_touch_height_sheet = 0 + self._alpha_channel_value = 0 + + if self.collide_point(touch.x, touch.y): + self._touch_sheet = not self._touch_sheet + if self.auto_positioning: + if self._state == "down": + self._set_state(touch.y) else: - dismiss() + if self._state == "down": + self._touch_sheet = not self._touch_sheet + self._set_state(touch.y) - def resize_content_layout(self, content, layout, interval=0): - if not layout.ids.get("box_sheet_list"): - _layout = layout - else: - _layout = layout.ids.box_sheet_list + def on_touch_move(self, touch): + if self._state == "down": + if not self._diff_between_touch_height_sheet: + self._diff_between_touch_height_sheet = ( + abs(self.y) if self.y else self.height + ) - touch.y - if _layout.height > Window.height / 2: - height = Window.height / 2 - else: - height = _layout.height + # FIXME: the behavior of the drag handle looks strange: + # sometimes the bottom sheet is dragged as needed, and sometimes + # it's position does not correspond to the cursor coordinates. + y = -( + (self.height - touch.y) + - 0 # self._diff_between_touch_height_sheet + ) - if self.animation: - Animation(height=height, d=self.duration_opening).start(_layout) - Animation(height=height, d=self.duration_opening).start(content) - else: - layout.height = height - content.height = height + if y > 0: + self.y = 0 + return + if self.max_opening_height and touch.y > self.max_opening_height: + self.y = -(self.height - self.max_opening_height) + return + self.y = y -class ListBottomSheetIconLeft(ILeftBody, MDIcon): - pass + if self._scrim_layer and self.type == "modal": + if not self._alpha_channel_value: + self._alpha_channel_value = ( + self._scrim_layer.md_bg_color[-1] - touch.psy + ) + + self._scrim_layer.md_bg_color = self._scrim_layer.md_bg_color[ + :-1 + ] + [touch.psy + self._alpha_channel_value] + + # + # if self.radius == [0.0, 0.0, 0.0, 0.0]: + # self.radius = [16, 16, 0, 0] + + return super().on_touch_move(touch) + + def on_type(self, *args) -> None: + self.add_scrim_layer() + + def add_scrim_layer(self, *args) -> None: + """ + Adds a scrim layer to the parent widget on which the bottom sheet + will be displayed. + """ + + if not self._scrim_layer and self.type == "modal": + self._scrim_layer = BottomSheetScrimLayer() + self.parent.add_widget(self._scrim_layer, index=1) + self._scrim_layer.bind(on_touch_down=self._on_touch_down_layer) + if self._scrim_layer and self.type == "standard": + self.parent.remove_widget(self._scrim_layer) + self._scrim_layer = None + + def check_max_opening_height(self, *args) -> None: + if ( + self.max_opening_height + and self.max_opening_height < self.default_opening_height + ): + raise ValueError( + "The value of `max_opening_height` cannot be less " + "than the value of `default_opening_height`" + ) + + def check_parent(self, *args) -> None: + """ + Checks the type of parent widget to which the bottom sheet + will be added. + """ + + if not issubclass(self.parent.__class__, Screen): + raise TypeError( + f"The bottom sheet can only be added to the {Screen} " + f"or {MDScreen} widgets." + ) + + def dismiss(self, *args) -> None: + """Dismiss of bottom sheet.""" + + anim = Animation( + y=-self.height, + d=self.duration_closing if self.animation else 0, + t=self.closing_transition, + ) + anim.bind( + on_complete=lambda x, y: self.dispatch("on_close"), + on_progress=lambda x, y, z: self.dispatch("on_progress", z), + ) + anim.start(self) + + # Animation( + # radius=[16, 16, 0, 0], + # d=self.duration_closing if self.animation else 0, + # ).start(self) + + if self.type == "modal": + Animation( + md_bg_color=self.scrim_layer_color[:-1] + [0], + d=self.duration_closing if self.animation else 0, + ).start(self._scrim_layer) + + self.state = "close" + + def expand(self) -> None: + """Expand of bottom sheet.""" + + Animation( + y=0 + if not self.max_opening_height + else -(self.height - self.default_opening_height), + d=self.duration_opening if self.animation else 0, + t=self.opening_transition, + ).start(self) + + # Animation( + # radius=[0, 0, 0, 0], + # d=self.duration_opening if self.animation else 0, + # ).start(self) + + def open(self, *args) -> None: + """Opening of bottom sheet.""" + + anim = Animation( + y=-(self.height - self.default_opening_height), + d=self.duration_opening if self.animation else 0, + t=self.opening_transition, + ) + anim.bind( + on_complete=lambda x, y: self.dispatch("on_open"), + on_progress=lambda x, y, z: self.dispatch("on_progress", z), + ) + anim.start(self) + + if self.type == "modal": + alpha_channel_value = 100 / self.parent.height + Animation( + md_bg_color=self.scrim_layer_color[:-1] + [alpha_channel_value], + d=self.duration_opening if self.animation else 0, + ).start(self._scrim_layer) + + self.state = "open" + + def clear_content(self) -> None: + """Removes custom content from the bottom sheet.""" + + self.ids.container.clear_widgets() + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDBottomSheetDragHandle): + self.ids.drag_handle_container.add_widget(widget) + return + elif isinstance(widget, MDBottomSheetContent): + self.ids.container.add_widget(widget) + return + return super().add_widget(widget) + + def _set_state(self, y): + self._state = "none" + if y < self.height / 2: + self.dismiss() + elif y > self.height / 2: + self.expand() + + def _on_touch_down_layer(self, instance, touch): + if instance.collide_point(touch.x, touch.y): + if self._touch_sheet: + return True + + if self.state == "open" and not self.auto_dismiss: + return True + elif self.state == "open" and self.auto_dismiss: + self.dismiss() + return True class MDCustomBottomSheet(MDBottomSheet): - screen = ObjectProperty() """ - Custom content. - - :attr:`screen` is an :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. + .. deprecated:: 1.2.0 + Use :class:`~kivymd.uix.bottomsheet.bottomsheet.MDBottomSheet` + class instead. """ - def __init__(self, **kwargs): - super().__init__(**kwargs) - self._gl_content.add_widget(self.screen) - Clock.schedule_once( - lambda x: self.resize_content_layout(self._gl_content, self.screen), - 0, + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + Logger.warning( + "KivyMD: " + "The `MDCustomBottomSheet` class has been deprecated. " + "Use the `MDBottomSheet` class instead." ) class MDListBottomSheet(MDBottomSheet): - sheet_list = ObjectProperty() """ - :attr:`sheet_list` is an :class:`~kivy.properties.ObjectProperty` - and defaults to `None`. + .. deprecated:: 1.2.0 + Use :class:`~kivymd.uix.bottomsheet.bottomsheet.MDBottomSheet` + class instead. """ - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.sheet_list = SheetList(size_hint_y=None) - self._gl_content.add_widget(self.sheet_list) - Clock.schedule_once( - lambda x: self.resize_content_layout( - self._gl_content, self.sheet_list - ), - 0, + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + Logger.warning( + "KivyMD: " + "The `MDListBottomSheet` class has been deprecated. " + "Use the `MDBottomSheet` class instead." ) - def add_item(self, text, callback, icon=None): - """ - :arg text: element text; - :arg callback: function that will be called when clicking on an item; - :arg icon: which will be used as an icon to the left of the item; - """ - - if icon: - item = OneLineIconListItem(text=text, on_release=callback) - item.add_widget(ListBottomSheetIconLeft(icon=icon)) - else: - item = OneLineListItem(text=text, on_release=callback) - item.bind(on_release=lambda x: self.dismiss()) - self.sheet_list.ids.box_sheet_list.add_widget(item) - - -class GridBottomSheetItem(ButtonBehavior, BoxLayout): - source = StringProperty() - """ - Icon path if you use a local image or icon name - if you use icon names from a file ``kivymd/icon_definitions.py``. - - :attr:`source` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - caption = StringProperty() - """ - Item text. - - :attr:`caption` is an :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - icon_size = NumericProperty("24sp") - """ - Icon size. - - :attr:`caption` is an :class:`~kivy.properties.StringProperty` - and defaults to `'24sp'`. - """ - class MDGridBottomSheet(MDBottomSheet): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.sheet_list = SheetList(size_hint_y=None) - self.sheet_list.ids.box_sheet_list.cols = 3 - self.sheet_list.ids.box_sheet_list.padding = (dp(16), 0, dp(16), dp(96)) - self._gl_content.add_widget(self.sheet_list) - Clock.schedule_once( - lambda x: self.resize_content_layout( - self._gl_content, self.sheet_list - ), - 0, + """ + .. deprecated:: 1.2.0 + Use :class:`~kivymd.uix.bottomsheet.bottomsheet.MDBottomSheet` + class instead. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + Logger.warning( + "KivyMD: " + "The `MDGridBottomSheet` class has been deprecated. " + "Use the `MDBottomSheet` class instead." ) - - def add_item(self, text, callback, icon_src): - """ - :arg text: element text; - :arg callback: function that will be called when clicking on an item; - :arg icon_src: icon item; - """ - - def tap_on_item(instance): - callback(instance) - self.dismiss() - - item = GridBottomSheetItem( - caption=text, on_release=tap_on_item, source=icon_src - ) - if len(self._gl_content.children) % 3 == 0: - self._gl_content.height += dp(96) - self.sheet_list.ids.box_sheet_list.add_widget(item) diff --git a/sbapp/kivymd/uix/boxlayout.py b/sbapp/kivymd/uix/boxlayout.py index 6922de7..c5f3e75 100644 --- a/sbapp/kivymd/uix/boxlayout.py +++ b/sbapp/kivymd/uix/boxlayout.py @@ -87,11 +87,14 @@ __all__ = ("MDBoxLayout",) from kivy.uix.boxlayout import BoxLayout +from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior -class MDBoxLayout(DeclarativeBehavior, BoxLayout, MDAdaptiveWidget): +class MDBoxLayout( + DeclarativeBehavior, ThemableBehavior, BoxLayout, MDAdaptiveWidget +): """ Box layout class. diff --git a/sbapp/kivymd/uix/button/button.kv b/sbapp/kivymd/uix/button/button.kv index 4cee339..77f7d09 100644 --- a/sbapp/kivymd/uix/button/button.kv +++ b/sbapp/kivymd/uix/button/button.kv @@ -2,6 +2,7 @@ canvas: Clear Color: + group: "bg-color" rgba: self._md_bg_color \ if not self.disabled else \ @@ -12,6 +13,7 @@ source: self.source if hasattr(self, "source") else "" radius: [root._radius, ] Color: + group: "outline-color" rgba: root._line_color \ if not root.disabled else \ @@ -92,9 +94,11 @@ root.theme_cls.disabled_hint_text_color \ if not root.disabled_color else \ root.disabled_color - - on_icon: - if self.icon not in md_icons.keys(): self.size_hint = (1, 1) + # Fix https://github.com/kivymd/KivyMD/issues/1448 + # TODO: Perhaps this change may affect other widgets. + # You need to create tests. + # on_icon: + # if self.icon not in md_icons.keys(): self.size_hint = (1, 1) theme_text_color: root._theme_icon_color diff --git a/sbapp/kivymd/uix/button/button.py b/sbapp/kivymd/uix/button/button.py index 0fac0e2..5750a33 100755 --- a/sbapp/kivymd/uix/button/button.py +++ b/sbapp/kivymd/uix/button/button.py @@ -679,6 +679,15 @@ from kivy.weakproxy import WeakProxy from kivymd import uix_path from kivymd.color_definitions import text_colors from kivymd.font_definitions import theme_font_styles +from kivymd.material_resources import ( + FLOATING_ACTION_BUTTON_M2_ELEVATION, + FLOATING_ACTION_BUTTON_M2_OFFSET, + FLOATING_ACTION_BUTTON_M3_ELEVATION, + FLOATING_ACTION_BUTTON_M3_OFFSET, + FLOATING_ACTION_BUTTON_M3_SOFTNESS, + RAISED_BUTTON_OFFSET, + RAISED_BUTTON_SOFTNESS, +) from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import ( CommonElevationBehavior, @@ -704,62 +713,6 @@ theme_text_color_options = ( "ContrastParentBackground", ) -# FIXME: If you set a new elevation value for the button -# (press the "Set elevation" button), then disable the button -# (press the "Disabled" button), and then enable the button -# (press the "Undisabled" button), then the previously set elevation value is -# reset to zero. -# In addition, if you set a new elevation value -# (press the "Set elevation" button) and click on the button for which we set -# the elevation value, then the new elevation value will receive the previous -# elevation value. This problem is only related to the buttons. -# For example, there is no such problem for the MDCard widget. - -""" -from kivy.lang import Builder - -from kivymd.app import MDApp - -KV = ''' -MDScreen: - - MDRaisedButton: - size_hint: .5, .5 - id: button - pos_hint: {"center_x": .5, "center_y": .5} - elevation: 0 - - MDBoxLayout: - adaptive_size: True - pos_hint: {"center_x": .5} - spacing: 12 - padding: 12 - - MDRaisedButton: - text: "Set elevation" - pos_hint: {"center_x": .5, "bottom": 1} - on_release: button.elevation = 4 - - MDRaisedButton: - text: "Disabled" - pos_hint: {"center_x": .5, "bottom": 1} - on_release: button.disabled = True - - MDRaisedButton: - text: "Undisabled" - pos_hint: {"center_x": .5, "bottom": 1} - on_release: button.disabled = False -''' - - -class Test(MDApp): - def build(self): - return Builder.load_string(KV) - - -Test().run() -""" - class BaseButton( DeclarativeBehavior, @@ -772,7 +725,12 @@ class BaseButton( Base class for all buttons. For more information, see in the - :class:`~kivy.uix.anchorlayout.AnchorLayout` class documentation. + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivy.uix.anchorlayout.AnchorLayout` + classes documentation. """ padding = VariableListProperty([dp(16), dp(8), dp(16), dp(8)]) @@ -1208,7 +1166,7 @@ class ButtonElevationBehaviour(CommonElevationBehavior): _elevation_raised = NumericProperty() _anim_raised = ObjectProperty(None, allownone=True) - _default_elevation = 3 + _default_elevation = 2 def __init__(self, **kwargs): super().__init__(**kwargs) @@ -1220,8 +1178,9 @@ class ButtonElevationBehaviour(CommonElevationBehavior): self.on_disabled(self, self.disabled) def create_anim_raised(self, *args) -> None: - self._elevation_raised = self.elevation + 1.2 - self._anim_raised = Animation(elevation=self.elevation + 1, d=0.15) + if self.elevation: + self._elevation_raised = self.elevation + self._anim_raised = Animation(elevation=self.elevation + 1, d=0.15) def on_touch_down(self, touch): if not self.disabled: @@ -1231,21 +1190,21 @@ class ButtonElevationBehaviour(CommonElevationBehavior): return False if self in touch.ud: return False - if self._anim_raised: + if self._anim_raised and self.elevation: self._anim_raised.start(self) return super().on_touch_down(touch) def on_touch_up(self, touch): if not self.disabled: - if touch.grab_current is not self: + if self in touch.ud: self.stop_elevation_anim() return super().on_touch_up(touch) - self.stop_elevation_anim() return super().on_touch_up(touch) def stop_elevation_anim(self): Animation.cancel_all(self, "elevation") - self.elevation = self._elevation_raised - 1 + if self._anim_raised and self.elevation: + self.elevation = self._elevation_raised class ButtonContentsText: @@ -1313,6 +1272,10 @@ class MDFlatButton(BaseButton, ButtonContentsText): """ A flat rectangular button with (by default) no border or background. Text is the default text color. + + For more information, see in the + :class:`~BaseButton` and :class:`~ButtonContentsText` + classes documentation. """ padding = VariableListProperty([dp(8), dp(8), dp(8), dp(8)]) @@ -1334,6 +1297,12 @@ class MDRaisedButton(BaseButton, ButtonElevationBehaviour, ButtonContentsText): """ A flat button with (by default) a primary color fill and matching color text. + + For more information, see in the + :class:`~BaseButton` and + :class:`~ButtonElevationBehaviour` and + :class:`~ButtonContentsText` + classes documentation. """ # FIXME: Move the underlying attributes to the :class:`~BaseButton` class. @@ -1345,15 +1314,19 @@ class MDRaisedButton(BaseButton, ButtonElevationBehaviour, ButtonContentsText): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.shadow_softness = 8 - self.shadow_offset = (0, 2) - self.shadow_radius = self._radius * 2 + self.shadow_softness = RAISED_BUTTON_SOFTNESS + self.shadow_offset = RAISED_BUTTON_OFFSET + # self.shadow_radius = self._radius * 2 class MDRectangleFlatButton(BaseButton, ButtonContentsText): """ A flat button with (by default) a primary color border and primary color text. + + For more information, see in the + :class:`~BaseButton` and :class:`~ButtonContentsText` + classes documentation. """ _default_line_color = None @@ -1368,6 +1341,12 @@ class MDRectangleFlatIconButton( """ A flat button with (by default) a primary color border, primary color text and a primary color icon on the left. + + For more information, see in the + :class:`~BaseButton` and + :class:`~OldButtonIconMixin` and + :class:`~ButtonContentsIconText` + classes documentation. """ _default_line_color = None @@ -1382,6 +1361,10 @@ class MDRoundFlatButton(BaseButton, ButtonContentsText): """ A flat button with (by default) fully rounded corners, a primary color border and primary color text. + + For more information, see in the + :class:`~BaseButton` and :class:`~ButtonContentsText` + classes documentation. """ _default_line_color = None @@ -1400,6 +1383,12 @@ class MDRoundFlatIconButton( """ A flat button with (by default) rounded corners, a primary color border, primary color text and a primary color icon on the left. + + For more information, see in the + :class:`~BaseButton` and + :class:`~OldButtonIconMixin` and + :class:`~ButtonContentsIconText` + classes documentation. """ _default_line_color = None @@ -1418,6 +1407,10 @@ class MDFillRoundFlatButton(BaseButton, ButtonContentsText): """ A flat button with (by default) rounded corners, a primary color fill and primary color text. + + For more information, see in the + :class:`~BaseButton` and :class:`~ButtonContentsText` + classes documentation. """ _default_md_bg_color = None @@ -1436,6 +1429,12 @@ class MDFillRoundFlatIconButton( """ A flat button with (by default) rounded corners, a primary color fill, primary color text and a primary color icon on the left. + + For more information, see in the + :class:`~BaseButton` and + :class:`~OldButtonIconMixin` and + :class:`~ButtonContentsIconText` + classes documentation. """ _default_md_bg_color = None @@ -1451,7 +1450,14 @@ class MDFillRoundFlatIconButton( class MDIconButton(BaseButton, OldButtonIconMixin, ButtonContentsIcon): - """A simple rounded icon button.""" + """ + A simple rounded icon button. + + For more information, see in the + :class:`~BaseButton` and + :class:`~OldButtonIconMixin` and + :class:`~ButtonContentsIcon` classes documentation. + """ icon = StringProperty("checkbox-blank-circle") """ @@ -1489,6 +1495,12 @@ class MDFloatingActionButton( Implementation `FAB `_ button. + + For more information, see in the + :class:`~BaseButton` and + :class:`~OldButtonIconMixin` and + :class:`~ButtonElevationBehaviour` and + :class:`~ButtonContentsIcon` classes documentation. """ type = OptionProperty("standard", options=["small", "large", "standard"]) @@ -1530,10 +1542,13 @@ class MDFloatingActionButton( def set__radius(self, *args) -> None: if self.theme_cls.material_style == "M2": self.shadow_radius = self.height / 2 + self.elevation = FLOATING_ACTION_BUTTON_M2_ELEVATION + self.shadow_offset = FLOATING_ACTION_BUTTON_M2_OFFSET self.rounded_button = True else: - self.shadow_softness = 8 - self.shadow_offset = (0, 2) + self.shadow_softness = FLOATING_ACTION_BUTTON_M3_SOFTNESS + self.shadow_offset = FLOATING_ACTION_BUTTON_M3_OFFSET + self.elevation = FLOATING_ACTION_BUTTON_M3_ELEVATION self.rounded_button = False if self.type == "small": @@ -1566,6 +1581,14 @@ class MDFloatingActionButton( class MDTextButton(ButtonBehavior, MDLabel): + """ + Text button class. + + For more information, see in the + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.label.MDLabel` classes documentation. + """ + color = ColorProperty(None) """ Button color in (r, g, b, a) or string format. @@ -1638,6 +1661,12 @@ class MDFloatingActionButtonSpeedDial( For more information, see in the :class:`~kivy.uix.floatlayout.FloatLayout` class documentation. + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.floatlayout.FloatLayout` + lasses documentation. + :Events: :attr:`on_open` Called when a stack is opened. @@ -1868,7 +1897,7 @@ class MDFloatingActionButtonSpeedDial( """ Background color of root button in (r, g, b, a) or string format. - .. code-clock:: kv + .. code-block:: kv MDFloatingActionButtonSpeedDial: bg_color_root_button: "red" @@ -1884,7 +1913,7 @@ class MDFloatingActionButtonSpeedDial( """ Background color of the stack buttons in (r, g, b, a) or string format. - .. code-clock:: kv + .. code-block:: kv MDFloatingActionButtonSpeedDial: bg_color_root_button: "red" @@ -1901,7 +1930,7 @@ class MDFloatingActionButtonSpeedDial( """ The color icon of the stack buttons in (r, g, b, a) or string format. - .. code-clock:: kv + .. code-block:: kv MDFloatingActionButtonSpeedDial: bg_color_root_button: "red" @@ -1919,7 +1948,7 @@ class MDFloatingActionButtonSpeedDial( """ The color icon of the root button in (r, g, b, a) or string format. - .. code-clock:: kv + .. code-block:: kv MDFloatingActionButtonSpeedDial: bg_color_root_button: "red" @@ -1939,7 +1968,7 @@ class MDFloatingActionButtonSpeedDial( Background color for the floating text of the buttons in (r, g, b, a) or string format. - .. code-clock:: kv + .. code-block:: kv MDFloatingActionButtonSpeedDial: bg_hint_color: "red" diff --git a/sbapp/kivymd/uix/card/card.py b/sbapp/kivymd/uix/card/card.py index d224ae1..7e0bdf9 100755 --- a/sbapp/kivymd/uix/card/card.py +++ b/sbapp/kivymd/uix/card/card.py @@ -94,6 +94,7 @@ An example of the implementation of a card in the style of material design versi style=style, text=style.capitalize(), md_bg_color=styles[style], + shadow_offset=(0, -1), ) ) @@ -152,10 +153,9 @@ An example of the implementation of a card in the style of material design versi ), line_color=(0.2, 0.2, 0.2, 0.8), style=style, - padding="4dp", - size_hint=(None, None), - size=("200dp", "100dp"), + text=style.capitalize(), md_bg_color=styles[style], + shadow_offset=(0, -1), ) ) @@ -699,7 +699,12 @@ from kivy.utils import get_color_from_hex from kivymd import uix_path from kivymd.color_definitions import colors +from kivymd.material_resources import ( + CARD_STYLE_ELEVATED_M3_ELEVATION, + CARD_STYLE_OUTLINED_FILLED_M3_ELEVATION, +) from kivymd.theming import ThemableBehavior +from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import ( BackgroundColorBehavior, CommonElevationBehavior, @@ -716,12 +721,17 @@ with open( Builder.load_string(kv_file.read()) -class MDSeparator(ThemableBehavior, MDBoxLayout): - """A separator line.""" +class MDSeparator(MDBoxLayout): + """ + A separator line. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. + """ color = ColorProperty(None) """ - Separator color. + Separator color in (r, g, b, a) or string format. :attr:`color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -743,6 +753,7 @@ class MDSeparator(ThemableBehavior, MDBoxLayout): class MDCard( DeclarativeBehavior, + MDAdaptiveWidget, ThemableBehavior, BackgroundColorBehavior, RectangularRippleBehavior, @@ -750,6 +761,21 @@ class MDCard( FocusBehavior, BoxLayout, ): + """ + Card class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.uix.MDAdaptiveWidget` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.BackgroundColorBehavior` and + :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and + :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.FocusBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` and + classes documentation. + """ + focus_behavior = BooleanProperty(False) """ Using focus when hovering over a card. @@ -824,9 +850,9 @@ class MDCard( def set_elevation(self) -> None: if self.theme_cls.material_style == "M3": if self.style == "outlined" or self.style == "filled": - self.elevation = 0 + self.elevation = CARD_STYLE_OUTLINED_FILLED_M3_ELEVATION elif self.style == "elevated": - self.elevation = 2 + self.elevation = CARD_STYLE_ELEVATED_M3_ELEVATION def set_radius(self) -> None: if ( @@ -848,6 +874,11 @@ class MDCard( class MDCardSwipe(MDRelativeLayout): """ + Card swipe class. + + For more information, see in the + :class:`~kivymd.uix.relativelayout.MDRelativeLayout` class documentation. + :Events: :attr:`on_swipe_complete` Called when a swipe of card is completed. @@ -1065,7 +1096,11 @@ class MDCardSwipe(MDRelativeLayout): class MDCardSwipeFrontBox(MDCard): - pass + """ + Card swipe front box. + + For more information, see in the :class:`~MDCard` class documentation. + """ class MDCardSwipeLayerBox(MDBoxLayout): diff --git a/sbapp/kivymd/uix/carousel.py b/sbapp/kivymd/uix/carousel.py index 4b56dba..5f68886 100644 --- a/sbapp/kivymd/uix/carousel.py +++ b/sbapp/kivymd/uix/carousel.py @@ -57,10 +57,11 @@ MDCarousel from kivy.animation import Animation from kivy.uix.carousel import Carousel +from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import DeclarativeBehavior -class MDCarousel(DeclarativeBehavior, Carousel): +class MDCarousel(DeclarativeBehavior, ThemableBehavior, Carousel): """ based on kivy's carousel. diff --git a/sbapp/kivymd/uix/chip/__init__.py b/sbapp/kivymd/uix/chip/__init__.py index ec6050b..fc6a3a2 100644 --- a/sbapp/kivymd/uix/chip/__init__.py +++ b/sbapp/kivymd/uix/chip/__init__.py @@ -1 +1 @@ -from .chip import MDChip # NOQA F401 +from .chip import MDChip, MDChipText # NOQA F401 diff --git a/sbapp/kivymd/uix/chip/chip.kv b/sbapp/kivymd/uix/chip/chip.kv index 361bc98..e008dd2 100644 --- a/sbapp/kivymd/uix/chip/chip.kv +++ b/sbapp/kivymd/uix/chip/chip.kv @@ -1,110 +1,38 @@ - - scale_value_x: 0 - scale_value_y: 0 - scale_value_z: 0 - - size_hint_y: None height: "32dp" - spacing: "8dp" adaptive_width: True - radius: 16 if self.radius == [0, 0, 0, 0] else self.radius - padding: - "12dp" if not self.icon_left else "4dp", \ - 0, \ - "12dp" if not self.icon_right else "8dp", \ - 0 + radius: + 16 \ + if self.radius == [0, 0, 0, 0] else \ + (max(self.radius) if max(self.radius) < self.height / 2 else 16) md_bg_color: + ( \ ( \ app.theme_cls.bg_darkest \ if app.theme_cls.theme_style == "Light" else \ app.theme_cls.bg_light \ ) \ - if not self.disabled else app.theme_cls.disabled_hint_text_color + if not self._origin_md_bg_color else \ + self._origin_md_bg_color + ) \ + if not self.disabled else app.theme_cls.disabled_primary_color + line_color: + app.theme_cls.disabled_hint_text_color \ + if self.disabled else ( \ + self._origin_line_color \ + if self._origin_line_color else \ + self.line_color \ + ) - canvas.before: - Color: - rgba: - self.line_color \ - if not self.disabled else \ - app.theme_cls.disabled_hint_text_color - Line: - width: 1 - rounded_rectangle: - ( \ - self.x, \ - self.y, \ - self.width, \ - self.height, \ - *self.radius, \ - self.height \ - ) + LeadingIconContainer: + id: leading_icon_container + adaptive_width: True - MDRelativeLayout: - id: relative_box - size_hint: None, None - size: ("24dp", "24dp") if root.icon_left else (0, 0) - pos_hint: {"center_y": .5} - radius: [int(self.height / 2),] + LabelTextContainer: + id: label_container + adaptive_width: True - MDIcon: - id: icon_left - icon: root.icon_left - size_hint: None, None - size: ("28dp", "28dp") if root.icon_left else (0, 0) - theme_text_color: "Custom" - pos_hint: {"center_y": .5} - pos: 0, -2 - text_color: - ( \ - root.icon_left_color \ - if root.icon_left_color else \ - root.theme_cls.disabled_hint_text_color \ - ) \ - if not self.disabled else app.theme_cls.disabled_hint_text_color - - MDBoxLayout: - id: icon_left_box - size_hint: None, None - radius: [int(self.height / 2),] - size: ("28dp", "28dp") if root.icon_left else (0, 0) - pos: 0, -2 - - MDScalableCheckIcon: - id: check_icon - icon: "check" - size_hint: None, None - size: "28dp", "28dp" - color: (1, 1, 1, 1) if not root.icon_check_color else root.icon_check_color - pos: 2, -2 - - MDLabel: - id: label - text: root.text - adaptive_size: True - markup: True - pos_hint: {"center_y": .5} - color: - ( \ - root.text_color \ - if root.text_color else \ - root.theme_cls.disabled_hint_text_color \ - ) \ - if not self.disabled else app.theme_cls.disabled_hint_text_color - - MDIcon: - id: icon_right - icon: root.icon_right - size_hint: None, None - size: ("18dp", "18dp") if root.icon_right else (0, 0) - font_size: "18sp" if root.icon_right else 0 - theme_text_color: "Custom" - pos_hint: {"center_y": .5} - text_color: - ( \ - root.icon_right_color \ - if root.icon_right_color else \ - root.theme_cls.disabled_hint_text_color \ - ) \ - if not self.disabled else app.theme_cls.disabled_hint_text_color + TrailingIconContainer: + id: trailing_icon_container + adaptive_width: True diff --git a/sbapp/kivymd/uix/chip/chip.py b/sbapp/kivymd/uix/chip/chip.py index 03e1a98..c9f3593 100755 --- a/sbapp/kivymd/uix/chip/chip.py +++ b/sbapp/kivymd/uix/chip/chip.py @@ -4,9 +4,12 @@ Components/Chip .. seealso:: - `Material Design spec, Chips `_ + `Material Design spec, Chips `_ -.. rubric:: Chips are compact elements that represent an input, attribute, or action. +.. rubric:: Chips can show multiple interactive elements together in the same + area, such as a list of selectable movie times, or a series of email + contacts. There are four types of chips: assist, filter, input, and + suggestion. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chips.png :align: center @@ -14,6 +17,633 @@ Components/Chip Usage ----- +.. tabs:: + + .. tab:: Declarative KV style + + .. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDChip: + pos_hint: {"center_x": .5, "center_y": .5} + + MDChipText: + text: "MDChip" + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + + .. tab:: Declarative Python style + + .. code-block:: python + + from kivymd.app import MDApp + from kivymd.uix.chip import MDChip, MDChipText + from kivymd.uix.screen import MDScreen + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return ( + MDScreen( + MDChip( + MDChipText( + text="MDChip" + ), + pos_hint={"center_x": .5, "center_y": .5}, + ) + ) + ) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip.png + :align: center + +Anatomy +------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/anatomy-chip.png + :align: center + +1. Container +2. Label text +3. Leading icon or image (optional) +4. Trailing remove icon (optional, input & filter chips only) + +Container +--------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/radius-chip.png + :align: center + +All chips are slightly rounded with an 8dp corner. + +Shadows and elevation +--------------------- + +Chip containers can be elevated if the placement requires protection, such as +on top of an image. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/shadows-elevation-chip.png + :align: center + +The following types of chips are available: +------------------------------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/available-type-chips.png + :align: center + +- Assist_ +- Filter_ +- Input_ +- Suggestion_ + +.. Assist: +Assist +------ + +`Assist chips `_ +represent smart or automated actions that can span multiple apps, such as +opening a calendar event from the home screen. Assist chips function as +though the user asked an assistant to complete the action. They should appear +dynamically and contextually in a UI. + +An alternative to assist chips are buttons, which should appear persistently +and consistently. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/assist-chip.png + :align: center + +Example of assist +----------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + + adaptive_size: True + theme_text_color: "Custom" + text_color: "#e6e9df" + + + + # Custom attribute. + text: "" + icon: "" + + # Chip attribute. + type: "assist" + md_bg_color: "#2a3127" + line_color: "grey" + elevation: 1 + shadow_softness: 2 + + MDChipLeadingIcon: + icon: root.icon + theme_text_color: "Custom" + text_color: "#68896c" + + MDChipText: + text: root.text + theme_text_color: "Custom" + text_color: "#e6e9df" + + + MDScreen: + + FitImage: + source: "bg.png" + + MDBoxLayout: + orientation: "vertical" + adaptive_size: True + pos_hint: {"center_y": .6, "center_x": .5} + + CommonLabel: + text: "in 10 mins" + bold: True + pos_hint: {"center_x": .5} + + CommonLabel: + text: "Therapy with Thea" + font_style: "H3" + padding_y: "12dp" + + CommonLabel: + text: "Video call" + font_style: "H5" + pos_hint: {"center_x": .5} + + MDBoxLayout: + adaptive_size: True + pos_hint: {"center_x": .5} + spacing: "12dp" + padding: 0, "24dp", 0, 0 + + CommonAssistChip: + text: "Home office" + icon: "map-marker" + + CommonAssistChip: + text: "Chat" + icon: "message" + + MDWidget: + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.primary_palette = "Teal" + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-assist-chip.png + :align: center + +.. Filter: +Filter +------ + +`Filter chips `_ +use tags or descriptive words to filter content. They can be a good alternative +to toggle buttons or checkboxes. + +Tapping on a filter chip activates it and appends a leading checkmark icon to +the starting edge of the chip label. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/filter-chip.png + :align: center + +Example of filtering +-------------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import StringProperty, ListProperty + + from kivymd.app import MDApp + from kivymd.uix.chip import MDChip, MDChipText + from kivymd.uix.list import OneLineIconListItem + from kivymd.icon_definitions import md_icons + from kivymd.uix.screen import MDScreen + from kivymd.utils import asynckivy + + Builder.load_string( + ''' + + + IconLeftWidget: + icon: root.icon + + + + + MDBoxLayout: + orientation: "vertical" + spacing: "14dp" + padding: "20dp" + + MDTextField: + id: search_field + hint_text: "Search icon" + mode: "rectangle" + icon_left: "magnify" + on_text: root.set_list_md_icons(self.text, True) + + MDBoxLayout: + id: chip_box + spacing: "12dp" + adaptive_height: True + + RecycleView: + id: rv + viewclass: "CustomOneLineIconListItem" + key_size: "height" + + RecycleBoxLayout: + padding: dp(10) + default_size: None, dp(48) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + orientation: "vertical" + ''' + ) + + + class CustomOneLineIconListItem(OneLineIconListItem): + icon = StringProperty() + + + class PreviewIconsScreen(MDScreen): + filter = ListProperty() # list of tags for filtering icons + + def set_filter_chips(self): + '''Asynchronously creates and adds chips to the container.''' + + async def set_filter_chips(): + for tag in ["Outline", "Off", "On"]: + await asynckivy.sleep(0) + chip = MDChip( + MDChipText( + text=tag, + ), + type="filter", + md_bg_color="#303A29", + ) + chip.bind(active=lambda x, y, z=tag: self.set_filter(y, z)) + self.ids.chip_box.add_widget(chip) + + asynckivy.start(set_filter_chips()) + + def set_filter(self, active: bool, tag: str) -> None: + '''Sets a list of tags for filtering icons.''' + + if active: + self.filter.append(tag) + else: + self.filter.remove(tag) + + def set_list_md_icons(self, text="", search=False) -> None: + '''Builds a list of icons.''' + + def add_icon_item(name_icon): + self.ids.rv.data.append( + { + "icon": name_icon, + "text": name_icon, + } + ) + + self.ids.rv.data = [] + for name_icon in md_icons.keys(): + for tag in self.filter: + if tag.lower() in name_icon: + if search: + if text in name_icon: + add_icon_item(name_icon) + else: + add_icon_item(name_icon) + + + class Example(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = PreviewIconsScreen() + + def build(self) -> PreviewIconsScreen: + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "LightGreen" + return self.screen + + def on_start(self) -> None: + self.screen.set_list_md_icons() + self.screen.set_filter_chips() + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-filtering-icons-chip.gif + :align: center + +Tap a chip to select it. Multiple chips can be selected or unselected: + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.chip import MDChip, MDChipText + from kivymd.uix.screen import MDScreen + from kivymd.utils import asynckivy + + Builder.load_string( + ''' + + + MDBoxLayout: + orientation: "vertical" + spacing: "14dp" + padding: "20dp" + + MDLabel: + adaptive_height: True + text: "Select Type" + + MDStackLayout: + id: chip_box + spacing: "12dp" + adaptive_height: True + + MDWidget: + + MDFlatButton: + text: "Uncheck chips" + pos: "20dp", "20dp" + on_release: root.unchecks_chips() + ''' + ) + + + class ChipScreen(MDScreen): + async def create_chips(self): + '''Asynchronously creates and adds chips to the container.''' + + for tag in ["Extra Soft", "Soft", "Medium", "Hard"]: + await asynckivy.sleep(0) + self.ids.chip_box.add_widget( + MDChip( + MDChipText( + text=tag, + ), + type="filter", + md_bg_color="#303A29", + active=True, + ) + ) + + def unchecks_chips(self) -> None: + '''Removes marks from all chips.''' + + for chip in self.ids.chip_box.children: + if chip.active: + chip.active = False + + + class Example(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = ChipScreen() + + def build(self) -> ChipScreen: + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "LightGreen" + return self.screen + + def on_start(self) -> None: + asynckivy.start(self.screen.create_chips()) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-filtering-icons-chip-2.gif + :align: center + +Alternatively, a single chip can be selected. +This offers an alternative to toggle buttons, radio buttons, or single select +menus: + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.chip import MDChip, MDChipText + from kivymd.uix.screen import MDScreen + from kivymd.utils import asynckivy + + Builder.load_string( + ''' + + + MDBoxLayout: + orientation: "vertical" + spacing: "14dp" + padding: "20dp" + + MDLabel: + adaptive_height: True + text: "Select Type" + + MDStackLayout: + id: chip_box + spacing: "12dp" + adaptive_height: True + + MDFillRoundFlatButton: + text: "Add to cart" + md_bg_color: "green" + size_hint_x: 1 + + MDWidget: + ''' + ) + + + class ChipScreen(MDScreen): + async def create_chips(self): + '''Asynchronously creates and adds chips to the container.''' + + for tag in ["Extra Soft", "Soft", "Medium", "Hard"]: + await asynckivy.sleep(0) + chip = MDChip( + MDChipText( + text=tag, + ), + type="filter", + md_bg_color="#303A29", + + ) + chip.bind(active=self.uncheck_chip) + self.ids.chip_box.add_widget(chip) + + def uncheck_chip(self, current_chip: MDChip, active: bool) -> None: + '''Removes a mark from an already marked chip.''' + + if active: + for chip in self.ids.chip_box.children: + if current_chip is not chip: + if chip.active: + chip.active = False + + + class Example(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = ChipScreen() + + def build(self) -> ChipScreen: + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "LightGreen" + return self.screen + + def on_start(self) -> None: + asynckivy.start(self.screen.create_chips()) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-filtering-single-select.gif + :align: center + +.. Input: +Input +----- + +`Input chips `_ +represent discrete pieces of information entered by a user, such as Gmail +contacts or filter options within a search field. + +They enable user input and verify that input by converting text into chips. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/input-chip.png + :align: center + +Example of input +---------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDChip: + pos_hint: {"center_x": .5, "center_y": .5} + type: "input" + line_color: "grey" + _no_ripple_effect: True + + MDChipLeadingAvatar: + source: "data/logo/kivy-icon-128.png" + + MDChipText: + text: "MDChip" + + MDChipTrailingIcon: + icon: "close" + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-input-chip.png + :align: center + +.. Suggestion: +Suggestion +---------- + +`Suggestion chips `_ +help narrow a user’s intent by presenting dynamically generated suggestions, +such as possible responses or search filters. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/suggestion-chip.png + :align: center + +Example of suggestion +--------------------- + +.. code-block:: + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDChip: + pos_hint: {"center_x": .5, "center_y": .5} + type: "suggestion" + line_color: "grey" + + MDChipText: + text: "MDChip" + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/example-suggestion.png + :align: center + +API break +========= + +1.1.1 version +------------- + .. code-block:: python from kivy.lang import Builder @@ -40,286 +670,76 @@ Usage Test().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/ordinary-chip.png - :align: center - -Use with right icon -------------------- - -.. code-block:: kv - - MDChip: - text: "Portland" - icon_right: "close-circle-outline" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-right-icon.png - :align: center - -Use with left icon ------------------- - -.. code-block:: kv - - MDChip: - text: "Portland" - icon_left: "map-marker" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-left-icon.png - :align: center - -Use with custom left icon -------------------------- - -.. code-block:: kv - - MDChip: - text: "Portland" - icon_left: "avatar.png" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-custom-left-icon.png - :align: center - -Use with left and right icon ----------------------------- - -.. code-block:: kv - - MDChip: - text: "Portland" - icon_left: "avatar.png" - icon_right: "close-circle-outline" - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-left-right-icon.png - :align: center - -Use with outline ----------------- - -.. code-block:: kv - - MDChip: - text: "Portland" - icon_left: "avatar.png" - icon_right: "close-circle-outline" - line_color: app.theme_cls.disabled_hint_text_color - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-outline.png - :align: center - -Use with custom color ---------------------- - -.. code-block:: kv - - MDChip: - text: "Portland" - icon_left: "avatar.png" - icon_right: "close-circle-outline" - line_color: app.theme_cls.disabled_hint_text_color - md_bg_color: 1, 0, 0, .5 - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-custom-color.png - :align: center - -Use with elevation ------------------- - -.. code-block:: kv - - MDChip: - text: "Portland" - icon_left: "avatar.png" - icon_right: "close-circle-outline" - line_color: app.theme_cls.disabled_hint_text_color - md_bg_color: 1, 0, 0, .5 - elevation: 4 - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-with-elevation.png - :align: center - -Behavior -======== - -Long press on the chip, it will be marked. -When you click on the marked chip, the mark will be removed: - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-activate.gif - :align: center - -Examples -======== - -Multiple choose ---------------- - -Selecting a single choice chip automatically deselects all other chips in the set. +1.2.0 version +------------- .. code-block:: python - from kivy.animation import Animation from kivy.lang import Builder - from kivymd.uix.screen import MDScreen - from kivymd.uix.chip import MDChip from kivymd.app import MDApp KV = ''' - + MDScreen: - MDBoxLayout: - orientation: "vertical" - adaptive_size: True - spacing: "12dp" - padding: "56dp" + MDChip: pos_hint: {"center_x": .5, "center_y": .5} + line_color: "grey" + on_release: app.on_release_chip(self) - MDLabel: - text: "Multiple choice" - bold: True - font_style: "H5" - adaptive_size: True - - MDBoxLayout: - id: chip_box - adaptive_size: True - spacing: "8dp" - - MyChip: - text: "Elevator" - on_press: if self.active: root.removes_marks_all_chips() - - MyChip: - text: "Washer / Dryer" - on_press: if self.active: root.removes_marks_all_chips() - - MyChip: - text: "Fireplace" - on_press: if self.active: root.removes_marks_all_chips() - - - ScreenManager: - - MyScreen: + MDChipText: + text: "MDChip" ''' - class MyChip(MDChip): - icon_check_color = (0, 0, 0, 1) - text_color = (0, 0, 0, 0.5) - _no_ripple_effect = True - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.bind(active=self.set_chip_bg_color) - self.bind(active=self.set_chip_text_color) - - def set_chip_bg_color(self, instance_chip, active_value: int): - ''' - Will be called every time the chip is activated/deactivated. - Sets the background color of the chip. - ''' - - self.md_bg_color = ( - (0, 0, 0, 0.4) - if active_value - else ( - self.theme_cls.bg_darkest - if self.theme_cls.theme_style == "Light" - else ( - self.theme_cls.bg_light - if not self.disabled - else self.theme_cls.disabled_hint_text_color - ) - ) - ) - - def set_chip_text_color(self, instance_chip, active_value: int): - Animation( - color=(0, 0, 0, 1) if active_value else (0, 0, 0, 0.5), d=0.2 - ).start(self.ids.label) - - - class MyScreen(MDScreen): - def removes_marks_all_chips(self): - for instance_chip in self.ids.chip_box.children: - if instance_chip.active: - instance_chip.active = False - - - class Test(MDApp): + class Example(MDApp): def build(self): return Builder.load_string(KV) - - Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-multiple-choose.gif - :align: center - -Only choose ------------ - -Only one chip will be selected. - -.. code-block:: python - - KV = ''' - - - [...] - - MDBoxLayout: - id: chip_box - adaptive_size: True - spacing: "8dp" - - MyChip: - text: "Elevator" - on_active: if self.active: root.removes_marks_all_chips(self) - - MyChip: - text: "Washer / Dryer" - on_active: if self.active: root.removes_marks_all_chips(self) - - MyChip: - text: "Fireplace" - on_active: if self.active: root.removes_marks_all_chips(self) + def on_release_chip(self, instance_check): + print(instance_check) - [...] - ''' - - - class MyScreen(MDScreen): - def removes_marks_all_chips(self, selected_instance_chip): - for instance_chip in self.ids.chip_box.children: - if instance_chip != selected_instance_chip: - instance_chip.active = False - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/chip-only-choose.gif - :align: center + Example().run() """ -__all__ = ("MDChip",) +from __future__ import annotations + +__all__ = ( + "MDChip", + "MDChipLeadingAvatar", + "MDChipLeadingIcon", + "MDChipTrailingIcon", + "MDChipText", +) import os +from kivy import Logger from kivy.animation import Animation +from kivy.clock import Clock from kivy.lang import Builder from kivy.metrics import dp -from kivy.properties import BooleanProperty, ColorProperty, StringProperty +from kivy.properties import ( + BooleanProperty, + ColorProperty, + OptionProperty, + StringProperty, + VariableListProperty, +) from kivy.uix.behaviors import ButtonBehavior from kivymd import uix_path -from kivymd.theming import ThemableBehavior +from kivymd.material_resources import DEVICE_TYPE from kivymd.uix.behaviors import ( + CircularRippleBehavior, CommonElevationBehavior, RectangularRippleBehavior, ScaleBehavior, TouchBehavior, ) from kivymd.uix.boxlayout import MDBoxLayout -from kivymd.uix.label import MDIcon +from kivymd.uix.label import MDIcon, MDLabel with open( os.path.join(uix_path, "chip", "chip.kv"), encoding="utf-8" @@ -327,73 +747,206 @@ with open( Builder.load_string(kv_file.read()) +class BaseChipIcon( + CircularRippleBehavior, ScaleBehavior, ButtonBehavior, MDIcon +): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.ripple_scale = 1.5 + Clock.schedule_once(self.adjust_icon_size) + + def adjust_icon_size(self, *args) -> None: + # If the user has not changed the icon size, then we set the standard + # icon size according to the standards of material design version 3. + if ( + self.font_name == "Icons" + and self.theme_cls.font_styles["Icon"][1] == self.font_size + ): + self.font_size = ( + "18sp" + if not self.source and not isinstance(self, MDChipLeadingAvatar) + else "24sp" + ) + if self.source and isinstance(self, MDChipLeadingAvatar): + self.icon = self.source + self._size = [dp(28), dp(28)] + self.font_size = "28sp" + self.padding_x = "6dp" + self._no_ripple_effect = True + + +class LabelTextContainer(MDBoxLayout): + """Implements a container for the chip label.""" + + +class LeadingIconContainer(MDBoxLayout): + """Implements a container for the leading icon.""" + + +class TrailingIconContainer(MDBoxLayout): + """Implements a container for the trailing icon.""" + + +class MDChipLeadingAvatar(BaseChipIcon): + """ + Implements the leading avatar for the chip. + + For more information, see in the + :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and + :class:`~kivymd.uix.behaviors.ScaleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.label.MDIcon` + classes documentation. + """ + + +class MDChipLeadingIcon(BaseChipIcon): + """ + Implements the leading icon for the chip. + + For more information, see in the + :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and + :class:`~kivymd.uix.behaviors.ScaleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.label.MDIcon` + classes documentation. + """ + + +class MDChipTrailingIcon(BaseChipIcon): + """ + Implements the trailing icon for the chip. + + For more information, see in the + :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and + :class:`~kivymd.uix.behaviors.ScaleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.label.MDIcon` + classes documentation. + """ + + +class MDChipText(MDLabel): + """ + Implements the label for the chip. + + For more information, see in the + :class:`~kivymd.uix.label.MDLabel` classes documentation. + """ + + class MDChip( MDBoxLayout, - ThemableBehavior, RectangularRippleBehavior, ButtonBehavior, CommonElevationBehavior, TouchBehavior, ): - text = StringProperty() + """ + Chip class. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` and + :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.TouchBehavior` + classes documentation. + """ + + radius = VariableListProperty([dp(8)], length=4) + """ + Chip radius. + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[dp(8), dp(8), dp(8), dp(8)]`. + """ + + text = StringProperty(deprecated=True) """ Chip text. + .. deprecated:: 1.2.0 + :attr:`text` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ - icon_left = StringProperty() + type = OptionProperty( + "suggestion", options=["assist", "filter", "input", "suggestion"] + ) + """ + Type of chip. + + .. versionadded:: 1.2.0 + + Available options are: `'assist'`, `'filter'`, `'input'`, `'suggestion'`. + + :attr:`type` is an :class:`~kivy.properties.OptionProperty` + and defaults to `'suggestion'`. + """ + + icon_left = StringProperty(deprecated=True) """ Chip left icon. .. versionadded:: 1.0.0 + .. deprecated:: 1.2.0 + :attr:`icon_left` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ - icon_right = StringProperty() + icon_right = StringProperty(deprecated=True) """ Chip right icon. .. versionadded:: 1.0.0 + .. deprecated:: 1.2.0 + :attr:`icon_right` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ - text_color = ColorProperty(None) + text_color = ColorProperty(None, deprecated=True) """ - Chip's text color in ``rgba`` format. + Chip's text color in (r, g, b, a) or string format. + + .. deprecated:: 1.2.0 :attr:`text_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - icon_right_color = ColorProperty(None) + icon_right_color = ColorProperty(None, deprecated=True) """ - Chip's right icon color in ``rgba`` format. + Chip's right icon color in (r, g, b, a) or string format. .. versionadded:: 1.0.0 + .. deprecated:: 1.2.0 + :attr:`icon_right_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - icon_left_color = ColorProperty(None) + icon_left_color = ColorProperty(None, deprecated=True) """ - Chip's left icon color in ``rgba`` format. + Chip's left icon color in (r, g, b, a) or string format. .. versionadded:: 1.0.0 + .. deprecated:: 1.2.0 + :attr:`icon_left_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ icon_check_color = ColorProperty(None) """ - Chip's check icon color in ``rgba`` format. + Chip's check icon color in (r, g, b, a) or string format. .. versionadded:: 1.0.0 @@ -411,171 +964,281 @@ class MDChip( and defaults to `False`. """ - def __init__(self, **kwargs): - super().__init__(**kwargs) + selected_color = ColorProperty(None) + """ + The background color of the chip in the marked state in (r, g, b, a) + or string format. + + .. versionadded:: 1.2.0 + + :attr:`selected_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _current_md_bg_color = ColorProperty(None) + # A flag that disallow ripple animation of the chip + # at the time of clicking the chip icons. + _allow_chip_ripple = BooleanProperty(True) + # The flag signals the end of the ripple animation. + _anim_complete = BooleanProperty(False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) def on_long_touch(self, *args) -> None: - if self.active: - return - self.active = True if not self.active else False + if self.type == "filter": + self.active = not self.active + + def on_type(self, instance, value: str) -> None: + """Called when the values of :attr:`type` change.""" + + def adjust_padding(*args): + """ + According to the type of chip, it sets the margins according + to the specification of the material design version 3. + """ + + self.padding = { + "input": ( + "12dp" + if not self.ids.leading_icon_container.children + else ( + "5dp" + if not self.ids.leading_icon_container.children[ + 0 + ].source + else "16dp" + ), + 0, + "4dp", + 0, + ), + "assist": ( + "16dp" + if not self.ids.leading_icon_container.children + else "8dp", + 0, + "16dp" + if not self.ids.leading_icon_container.children + else "8dp", + 0, + ), + "suggestion": ( + "16dp" + if not self.ids.leading_icon_container.children + else "8dp", + 0, + "16dp", + 0, + ), + "filter": ( + "16dp" + if not self.ids.leading_icon_container.children + else ( + "8dp" + if not self.ids.leading_icon_container.children[ + 0 + ].source + else "4dp" + ), + 0, + "16dp" + if not self.ids.trailing_icon_container.children + else "8dp", + 0, + ), + }[value] + + Clock.schedule_once(adjust_padding) def on_active(self, instance_check, active_value: bool) -> None: + """Called when the values of :attr:`active` change.""" + if active_value: - self.do_animation_check((0, 0, 0, 0.4), 1) + self._current_md_bg_color = self.md_bg_color + + Clock.schedule_once(self.complete_anim_ripple, 0.5) + + def complete_anim_ripple(self, *args) -> None: + """Called at the end of the ripple animation.""" + + if self.active: + if not self.ids.leading_icon_container.children: + if self.type == "filter": + self.add_marked_icon_to_chip() + self.set_chip_bg_color( + self.selected_color + if self.selected_color + else self.theme_cls.primary_color + ) else: - self.do_animation_check((0, 0, 0, 0), 0) + if ( + self.ids.leading_icon_container.children + and self.ids.leading_icon_container.children[0].icon == "check" + ): + if self.type == "filter": + self.remove_marked_icon_from_chip() + self.set_chip_bg_color(self._current_md_bg_color) - def do_animation_check(self, md_bg_color: list, scale_value: int) -> None: - Animation(md_bg_color=md_bg_color, t="out_sine", d=0.1).start( - self.ids.icon_left_box + def remove_marked_icon_from_chip(self) -> None: + def remove_marked_icon_from_chip(*args): + self.ids.leading_icon_container.clear_widgets() + + if self.ids.leading_icon_container.children: + anim = Animation(scale_value_x=0, scale_value_y=0, d=0.2) + anim.bind(on_complete=remove_marked_icon_from_chip) + anim.start(self.ids.leading_icon_container.children[0]) + Animation( + padding=[dp(16), 0, dp(16), 0], + spacing=0, + d=0.2, + ).start(self) + + def add_marked_icon_to_chip(self) -> None: + """Adds and animates a check icon to the chip.""" + + icon_check = MDChipLeadingIcon( + icon="check", + pos_hint={"center_y": 0.5}, + font_size=dp(18), + scale_value_x=0, + scale_value_y=0, ) + icon_check.bind( + on_press=self._set_allow_chip_ripple, + on_release=self._set_allow_chip_ripple, + ) + self.ids.leading_icon_container.add_widget(icon_check) + # Animating the scale of the icon. + Animation(scale_value_x=1, scale_value_y=1, d=0.2).start(icon_check) + # Animating the padding of the chip. Animation( - scale_value_x=scale_value, - scale_value_y=scale_value, - scale_value_z=scale_value, - t="out_sine", - d=0.1, - ).start(self.ids.check_icon) + padding=[dp(18), 0, 0, 0], + spacing=dp(18) if self.type == "filter" else 0, + d=0.2, + ).start(self) - if not self.icon_left: - if scale_value: - self.ids.check_icon.x = -dp(4) - Animation(size=(dp(24), dp(24)), t="out_sine", d=0.1).start( - self.ids.relative_box - ) - else: - self.ids.check_icon.x = 0 - Animation(size=(0, 0), t="out_sine", d=0.1).start( - self.ids.relative_box - ) + def set_chip_bg_color(self, color: list | str) -> None: + """Animates the background color of the chip.""" + + if color: + Animation(md_bg_color=color, d=0.2).start(self) + self._anim_complete = not self._anim_complete def on_press(self, *args): if self.active: self.active = False - -class MDScalableCheckIcon(MDIcon, ScaleBehavior): - pos_hint = {"center_y": 0.5} - - -if __name__ == "__main__": - from kivymd.app import MDApp - from kivymd.uix.screen import MDScreen - - KV = """ - - - MDBoxLayout: - orientation: "vertical" - adaptive_size: True - spacing: "12dp" - padding: "56dp" - pos_hint: {"center_x": .5, "center_y": .5} - - MDLabel: - text: "Multiple choose" - bold: True - font_style: "H5" - adaptive_size: True - - MDBoxLayout: - id: chip_box - adaptive_size: True - spacing: "8dp" - - MyChip: - text: "Elevator" - on_press: if self.active: root.removes_marks_all_chips() - - MyChip: - text: "Washer / Dryer" - on_press: if self.active: root.removes_marks_all_chips() - - MyChip: - text: "Fireplace" - on_press: if self.active: root.removes_marks_all_chips() - - MDSeparator: - - MDLabel: - text: "Only choose" - bold: True - font_style: "H5" - adaptive_size: True - - MDBoxLayout: - id: chip_only_box - adaptive_size: True - spacing: "8dp" - - MyChip: - text: "Elevator" - on_active: if self.active: root.removes_marks_all_chips(self, False) - - MyChip: - text: "Washer / Dryer" - on_active: if self.active: root.removes_marks_all_chips(self, False) - - MyChip: - text: "Fireplace" - on_active: if self.active: root.removes_marks_all_chips(self, False) - - -ScreenManager: - - MyScreen: - """ - - class MyChip(MDChip): - icon_check_color = (0, 0, 0, 1) - text_color = (0, 0, 0, 0.5) - _no_ripple_effect = True - - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.bind(active=self.set_chip_bg_color) - self.bind(active=self.set_chip_text_color) - - def set_chip_bg_color(self, instance_chip, active_value: int): - """ - Will be called every time the chip is activated/deactivated. - Sets the background color of the chip. - """ - - self.md_bg_color = ( - (0, 0, 0, 0.4) - if active_value - else ( - self.theme_cls.bg_darkest - if self.theme_cls.theme_style == "Light" - else ( - self.theme_cls.bg_light - if not self.disabled - else self.theme_cls.disabled_hint_text_color + def add_widget(self, widget, *args, **kwargs): + def add_icon_leading_trailing(container): + if len(container.children): + type_icon = ( + "'leading'" + if isinstance( + widget, (MDChipLeadingIcon, MDChipLeadingAvatar) ) + else "'trailing'" + ) + Logger.warning( + f"KivyMD: " + f"Do not use more than one {type_icon} icon. " + f"This is contrary to the material design rules " + f"of version 3" + ) + return + if isinstance(widget, MDChipTrailingIcon) and self.type in [ + "assist", + "suggestion", + ]: + Logger.warning( + f"KivyMD: " + f"According to the material design standards of version " + f"3, do not use the trailing icon for an '{self.type}' " + f"type chip." + ) + return + if ( + isinstance(widget, MDChipTrailingIcon) + and self.type == "filter" + and DEVICE_TYPE == "mobile" + ): + Logger.warning( + "KivyMD: " + "According to the material design standards of version 3, " + "only on desktop computers and tablets, filter chips can " + "contain a finishing icon for directly removing the chip " + "or opening the options menu." + ) + return + if ( + isinstance(widget, (MDChipLeadingIcon, MDChipLeadingAvatar)) + and self.type == "filter" + ): + Logger.warning( + "KivyMD: " + "According to the material design standards of version 3, " + "it is better not to use a leading icon for a 'filter' " + "type chip." + ) + if ( + isinstance(widget, MDChipLeadingAvatar) + and self.type == "suggestion" + ): + Logger.warning( + "KivyMD: " + "According to the material design standards of version 3, " + "it is better not to use a leading avatar for a " + "'suggestion' type chip." + ) + return + + widget.bind( + on_press=self._set_allow_chip_ripple, + on_release=self._set_allow_chip_ripple, + ) + widget.pos_hint = {"center_y": 0.5} + self.padding = ("8dp", 0, "8dp", 0) + self.spacing = ( + "8dp" + if isinstance( + widget, + ( + MDChipLeadingIcon, + MDChipLeadingAvatar, + MDChipTrailingIcon, + ), + ) + else 0 + ) + container.add_widget(widget) + + if isinstance(widget, MDChipText): + widget.adaptive_size = True + widget.pos_hint = {"center_y": 0.5} + if self.type == "suggestion": + self.padding = ("16dp", 0, "16dp", 0) + Clock.schedule_once( + lambda x: self.ids.label_container.add_widget(widget) + ) + elif isinstance(widget, (MDChipLeadingIcon, MDChipLeadingAvatar)): + Clock.schedule_once( + lambda x: add_icon_leading_trailing( + self.ids.leading_icon_container ) ) - - def set_chip_text_color(self, instance_chip, active_value: int): - Animation( - color=(0, 0, 0, 1) if active_value else (0, 0, 0, 0.5), d=0.2 - ).start(self.ids.label) - - class MyScreen(MDScreen): - def removes_marks_all_chips( - self, selected_instance_chip=None, multiple=True + elif isinstance(widget, MDChipTrailingIcon): + Clock.schedule_once( + lambda x: add_icon_leading_trailing( + self.ids.trailing_icon_container + ) + ) + elif isinstance( + widget, + (LabelTextContainer, LeadingIconContainer, TrailingIconContainer), ): - if multiple: - for instance_chip in self.ids.chip_box.children: - if instance_chip.active: - instance_chip.active = False - else: - for instance_chip in self.ids.chip_only_box.children: - if instance_chip != selected_instance_chip: - instance_chip.active = False + return super().add_widget(widget) - class Test(MDApp): - def build(self): - return Builder.load_string(KV) - - Test().run() + def _set_allow_chip_ripple( + self, instance: MDChipLeadingIcon | MDChipTrailingIcon + ) -> None: + self._allow_chip_ripple = not self._allow_chip_ripple diff --git a/sbapp/kivymd/uix/circularlayout.py b/sbapp/kivymd/uix/circularlayout.py index 9f02e85..cca5d21 100644 --- a/sbapp/kivymd/uix/circularlayout.py +++ b/sbapp/kivymd/uix/circularlayout.py @@ -53,7 +53,6 @@ from kivymd.uix.floatlayout import MDFloatLayout class MDCircularLayout(MDFloatLayout): - degree_spacing = NumericProperty(30) """ The space between children in degree. diff --git a/sbapp/kivymd/uix/datatables/datatables.kv b/sbapp/kivymd/uix/datatables/datatables.kv index a6a7220..b5d81a7 100644 --- a/sbapp/kivymd/uix/datatables/datatables.kv +++ b/sbapp/kivymd/uix/datatables/datatables.kv @@ -231,4 +231,11 @@ id: container orientation: "vertical" elevation: root.elevation + shadow_radius: root.shadow_radius + shadow_softness: root.shadow_softness + shadow_offset: root.shadow_offset + shadow_color: root.shadow_color + shadow_color: root.shadow_color + shadow_softness_size: root.shadow_softness_size padding: "24dp", "24dp", "8dp", "8dp" + md_bg_color: app.theme_cls.bg_normal diff --git a/sbapp/kivymd/uix/datatables/datatables.py b/sbapp/kivymd/uix/datatables/datatables.py index 1626283..44748a5 100644 --- a/sbapp/kivymd/uix/datatables/datatables.py +++ b/sbapp/kivymd/uix/datatables/datatables.py @@ -37,6 +37,7 @@ from kivy.lang import Builder from kivy.metrics import dp from kivy.properties import ( BooleanProperty, + BoundedNumericProperty, ColorProperty, DictProperty, ListProperty, @@ -44,6 +45,7 @@ from kivy.properties import ( ObjectProperty, OptionProperty, StringProperty, + VariableListProperty, ) from kivy.uix.anchorlayout import AnchorLayout from kivy.uix.behaviors import ButtonBehavior, FocusBehavior @@ -56,6 +58,11 @@ from kivy.uix.scrollview import ScrollView from kivymd import uix_path from kivymd.effects.stiffscroll import StiffScrollEffect +from kivymd.material_resources import ( + DATA_TABLE_ELEVATION, + DATA_TABLE_OFFSET, + DATA_TABLE_SOFTNESS, +) from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import HoverBehavior from kivymd.uix.boxlayout import MDBoxLayout @@ -758,7 +765,7 @@ class TableData(RecycleView): # instance_pagination.ids.button_forward.disabled = True -class TablePagination(ThemableBehavior, MDBoxLayout): +class TablePagination(MDBoxLayout): """Pagination Container.""" table_data = ObjectProperty() @@ -772,8 +779,11 @@ class TablePagination(ThemableBehavior, MDBoxLayout): class MDDataTable(ThemableBehavior, AnchorLayout): """ - See :class:`~kivy.uix.anchorlayout.AnchorLayout` class documentation for - more information. + Datatable class. + + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.anchorlayout.AnchorLayout` classes documentation. :Events: :attr:`on_row_press` @@ -1297,14 +1307,70 @@ class MDDataTable(ThemableBehavior, AnchorLayout): and defaults to `False`. """ - elevation = NumericProperty(4) + elevation = NumericProperty(DATA_TABLE_ELEVATION) """ - Table elevation. + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.elevation` + attribute. :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` and defaults to `4`. """ + shadow_radius = VariableListProperty([6], length=4) + """ + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_radius` + attribute. + + .. versionadded:: 1.2.0 + + :attr:`shadow_radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[6]`. + """ + + shadow_softness = NumericProperty(DATA_TABLE_SOFTNESS) + """ + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_softness` + attribute. + + .. versionadded:: 1.2.0 + + :attr:`shadow_softness` is an :class:`~kivy.properties.NumericProperty` + and defaults to `12`. + """ + + shadow_softness_size = BoundedNumericProperty(2, min=2) + """ + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_softness_size` + attribute. + + .. versionadded:: 1.2.0 + + :attr:`shadow_softness_size` is an :class:`~kivy.properties.BoundedNumericProperty` + and defaults to `2`. + """ + + shadow_offset = ListProperty(DATA_TABLE_OFFSET) + """ + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_offset` + attribute. + + .. versionadded:: 1.2.0 + + :attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty` + and defaults to `(0, 2)`. + """ + + shadow_color = ColorProperty([0, 0, 0, 0.6]) + """ + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_color` + attribute. + + .. versionadded:: 1.2.0 + + :attr:`shadow_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `[0, 0, 0, 0.6]`. + """ + rows_num = NumericProperty(5) """ The number of rows displayed on one page of the table. @@ -1626,6 +1692,77 @@ class MDDataTable(ThemableBehavior, AnchorLayout): .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-add-remove-row.gif :align: center + Deleting checked rows + --------------------- + + .. code-block:: python + + from kivy.metrics import dp + from kivy.lang import Builder + from kivy.clock import Clock + + from kivymd.app import MDApp + from kivymd.uix.datatables import MDDataTable + from kivymd.uix.screen import MDScreen + + KV = ''' + MDBoxLayout: + orientation: "vertical" + padding: "56dp" + spacing: "24dp" + + MDData: + id: table_screen + + MDRaisedButton: + text: "DELETE CHECKED ROWS" + on_release: table_screen.delete_checked_rows() + ''' + + + class MDData(MDScreen): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.data = [ + ["1", "Asep Sudrajat", "Male", "Soccer"], + ["2", "Egy", "Male", "Soccer"], + ["3", "Tanos", "Demon", "Soccer"], + ] + self.data_tables = MDDataTable( + use_pagination=True, + check=True, + column_data=[ + ("No", dp(30)), + ("No Urut.", dp(30)), + ("Alamat Pengirim", dp(30)), + ("No Surat", dp(60)), + ] + ) + self.data_tables.row_data = self.data + self.add_widget(self.data_tables) + + def delete_checked_rows(self): + def deselect_rows(*args): + self.data_tables.table_data.select_all("normal") + + for data in self.data_tables.get_row_checks(): + self.data_tables.remove_row(data) + + Clock.schedule_once(deselect_rows) + + + class MyApp(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" + return Builder.load_string(KV) + + + MyApp().run() + + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/data-tables-deleting-checked-rows.gif + :align: center + .. versionadded:: 1.0.0 """ @@ -1760,7 +1897,6 @@ class MDDataTable(ThemableBehavior, AnchorLayout): class CellRow( - ThemableBehavior, RecycleDataViewBehavior, HoverBehavior, ButtonBehavior, diff --git a/sbapp/kivymd/uix/dialog/dialog.kv b/sbapp/kivymd/uix/dialog/dialog.kv index 2e95b9c..96e01a3 100644 --- a/sbapp/kivymd/uix/dialog/dialog.kv +++ b/sbapp/kivymd/uix/dialog/dialog.kv @@ -32,7 +32,7 @@ orientation: "vertical" size_hint_y: None height: self.minimum_height - padding: "24dp", "24dp", "16dp", "8dp" + padding: "24dp", "24dp", "8dp", "8dp" radius: root.radius md_bg_color: root.theme_cls.bg_dark \ diff --git a/sbapp/kivymd/uix/dialog/dialog.py b/sbapp/kivymd/uix/dialog/dialog.py index f254f30..9f2db53 100755 --- a/sbapp/kivymd/uix/dialog/dialog.py +++ b/sbapp/kivymd/uix/dialog/dialog.py @@ -89,7 +89,7 @@ from kivy.uix.modalview import ModalView from kivymd import uix_path from kivymd.material_resources import DEVICE_TYPE from kivymd.theming import ThemableBehavior -from kivymd.uix.behaviors import CommonElevationBehavior +from kivymd.uix.behaviors import CommonElevationBehavior, MotionDialogBehavior from kivymd.uix.button import BaseButton from kivymd.uix.card import MDSeparator from kivymd.uix.list import BaseListItem @@ -100,7 +100,9 @@ with open( Builder.load_string(kv_file.read()) -class BaseDialog(ThemableBehavior, ModalView, CommonElevationBehavior): +class BaseDialog( + ThemableBehavior, MotionDialogBehavior, ModalView, CommonElevationBehavior +): elevation = NumericProperty(3) """ See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.elevation` @@ -159,6 +161,16 @@ class BaseDialog(ThemableBehavior, ModalView, CommonElevationBehavior): class MDDialog(BaseDialog): + """ + Dialog class. + + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.modalview.ModalView` and + :class:`~kivymd.uix.behaviors.CommonElevationBehavior` + classes documentation. + """ + title = StringProperty() """ Title dialog. @@ -286,22 +298,22 @@ class MDDialog(BaseDialog): class Example(MDApp): dialog = None - def build(self): - self.theme_cls.theme_style = "Dark" - self.theme_cls.primary_palette = "Orange" - return Builder.load_string(KV) + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" + return Builder.load_string(KV) - def show_simple_dialog(self): - if not self.dialog: - self.dialog = MDDialog( - title="Set backup account", - type="simple", - items=[ - Item(text="user01@gmail.com", source="kivymd/images/logo/kivymd-icon-128.png"), - Item(text="user02@gmail.com", source="data/logo/kivy-icon-128.png"), - ], - ) - self.dialog.open() + def show_simple_dialog(self): + if not self.dialog: + self.dialog = MDDialog( + title="Set backup account", + type="simple", + items=[ + Item(text="user01@gmail.com", source="kivymd/images/logo/kivymd-icon-128.png"), + Item(text="user02@gmail.com", source="data/logo/kivy-icon-128.png"), + ], + ) + self.dialog.open() Example().run() @@ -422,7 +434,8 @@ class MDDialog(BaseDialog): content_cls = ObjectProperty() """ - Custom content class. + Custom content class. This attribute is only available when :attr:`type` is + set to `'custom'`. .. tabs:: @@ -637,6 +650,7 @@ class MDDialog(BaseDialog): def on_open(self) -> None: # TODO: Add scrolling text. self.height = self.ids.container.height + super().on_open() def get_normal_height(self) -> float: return ( diff --git a/sbapp/kivymd/uix/dropdownitem/dropdownitem.py b/sbapp/kivymd/uix/dropdownitem/dropdownitem.py index 3967c5c..951357d 100644 --- a/sbapp/kivymd/uix/dropdownitem/dropdownitem.py +++ b/sbapp/kivymd/uix/dropdownitem/dropdownitem.py @@ -68,6 +68,17 @@ class _Triangle(Widget): class MDDropDownItem( DeclarativeBehavior, ThemableBehavior, ButtonBehavior, BoxLayout ): + """ + Dropdown item class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` + classes documentation. + """ + text = StringProperty() """ Text item. diff --git a/sbapp/kivymd/uix/expansionpanel/expansionpanel.py b/sbapp/kivymd/uix/expansionpanel/expansionpanel.py index 2bbd8ee..d0fe33d 100755 --- a/sbapp/kivymd/uix/expansionpanel/expansionpanel.py +++ b/sbapp/kivymd/uix/expansionpanel/expansionpanel.py @@ -185,21 +185,39 @@ class MDExpansionChevronRight(IRightBodyTouch, MDIconButton): class MDExpansionPanelOneLine(OneLineAvatarIconListItem): - """Single line panel.""" + """ + Single line panel. + + For more information, see in the + :class:`~kivymd.uix.list.OneLineAvatarIconListItem` class documentation. + """ class MDExpansionPanelTwoLine(TwoLineAvatarIconListItem): - """Two-line panel.""" + """ + Two-line panel. + + For more information, see in the + :class:`~kivymd.uix.list.TwoLineAvatarIconListItem` class documentation. + """ class MDExpansionPanelThreeLine(ThreeLineAvatarIconListItem): - """Three-line panel.""" + """ + Three-line panel. + + For more information, see in the + :class:`~kivymd.uix.list.ThreeLineAvatarIconListItem` class documentation. + """ class MDExpansionPanelLabel(TwoLineListItem): """ Label panel. + For more information, see in the + :class:`~kivymd.uix.list.TwoLineListItem` class documentation. + ..warning:: This class is created for use in the :class:`~kivymd.uix.stepper.MDStepperVertical` and :class:`~kivymd.uix.stepper.MDStepper` classes, and has not @@ -217,6 +235,11 @@ class MDExpansionPanelLabel(TwoLineListItem): class MDExpansionPanel(RelativeLayout): """ + Expansion panel class. + + For more information, see in the + :class:`~kivy.uix.relativelayout.RelativeLayout` classes documentation. + :Events: :attr:`on_open` Called when a panel is opened. diff --git a/sbapp/kivymd/uix/filemanager/filemanager.kv b/sbapp/kivymd/uix/filemanager/filemanager.kv index ad5704f..29352d7 100644 --- a/sbapp/kivymd/uix/filemanager/filemanager.kv +++ b/sbapp/kivymd/uix/filemanager/filemanager.kv @@ -1,4 +1,6 @@ #:import os os +#:import FILE_MANAGER_TOP_APP_BAR_ELEVATION kivymd.material_resources.FILE_MANAGER_TOP_APP_BAR_ELEVATION + icon: "folder" @@ -74,7 +76,7 @@ title: root.current_path right_action_items: [["close-box", lambda x: root.exit_manager(1)]] left_action_items: [["chevron-left", lambda x: root.back()]] - elevation: 3 + elevation: FILE_MANAGER_TOP_APP_BAR_ELEVATION md_bg_color: app.theme_cls.primary_color \ if not root.background_color_toolbar else \ diff --git a/sbapp/kivymd/uix/filemanager/filemanager.py b/sbapp/kivymd/uix/filemanager/filemanager.py index 1b7727e..4073764 100755 --- a/sbapp/kivymd/uix/filemanager/filemanager.py +++ b/sbapp/kivymd/uix/filemanager/filemanager.py @@ -158,7 +158,6 @@ from kivy.uix.behaviors import ButtonBehavior from kivy.uix.modalview import ModalView from kivymd import images_path, uix_path -from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import CircularRippleBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import MDFloatingActionButton @@ -197,7 +196,7 @@ class ModifiedOneLineIconListItem(BaseListItem): self.height = dp(48) -class MDFileManager(MDRelativeLayout, ThemableBehavior): +class MDFileManager(MDRelativeLayout): """ Implements a modal dialog with a file manager. @@ -248,7 +247,8 @@ class MDFileManager(MDRelativeLayout, ThemableBehavior): background_color_selection_button = ColorProperty(None) """ - Background color of the current directory/path selection button. + Background color in (r, g, b, a) or string format of the current + directory/path selection button. .. versionadded:: 1.1.0 @@ -268,7 +268,7 @@ class MDFileManager(MDRelativeLayout, ThemableBehavior): background_color_toolbar = ColorProperty(None) """ - Background color of the file manager toolbar. + Background color in (r, g, b, a) or string format of the file manager toolbar. .. versionadded:: 1.1.0 @@ -307,7 +307,8 @@ class MDFileManager(MDRelativeLayout, ThemableBehavior): icon_color = ColorProperty(None) """ - Color of the folder icon when the :attr:`preview` property is set to False. + Color in (r, g, b, a) or string format of the folder icon when the + :attr:`preview` property is set to False. .. versionadded:: 1.1.0 diff --git a/sbapp/kivymd/uix/fitimage/fitimage.py b/sbapp/kivymd/uix/fitimage/fitimage.py index 35ded4d..246bb5a 100644 --- a/sbapp/kivymd/uix/fitimage/fitimage.py +++ b/sbapp/kivymd/uix/fitimage/fitimage.py @@ -137,6 +137,14 @@ from kivymd.uix.boxlayout import MDBoxLayout class FitImage(MDBoxLayout, StencilBehavior): + """ + Fit image class. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDLayout` and + :class:`~kivymd.uix.behaviors.StencilBehavior` classes documentation. + """ + source = ObjectProperty() """ Filename/source of your image. diff --git a/sbapp/kivymd/uix/floatlayout.py b/sbapp/kivymd/uix/floatlayout.py index 3f99981..b64ddcf 100644 --- a/sbapp/kivymd/uix/floatlayout.py +++ b/sbapp/kivymd/uix/floatlayout.py @@ -35,11 +35,14 @@ MDFloatLayout from kivy.uix.floatlayout import FloatLayout +from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior -class MDFloatLayout(DeclarativeBehavior, FloatLayout, MDAdaptiveWidget): +class MDFloatLayout( + DeclarativeBehavior, ThemableBehavior, FloatLayout, MDAdaptiveWidget +): """ Float layout class. For more information, see in the :class:`~kivy.uix.floatlayout.FloatLayout` class documentation. diff --git a/sbapp/kivymd/uix/gridlayout.py b/sbapp/kivymd/uix/gridlayout.py index 84d883c..96bfd79 100644 --- a/sbapp/kivymd/uix/gridlayout.py +++ b/sbapp/kivymd/uix/gridlayout.py @@ -85,11 +85,14 @@ Equivalent from kivy.uix.gridlayout import GridLayout +from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior -class MDGridLayout(DeclarativeBehavior, GridLayout, MDAdaptiveWidget): +class MDGridLayout( + DeclarativeBehavior, ThemableBehavior, GridLayout, MDAdaptiveWidget +): """ Grid layout class. For more information, see in the :class:`~kivy.uix.gridlayout.GridLayout` class documentation. diff --git a/sbapp/kivymd/uix/imagelist/imagelist.kv b/sbapp/kivymd/uix/imagelist/imagelist.kv index 8cb474c..fe87f76 100644 --- a/sbapp/kivymd/uix/imagelist/imagelist.kv +++ b/sbapp/kivymd/uix/imagelist/imagelist.kv @@ -13,6 +13,7 @@ (0, 0) on_release: root.dispatch("on_release") on_press: root.dispatch("on_press") + _no_ripple_effect: root._no_ripple_effect SmartTileOverlayBox: id: box diff --git a/sbapp/kivymd/uix/imagelist/imagelist.py b/sbapp/kivymd/uix/imagelist/imagelist.py old mode 100755 new mode 100644 index 970ed2e..ba84b14 --- a/sbapp/kivymd/uix/imagelist/imagelist.py +++ b/sbapp/kivymd/uix/imagelist/imagelist.py @@ -71,6 +71,7 @@ __all__ = [ import os +from kivy.clock import Clock from kivy.lang import Builder from kivy.properties import ( BooleanProperty, @@ -82,7 +83,6 @@ from kivy.properties import ( from kivy.uix.behaviors import ButtonBehavior from kivymd import uix_path -from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import RectangularRippleBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.fitimage import FitImage @@ -103,10 +103,13 @@ class SmartTileOverlayBox(MDBoxLayout): """Implements a container for custom widgets to be added to the tile.""" -class MDSmartTile(MDRelativeLayout, ThemableBehavior): +class MDSmartTile(MDRelativeLayout): """ A tile for more complex needs. + For more information, see in the + :class:`~kivymd.uix.relativelayout.MDRelativeLayout` class documentation. + Includes an image, a container to place overlays and a box that can act as a header or a footer, as described in the Material Design specs. @@ -139,7 +142,8 @@ class MDSmartTile(MDRelativeLayout, ThemableBehavior): box_color = ColorProperty((0, 0, 0, 0.5)) """ - Sets the color and opacity for the information box. + Sets the color in (r, g, b, a) or string format and opacity for the + information box. .. code-block:: kv @@ -249,6 +253,8 @@ class MDSmartTile(MDRelativeLayout, ThemableBehavior): and defaults to `False`. """ + _no_ripple_effect = BooleanProperty(False) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.register_event_type("on_release") @@ -270,4 +276,4 @@ class MDSmartTile(MDRelativeLayout, ThemableBehavior): if isinstance(widget, MDLabel): widget.shorten = True widget.shorten_from = "right" - self.ids.box.add_widget(widget) + Clock.schedule_once(lambda x: self.ids.box.add_widget(widget)) diff --git a/sbapp/kivymd/uix/label/label.kv b/sbapp/kivymd/uix/label/label.kv index 71cac91..7fba348 100644 --- a/sbapp/kivymd/uix/label/label.kv +++ b/sbapp/kivymd/uix/label/label.kv @@ -3,12 +3,10 @@ disabled_color: self.theme_cls.disabled_hint_text_color - # FIXME: Overriding the values of this property greatly affects application - # performance. Especially when the application window is resized and a - # custom font is used. Performance is especially slow when you are using - # `PIL` as your text processing provider - os.environ ['KIVY_TEXT'] = 'pil'. - # Priority - CRITICAL. - text_size: self.width, None + text_size: + (self.width if not self.adaptive_width else None) \ + if not self.adaptive_size else None, \ + None : @@ -16,6 +14,7 @@ Color: rgba: (1, 1, 1, 1) if self.source else (0, 0, 0, 0) Rectangle: + group: "rectangle" source: self.source if self.source else None pos: self.pos \ @@ -32,6 +31,7 @@ # Badge icon. MDLabel: + id: badge font_style: "Icon" adaptive_size: True opposite_icon_color: True @@ -62,6 +62,7 @@ if root.badge_icon else \ (0, 0, 0, 0) RoundedRectangle: + group: "badge" radius: [self.width / 2,] pos: self.pos size: self.size diff --git a/sbapp/kivymd/uix/label/label.py b/sbapp/kivymd/uix/label/label.py index fc34ba4..73bd46b 100755 --- a/sbapp/kivymd/uix/label/label.py +++ b/sbapp/kivymd/uix/label/label.py @@ -18,32 +18,58 @@ Class :class:`MDLabel` inherited from the :class:`~kivy.uix.label.Label` class but for :class:`MDLabel` the ``text_size`` parameter is ``(self.width, None)`` and default is positioned on the left: -.. code-block:: python +.. tabs:: - from kivy.lang import Builder + .. tab:: Declarative KV style - from kivymd.app import MDApp + .. code-block:: python - KV = ''' - MDScreen: + from kivy.lang import Builder - MDBoxLayout: - orientation: "vertical" + from kivymd.app import MDApp - MDTopAppBar: - title: "MDLabel" + KV = ''' + MDScreen: - MDLabel: - text: "MDLabel" - ''' + MDLabel: + text: "MDLabel" + ''' - class Test(MDApp): - def build(self): - return Builder.load_string(KV) + class Test(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" + return Builder.load_string(KV) - Test().run() + Test().run() + + .. tab:: Declarative Python style + + .. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + from kivymd.uix.screen import MDScreen + from kivymd.uix.label import MDLabel + + + class Test(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" + return ( + MDScreen( + MDLabel( + text="MDLabel" + ) + ) + ) + + + Test().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-to-left.png :align: center @@ -74,20 +100,16 @@ and default is positioned on the left: from kivymd.uix.label import MDLabel KV = ''' - MDScreen: - - MDBoxLayout: - id: box - orientation: "vertical" - - MDTopAppBar: - title: "MDLabel" + MDBoxLayout: + orientation: "vertical" ''' class Test(MDApp): def build(self): + self.theme_cls.theme_style = "Dark" screen = Builder.load_string(KV) + # Names of standard color themes. for name_theme in [ "Primary", @@ -96,7 +118,7 @@ and default is positioned on the left: "Error", "ContrastParentBackground", ]: - screen.ids.box.add_widget( + screen.add_widget( MDLabel( text=name_theme, halign="center", @@ -121,7 +143,7 @@ in the ``text_color`` parameter: text: "Custom color" halign: "center" theme_text_color: "Custom" - text_color: 0, 0, 1, 1 + text_color: "blue" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-custom-color.png :align: center @@ -138,26 +160,20 @@ parameter: from kivymd.uix.label import MDLabel from kivymd.font_definitions import theme_font_styles - KV = ''' - MDScreen: + MDScrollView: - MDBoxLayout: - orientation: "vertical" - - MDTopAppBar: - title: "MDLabel" - - ScrollView: - - MDList: - id: box + MDList: + id: box + spacing: "8dp" ''' class Test(MDApp): def build(self): + self.theme_cls.theme_style = "Dark" screen = Builder.load_string(KV) + # Names of standard font styles. for name_style in theme_font_styles[:-1]: screen.ids.box.add_widget( @@ -165,6 +181,7 @@ parameter: text=f"{name_style} style", halign="center", font_style=name_style, + adaptive_height=True, ) ) return screen @@ -172,7 +189,273 @@ parameter: Test().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-font-style.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-font-style.png + :align: center + +Highlighting and copying labels +=============================== + +You can highlight labels by double tap on the label: +---------------------------------------------------- + +.. tabs:: + + .. tab:: Declarative KV style + + .. code-block:: python + + from kivy.lang.builder import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDLabel: + adaptive_size: True + pos_hint: {"center_x": .5, "center_y": .5} + text: "MDLabel" + allow_selection: True + padding: "4dp", "4dp" + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" + return Builder.load_string(KV) + + + Example().run() + + .. tab:: Declarative Python style + + .. code-block:: python + + from kivy.lang.builder import Builder + + from kivymd.app import MDApp + from kivymd.uix.label import MDLabel + from kivymd.uix.screen import MDScreen + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" + return ( + MDScreen( + MDLabel( + adaptive_size=True, + pos_hint={"center_x": .5, "center_y": .5}, + text="MDLabel", + allow_selection=True, + padding=("4dp", "4dp"), + ) + ) + ) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-label-allow-selection.gif + :align: center + +You can copy the label text by double clicking on it: +----------------------------------------------------- + +.. tabs:: + + .. tab:: Declarative KV style + + .. code-block:: python + + from kivy.lang.builder import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDLabel: + adaptive_size: True + pos_hint: {"center_x": .5, "center_y": .5} + text: "MDLabel" + padding: "4dp", "4dp" + allow_selection: True + allow_copy: True + on_copy: print("The text is copied to the clipboard") + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" + return Builder.load_string(KV) + + + Example().run() + + .. tab:: Declarative Python style + + .. code-block:: python + + from kivy.lang.builder import Builder + + from kivymd.app import MDApp + from kivymd.uix.label import MDLabel + from kivymd.uix.screen import MDScreen + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" + return ( + MDScreen( + MDLabel( + id="label", + adaptive_size=True, + pos_hint={"center_x": .5, "center_y": .5}, + text="MDLabel", + allow_selection=True, + allow_copy=True, + padding=("4dp", "4dp"), + ) + ) + ) + + def on_start(self): + self.root.ids.label.bind(on_copy=self.on_copy) + + def on_copy(self, instance_label: MDLabel): + print("The text is copied to the clipboard") + + + Example().run() + +Example of copying/cutting labels using the context menu +-------------------------------------------------------- + +.. code-block:: python + + from kivy.core.clipboard import Clipboard + from kivy.lang.builder import Builder + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.label import MDLabel + from kivymd.uix.menu import MDDropdownMenu + from kivymd.toast import toast + + KV = ''' + MDBoxLayout: + orientation: "vertical" + spacing: "12dp" + padding: "24dp" + + MDScrollView: + + MDBoxLayout: + id: box + orientation: "vertical" + padding: "24dp" + spacing: "12dp" + adaptive_height: True + + MDTextField: + max_height: "200dp" + mode: "fill" + multiline: True + + MDWidget: + ''' + + data = [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "Sed blandit libero volutpat sed cras ornare arcu. Nisl vel pretium " + "lectus quam id leo in. Tincidunt arcu non sodales neque sodales ut etiam.", + "Elit scelerisque mauris pellentesque pulvinar pellentesque habitant. " + "Nisl rhoncus mattis rhoncus urna neque. Orci nulla pellentesque " + "dignissim enim. Ac auctor augue mauris augue neque gravida in fermentum. " + "Lacus suspendisse faucibus interdum posuere." + + ] + + + class CopyLabel(MDLabel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.allow_selection = True + self.adaptive_height = True + self.theme_text_color = "Custom" + self.text_color = self.theme_cls.text_color + + + class Example(MDApp): + context_menu = None + + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" + return Builder.load_string(KV) + + def on_start(self): + for text in data: + copy_label = CopyLabel(text=text) + copy_label.bind( + on_selection=self.open_context_menu, + on_cancel_selection=self.restore_text_color, + ) + self.root.ids.box.add_widget(copy_label) + + def click_item_context_menu( + self, type_click: str, instance_label: CopyLabel + ) -> None: + Clipboard.copy(instance_label.text) + + if type_click == "copy": + toast("Copied") + elif type_click == "cut": + self.root.ids.box.remove_widget(instance_label) + toast("Cut") + if self.context_menu: + self.context_menu.dismiss() + + def restore_text_color(self, instance_label: CopyLabel) -> None: + instance_label.text_color = self.theme_cls.text_color + + def open_context_menu(self, instance_label: CopyLabel) -> None: + instance_label.text_color = "black" + menu_items = [ + { + "text": "Copy text", + "viewclass": "OneLineListItem", + "height": dp(48), + "on_release": lambda: self.click_item_context_menu( + "copy", instance_label + ), + }, + { + "text": "Cut text", + "viewclass": "OneLineListItem", + "height": dp(48), + "on_release": lambda: self.click_item_context_menu( + "cut", instance_label + ), + }, + ] + self.context_menu = MDDropdownMenu( + caller=instance_label, items=menu_items, width_mult=3 + ) + self.context_menu.open() + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/copying-cutting-labels-using-context-menu.gif :align: center .. MDIcon: @@ -217,6 +500,8 @@ MDIcon with badge icon :align: center """ +from __future__ import annotations + __all__ = ("MDLabel", "MDIcon") import os @@ -224,6 +509,8 @@ from typing import Union from kivy.animation import Animation from kivy.clock import Clock +from kivy.core.clipboard import Clipboard +from kivy.core.window import Window from kivy.graphics import Color, Rectangle from kivy.lang import Builder from kivy.metrics import sp @@ -243,7 +530,7 @@ from kivymd import uix_path from kivymd.theming import ThemableBehavior from kivymd.theming_dynamic_text import get_contrast_text_color from kivymd.uix import MDAdaptiveWidget -from kivymd.uix.behaviors import DeclarativeBehavior +from kivymd.uix.behaviors import DeclarativeBehavior, TouchBehavior from kivymd.uix.floatlayout import MDFloatLayout __MDLabel_colors__ = { @@ -264,7 +551,36 @@ with open( Builder.load_string(kv_file.read()) -class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget): +class MDLabel( + DeclarativeBehavior, + ThemableBehavior, + Label, + MDAdaptiveWidget, + TouchBehavior, +): + """ + Label class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.label.Label` and + :class:`~kivymd.uix.MDAdaptiveWidget` and + :class:`~kivymd.uix.behaviors.TouchBehavior` + classes documentation. + + :Events: + `on_ref_press` + Called when the user clicks on a word referenced with a + ``[ref]`` tag in a text markup. + `on_copy` + Called when double-tapping on the label. + `on_selection` + Called when double-tapping on the label. + `on_cancel_selection` + Called when the highlighting is removed from the label text. + """ + font_style = StringProperty("Body1") """ Label font style. @@ -316,29 +632,84 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget): text_color = ColorProperty(None) """ - Label text color in (r, g, b, a) format. + Label text color in (r, g, b, a) or string format. :attr:`text_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ + allow_copy = BooleanProperty(False) + """ + Allows you to copy text to the clipboard by double-clicking on the label. + + .. versionadded:: 1.2.0 + + :attr:`allow_copy` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + allow_selection = BooleanProperty(False) + """ + Allows to highlight text by double-clicking on the label. + + .. versionadded:: 1.2.0 + + :attr:`allow_selection` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + color_selection = ColorProperty(None) + """ + The color in (r, g, b, a) or string format of the text selection when the + value of the :attr:`allow_selection` attribute is True. + + .. versionadded:: 1.2.0 + + :attr:`color_selection` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + color_deselection = ColorProperty(None) + """ + The color in (r, g, b, a) or string format of the text deselection when the + value of the :attr:`allow_selection` attribute is True. + + .. versionadded:: 1.2.0 + + :attr:`color_deselection` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + is_selected = BooleanProperty(False) + """ + Is the label text highlighted. + + .. versionadded:: 1.2.0 + + :attr:`is_selected` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + _text_color_str = StringProperty() parent_background = ColorProperty(None) can_capitalize = BooleanProperty(True) canvas_bg = ObjectProperty() - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.bind( font_style=self.update_font_style, can_capitalize=self.update_font_style, ) + self.theme_cls.bind(theme_style=self._do_update_theme_color) + self.register_event_type("on_copy") + self.register_event_type("on_selection") + self.register_event_type("on_cancel_selection") self.on_theme_text_color(None, self.theme_text_color) self.update_font_style(None, "") self.on_opposite_colors(None, self.opposite_colors) Clock.schedule_once(self.check_font_styles) - self.theme_cls.bind(theme_style=self._do_update_theme_color) def check_font_styles(self, interval: Union[int, float] = 0) -> bool: if self.font_style not in list(self.theme_cls.font_styles.keys()): @@ -353,7 +724,8 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget): if self.check_font_styles() is True: font_info = self.theme_cls.font_styles[self.font_style] self.font_name = font_info[0] - self.font_size = sp(font_info[1]) + if self.font_style in list(self.theme_cls.font_styles.keys())[0:14]: + self.font_size = sp(font_info[1]) if font_info[2] and self.can_capitalize: self._capitalizing = True @@ -363,6 +735,64 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget): # TODO: Add letter spacing change # self.letter_spacing = font_info[3] + def do_selection(self) -> None: + if not self.is_selected: + self.md_bg_color = ( + self.theme_cls.primary_light + if not self.color_selection + else self.color_selection + ) + + def cancel_selection(self) -> None: + if self.is_selected: + self.md_bg_color = ( + self.theme_cls.bg_normal + if not self.color_deselection + else self.color_deselection + ) + self.dispatch("on_cancel_selection") + self.is_selected = False + + def on_double_tap(self, touch, *args) -> None: + if self.allow_copy and self.collide_point(*touch.pos): + Clipboard.copy(self.text) + self.dispatch("on_copy") + if self.allow_selection and self.collide_point(*touch.pos): + self.do_selection() + self.dispatch("on_selection") + self.is_selected = True + + def on_window_touch(self, *args): + if self.is_selected: + self.cancel_selection() + + def on_copy(self, *args) -> None: + """ + Called when double-tapping on the label. + + .. versionadded:: 1.2.0 + """ + + def on_selection(self, *args) -> None: + """ + Called when double-tapping on the label. + + .. versionadded:: 1.2.0 + """ + + def on_cancel_selection(self, *args) -> None: + """ + Called when the highlighting is removed from the label text. + + .. versionadded:: 1.2.0 + """ + + def on_allow_selection(self, instance_label, selection: bool) -> None: + if selection: + Window.bind(on_touch_down=self.on_window_touch) + else: + Window.unbind(on_touch_down=self.on_window_touch) + def on_theme_text_color( self, instance_label, theme_text_color: str ) -> None: @@ -414,6 +844,7 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget): def on_md_bg_color(self, instance_label, color: Union[list, str]) -> None: self.canvas.remove_group("Background_instruction") + self.canvas.before.clear() with self.canvas.before: Color(rgba=color) self.canvas_bg = Rectangle(pos=self.pos, size=self.size) @@ -445,6 +876,13 @@ class MDLabel(DeclarativeBehavior, ThemableBehavior, Label, MDAdaptiveWidget): class MDIcon(MDFloatLayout, MDLabel): + """ + Icon class. + + For more information, see in the :class:`~MDLabel` and + :class:`~kivymd.uix.floatlayout.MDFloatLayout` classes documentation. + """ + icon = StringProperty("android") """ Label icon name. @@ -465,7 +903,7 @@ class MDIcon(MDFloatLayout, MDLabel): badge_icon_color = ColorProperty([1, 1, 1, 1]) """ - Badge icon color in (r, g, b, a) format. + Badge icon color in (r, g, b, a) or string format. .. versionadded:: 1.0.0 @@ -475,7 +913,7 @@ class MDIcon(MDFloatLayout, MDLabel): badge_bg_color = ColorProperty(None) """ - Badge icon background color in (r, g, b, a) format. + Badge icon background color in (r, g, b, a) or string format. .. versionadded:: 1.0.0 diff --git a/sbapp/kivymd/uix/list/list.py b/sbapp/kivymd/uix/list/list.py index 9f02cab..035ca8a 100755 --- a/sbapp/kivymd/uix/list/list.py +++ b/sbapp/kivymd/uix/list/list.py @@ -984,6 +984,9 @@ class MDList(MDGridLayout): When adding (or removing) a widget, it will resize itself to fit its children, plus top and bottom paddings as described by the `MD` spec. + + For more information, see in the + :class:`~kivymd.uix.gridlayout.MDGridLayout` classes documentation. """ _list_vertical_padding = NumericProperty("8dp") @@ -1002,6 +1005,13 @@ class BaseListItem( ): """ Base class to all ListItems. Not supposed to be instantiated on its own. + + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivy.uix.floatlayout.FloatLayout` classes documentation. """ text = StringProperty() @@ -1242,7 +1252,12 @@ class IRightBodyTouch: class OneLineListItem(BaseListItem): - """A one line list item.""" + """ + A one line list item. + + For more information, see in the :class:`~BaseListItem` + classes documentation. + """ _txt_top_pad = NumericProperty("16dp") _txt_bot_pad = NumericProperty("15dp") @@ -1255,7 +1270,12 @@ class OneLineListItem(BaseListItem): class TwoLineListItem(BaseListItem): - """A two line list item.""" + """ + A two line list item. + + For more information, see in the :class:`~BaseListItem` + classes documentation. + """ _txt_top_pad = NumericProperty("20dp") _txt_bot_pad = NumericProperty("15dp") @@ -1267,7 +1287,12 @@ class TwoLineListItem(BaseListItem): class ThreeLineListItem(BaseListItem): - """A three line list item.""" + """ + A three line list item. + + For more information, see in the :class:`~BaseListItem` + classes documentation. + """ _txt_top_pad = NumericProperty("16dp") _txt_bot_pad = NumericProperty("15dp") @@ -1280,6 +1305,13 @@ class ThreeLineListItem(BaseListItem): class OneLineAvatarListItem(BaseListItem): + """ + A one line list item with left image. + + For more information, see in the :class:`~BaseListItem` + classes documentation. + """ + _txt_left_pad = NumericProperty("72dp") _txt_top_pad = NumericProperty("20dp") _txt_bot_pad = NumericProperty("19dp") @@ -1292,6 +1324,13 @@ class OneLineAvatarListItem(BaseListItem): class TwoLineAvatarListItem(OneLineAvatarListItem): + """ + A two line list item with left image. + + For more information, see in the :class:`~OneLineAvatarListItem` + classes documentation. + """ + _txt_top_pad = NumericProperty("20dp") _txt_bot_pad = NumericProperty("15dp") _height = NumericProperty() @@ -1303,6 +1342,13 @@ class TwoLineAvatarListItem(OneLineAvatarListItem): class ThreeLineAvatarListItem(ThreeLineListItem): + """ + A three line list item with left image. + + For more information, see in the :class:`~ThreeLineListItem` + classes documentation. + """ + _txt_left_pad = NumericProperty("72dp") def __init__(self, *args, **kwargs): @@ -1310,10 +1356,24 @@ class ThreeLineAvatarListItem(ThreeLineListItem): class OneLineIconListItem(OneLineListItem): + """ + A one line list item with left icon. + + For more information, see in the :class:`~OneLineListItem` + classes documentation. + """ + _txt_left_pad = NumericProperty("72dp") class TwoLineIconListItem(OneLineIconListItem): + """ + A two line list item with left icon. + + For more information, see in the :class:`~OneLineIconListItem` + classes documentation. + """ + _txt_top_pad = NumericProperty("20dp") _txt_bot_pad = NumericProperty("15dp") _height = NumericProperty() @@ -1325,10 +1385,24 @@ class TwoLineIconListItem(OneLineIconListItem): class ThreeLineIconListItem(ThreeLineListItem): + """ + A three line list item with left icon. + + For more information, see in the :class:`~ThreeLineListItem` + classes documentation. + """ + _txt_left_pad = NumericProperty("72dp") class OneLineRightIconListItem(OneLineListItem): + """ + A one line list item with right icon/image. + + For more information, see in the :class:`~OneLineListItem` + classes documentation. + """ + _txt_right_pad = NumericProperty("40dp") def __init__(self, *args, **kwargs): @@ -1337,6 +1411,13 @@ class OneLineRightIconListItem(OneLineListItem): class TwoLineRightIconListItem(OneLineRightIconListItem): + """ + A two line list item with right icon/image. + + For more information, see in the :class:`~OneLineRightIconListItem` + classes documentation. + """ + _txt_top_pad = NumericProperty("20dp") _txt_bot_pad = NumericProperty("15dp") _height = NumericProperty() @@ -1348,6 +1429,13 @@ class TwoLineRightIconListItem(OneLineRightIconListItem): class ThreeLineRightIconListItem(ThreeLineListItem): + """ + A three line list item with right icon/image. + + For more information, see in the :class:`~ThreeLineRightIconListItem` + classes documentation. + """ + _txt_right_pad = NumericProperty("40dp") def __init__(self, **kwargs): @@ -1356,6 +1444,13 @@ class ThreeLineRightIconListItem(ThreeLineListItem): class OneLineAvatarIconListItem(OneLineAvatarListItem): + """ + A one line list item with left/right icon/image/widget. + + For more information, see in the :class:`~OneLineAvatarListItem` + classes documentation. + """ + _txt_right_pad = NumericProperty("40dp") def __init__(self, *args, **kwargs): @@ -1364,6 +1459,13 @@ class OneLineAvatarIconListItem(OneLineAvatarListItem): class TwoLineAvatarIconListItem(TwoLineAvatarListItem): + """ + A two line list item with left/right icon/image/widget. + + For more information, see in the :class:`~TwoLineAvatarListItem` + classes documentation. + """ + _txt_right_pad = NumericProperty("40dp") def __init__(self, *args, **kwargs): @@ -1372,6 +1474,13 @@ class TwoLineAvatarIconListItem(TwoLineAvatarListItem): class ThreeLineAvatarIconListItem(ThreeLineAvatarListItem): + """ + A three line list item with left/right icon/image/widget. + + For more information, see in the :class:`~ThreeLineAvatarListItem` + classes documentation. + """ + _txt_right_pad = NumericProperty("40dp") def __init__(self, *args, **kwargs): @@ -1388,13 +1497,31 @@ class TouchBehavior: class ImageLeftWidget( CircularRippleBehavior, ButtonBehavior, ILeftBodyTouch, FitImage ): - pass + """ + The widget implements the left image for use in ListItem classes. + + For more information, see in the + :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~ILeftBodyTouch` and + :class:`~kivymd.uix.fitimage.FitImage` classes documentation. + """ class ImageLeftWidgetWithoutTouch( CircularRippleBehavior, TouchBehavior, ButtonBehavior, ILeftBody, FitImage ): """ + Disables the image event. + The widget implements the left image for use in `ListItem` classes. + + For more information, see in the + :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and + :class:`~TouchBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~ILeftBody` and + :class:`~kivymd.uix.fitimage.FitImage` classes documentation. + .. versionadded:: 1.0.0 """ @@ -1404,13 +1531,31 @@ class ImageLeftWidgetWithoutTouch( class ImageRightWidget( CircularRippleBehavior, ButtonBehavior, IRightBodyTouch, FitImage ): - pass + """ + The widget implements the right image for use in ListItem classes. + + For more information, see in the + :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~IRightBodyTouch` and + :class:`~kivymd.uix.fitimage.FitImage` classes documentation. + """ class ImageRightWidgetWithoutTouch( CircularRippleBehavior, TouchBehavior, ButtonBehavior, IRightBody, FitImage ): """ + Disables the image event. + The widget implements the right image for use in `ListItem` classes. + + For more information, see in the + :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and + :class:`~TouchBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~IRightBody` and + :class:`~kivymd.uix.fitimage.FitImage` classes documentation. + .. versionadded:: 1.0.0 """ @@ -1418,11 +1563,29 @@ class ImageRightWidgetWithoutTouch( class IconRightWidget(IRightBodyTouch, MDIconButton): + """ + The widget implements the right icon for use in ListItem classes. + + For more information, see in the + :class:`~IRightBodyTouch` and + :class:`~kivymd.uix.button.MDIconButton` + classes documentation. + """ + pos_hint = {"center_y": 0.5} class IconRightWidgetWithoutTouch(TouchBehavior, IRightBody, MDIconButton): """ + Disables the icon event. + The widget implements the right icon for use in ListItem classes. + + For more information, see in the + :class:`~TouchBehavior` and + :class:`~IRightBody` and + :class:`~kivymd.uix.button.MDIconButton` + classes documentation. + .. versionadded:: 1.0.0 """ @@ -1431,11 +1594,29 @@ class IconRightWidgetWithoutTouch(TouchBehavior, IRightBody, MDIconButton): class IconLeftWidget(ILeftBodyTouch, MDIconButton): + """ + The widget implements the left icon for use in ListItem classes. + + For more information, see in the + :class:`~ILeftBodyTouch` and + :class:`~kivymd.uix.button.MDIconButton` + classes documentation. + """ + pos_hint = {"center_y": 0.5} class IconLeftWidgetWithoutTouch(TouchBehavior, ILeftBody, MDIconButton): """ + Disables the icon event. + The widget implements the left icon for use in ListItem classes. + + For more information, see in the + :class:`~TouchBehavior` and + :class:`~ILeftBody` and + :class:`~kivymd.uix.button.MDIconButton` + classes documentation. + .. versionadded:: 1.0.0 """ @@ -1444,4 +1625,11 @@ class IconLeftWidgetWithoutTouch(TouchBehavior, ILeftBody, MDIconButton): class CheckboxLeftWidget(ILeftBodyTouch, MDCheckbox): - pass + """ + The widget implements the left checkbox element for use in ListItem classes. + + For more information, see in the + :class:`~ILeftBodyTouch` and + :class:`~kivymd.uix.selectioncontrol.MDCheckbox` + classes documentation. + """ diff --git a/sbapp/kivymd/uix/menu/menu.kv b/sbapp/kivymd/uix/menu/menu.kv index f72f87d..a53c071 100644 --- a/sbapp/kivymd/uix/menu/menu.kv +++ b/sbapp/kivymd/uix/menu/menu.kv @@ -1,26 +1,9 @@ -#:import STANDARD_INCREMENT kivymd.material_resources.STANDARD_INCREMENT - - - - adaptive_width: True - - - - - IconLeftWidget: - id: icon_widget - icon: root.icon - - - size_hint: None, None - width: root.width_mult * STANDARD_INCREMENT bar_width: 0 key_viewclass: "viewclass" key_size: "height" RecycleBoxLayout: - padding: 0, "4dp", 0, "4dp" default_size: None, dp(48) default_size_hint: 1, None size_hint_y: None @@ -28,32 +11,478 @@ orientation: "vertical" - + + orientation: "vertical" + + MDBoxLayout: + id: container + spacing: "12dp" + padding: "12dp", 0, "12dp", 0 + + MDLabel: + text: root.text + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.text_color else "Primary" + shorten: True + shorten_from: "right" + size_hint_x: None + width: + root.width - \ + ( \ + + trailing_container.width \ + + container.padding[0] \ + + container.padding[2] \ + + container.spacing \ + ) + text_color: + root.text_color \ + if root.text_color else \ + app.theme_cls.text_color + + MDTrailingTextContainer: + id: trailing_container + text: root.trailing_text + adaptive_width: True + theme_text_color: "Custom" if root.trailing_text_color else "Primary" + text_color: + root.trailing_text_color \ + if root.trailing_text_color else \ + app.theme_cls.text_color + + MDSeparator: + md_bg_color: + ( \ + self.theme_cls.divider_color \ + if not root.divider_color \ + else root.divider_color \ + ) \ + if root.divider else \ + (0, 0, 0, 0) + + + + orientation: "vertical" + + MDBoxLayout: + id: container + spacing: "12dp" + padding: "10dp", 0, "16dp", 0 + + MDIcon: + id: leading_icon + icon: root.leading_icon + size_hint: None, None + size: "48dp", "48dp" + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.leading_icon_color else "Primary" + text_color: + root.leading_icon_color \ + if root.leading_icon_color else \ + app.theme_cls.text_color + + MDLabel: + text: root.text + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.text_color else "Primary" + shorten: True + shorten_from: "right" + size_hint_x: None + width: + root.width - \ + ( \ + leading_icon.width \ + + trailing_container.width \ + + container.padding[0] \ + + container.padding[2] \ + + container.spacing \ + + dp(18) \ + ) + text_color: + root.text_color \ + if root.text_color else \ + app.theme_cls.text_color + + Widget: + + MDTrailingTextContainer: + id: trailing_container + text: root.trailing_text + adaptive_width: True + theme_text_color: "Custom" if root.trailing_text_color else "Primary" + text_color: + root.trailing_text_color \ + if root.trailing_text_color else \ + app.theme_cls.text_color + + MDSeparator: + md_bg_color: + ( \ + self.theme_cls.divider_color \ + if not root.divider_color \ + else root.divider_color \ + ) \ + if root.divider else \ + (0, 0, 0, 0) + + + + orientation: "vertical" + + MDBoxLayout: + id: container + spacing: "12dp" + padding: "12dp", 0, "12dp", 0 + + MDLabel: + id: label + text: root.text + shorten: True + size_hint_x: None + shorten_from: "right" + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.text_color else "Primary" + shorten: True + shorten_from: "right" + width: + root.width - \ + ( \ + + trailing_icon.width \ + + container.padding[0] \ + + container.padding[2] \ + + container.spacing \ + + dp(18) \ + ) + text_color: + root.text_color \ + if root.text_color else \ + app.theme_cls.text_color + + Widget: + + MDIcon: + id: trailing_icon + size_hint: None, None + size: "48dp", "48dp" + pos_hint: {"center_y": .5} + icon: root.trailing_icon + theme_text_color: "Custom" if root.trailing_icon_color else "Primary" + text_color: + root.trailing_icon_color \ + if root.trailing_icon_color else \ + app.theme_cls.text_color + + MDSeparator: + md_bg_color: + ( \ + self.theme_cls.divider_color \ + if not root.divider_color \ + else root.divider_color \ + ) \ + if root.divider else \ + (0, 0, 0, 0) + + + + adaptive_width: True + + MDIcon: + icon: root.trailing_icon + size_hint: None, None + size: "48dp", "48dp" + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.trailing_icon_color else "Primary" + text_color: + root.trailing_icon_color \ + if root.trailing_icon_color else \ + app.theme_cls.text_color + + MDLabel: + text: root.trailing_text + adaptive_size: True + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.trailing_text_color else "Primary" + text_color: + root.trailing_text_color \ + if root.trailing_text_color else \ + app.theme_cls.text_color + + + + orientation: "vertical" + + MDBoxLayout: + id: container + spacing: "12dp" + padding: "12dp", 0, "12dp", 0 + + MDLabel: + id: label + text: root.text + shorten: True + size_hint_x: None + shorten_from: "right" + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.text_color else "Primary" + shorten: True + shorten_from: "right" + width: + root.width - \ + ( \ + + trailing_container.width \ + + container.padding[0] \ + + container.padding[2] \ + + container.spacing \ + ) + text_color: + root.text_color \ + if root.text_color else \ + app.theme_cls.text_color + + MDTrailingIconTextContainer: + id: trailing_container + trailing_icon: root.trailing_icon + trailing_text: root.trailing_text + trailing_text_color: root.trailing_text_color + trailing_icon_color: root.trailing_icon_color + + MDSeparator: + md_bg_color: + ( \ + self.theme_cls.divider_color \ + if not root.divider_color \ + else root.divider_color \ + ) \ + if root.divider else \ + (0, 0, 0, 0) + + + + orientation: "vertical" + + MDLabel: + text: root.text + valign: "center" + padding_x: "12dp" + theme_text_color: "Custom" if root.text_color else "Primary" + shorten: True + shorten_from: "right" + text_color: + root.text_color \ + if root.text_color else \ + app.theme_cls.text_color + + MDSeparator: + md_bg_color: + ( \ + self.theme_cls.divider_color \ + if not root.divider_color \ + else root.divider_color \ + ) \ + if root.divider else \ + (0, 0, 0, 0) + + + + orientation: "vertical" + + MDBoxLayout: + id: container + spacing: "12dp" + padding: "10dp", 0, "16dp", 0 + + MDIcon: + id: leading_icon + icon: root.leading_icon + size_hint: None, None + size: "48dp", "48dp" + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.leading_icon_color else "Primary" + text_color: + root.leading_icon_color \ + if root.leading_icon_color else \ + app.theme_cls.text_color + + MDLabel: + text: root.text + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.text_color else "Primary" + shorten: True + shorten_from: "right" + size_hint_x: None + width: + root.width - \ + ( \ + leading_icon.width \ + + trailing_container.width \ + + container.padding[0] \ + + container.padding[2] \ + + container.spacing \ + + dp(18) \ + ) + text_color: + root.text_color \ + if root.text_color else \ + app.theme_cls.text_color + + Widget: + + MDTrailingIconTextContainer: + id: trailing_container + trailing_icon: root.trailing_icon + trailing_text: root.trailing_text + trailing_icon_color: root.trailing_icon_color + trailing_text_color: root.trailing_text_color + + MDSeparator: + md_bg_color: + ( \ + self.theme_cls.divider_color \ + if not root.divider_color \ + else root.divider_color \ + ) \ + if root.divider else \ + (0, 0, 0, 0) + + + + orientation: "vertical" + + MDBoxLayout: + id: container + spacing: "12dp" + padding: "10dp", 0, "12dp", 0 + + MDIcon: + id: leading_icon + icon: root.leading_icon + size_hint: None, None + size: "48dp", "48dp" + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.leading_icon_color else "Primary" + text_color: + root.leading_icon_color \ + if root.leading_icon_color else \ + app.theme_cls.text_color + + MDLabel: + id: label + text: root.text + shorten: True + size_hint_x: None + shorten_from: "right" + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.text_color else "Primary" + shorten: True + shorten_from: "right" + width: + root.width - \ + ( \ + leading_icon.width \ + + trailing_icon.width \ + + container.padding[0] \ + + container.padding[2] \ + + container.spacing \ + + dp(18) \ + ) + text_color: + root.text_color \ + if root.text_color else \ + app.theme_cls.text_color + + Widget: + + MDIcon: + id: trailing_icon + size_hint: None, None + size: "48dp", "48dp" + pos_hint: {"center_y": .5} + icon: root.trailing_icon + theme_text_color: "Custom" if root.trailing_icon_color else "Primary" + text_color: + root.trailing_icon_color \ + if root.trailing_icon_color else \ + app.theme_cls.text_color + + MDSeparator: + md_bg_color: + ( \ + self.theme_cls.divider_color \ + if not root.divider_color \ + else root.divider_color \ + ) \ + if root.divider else \ + (0, 0, 0, 0) + + + + orientation: "vertical" + + MDBoxLayout: + id: container + spacing: "12dp" + padding: "12dp", 0, "12dp", 0 + + MDIcon: + id: leading_icon + icon: root.leading_icon + size_hint: None, None + size: "48dp", "48dp" + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.leading_icon_color else "Primary" + text_color: + root.leading_icon_color \ + if root.leading_icon_color else \ + app.theme_cls.text_color + + MDLabel: + id: label + text: root.text + shorten: True + size_hint_x: None + shorten_from: "right" + pos_hint: {"center_y": .5} + theme_text_color: "Custom" if root.text_color else "Primary" + shorten: True + shorten_from: "right" + width: + root.width - \ + ( \ + leading_icon.width \ + + container.padding[0] \ + + container.padding[2] \ + + container.spacing \ + ) + text_color: + root.text_color \ + if root.text_color else \ + app.theme_cls.text_color + + MDSeparator: + md_bg_color: + ( \ + self.theme_cls.divider_color \ + if not root.divider_color \ + else root.divider_color \ + ) \ + if root.divider else \ + (0, 0, 0, 0) + orientation: "vertical" + elevation: root.elevation + shadow_radius: root.shadow_radius + shadow_softness: root.shadow_softness + shadow_offset: root.shadow_offset + shadow_color: root.shadow_color + shadow_color: root.shadow_color + radius: root.radius + size_hint: None, None - MenuContainer: - id: card - orientation: "vertical" - elevation: root.elevation - size_hint: None, None - size: md_menu.size[0], md_menu.size[1] + content_header.height - pos: md_menu.pos - opacity: md_menu.opacity - radius: root.radius - md_bg_color: - root.background_color \ - if root.background_color else root.theme_cls.bg_dark + MDBoxLayout: + id: content_header + adaptive_size: True - MDBoxLayout: - id: content_header - adaptive_size: True - - MDMenu: - id: md_menu - drop_cls: root - width_mult: root.width_mult - size_hint: None, None - size: 0, 0 - opacity: 0 + MDMenu: + id: md_menu + drop_cls: root diff --git a/sbapp/kivymd/uix/menu/menu.py b/sbapp/kivymd/uix/menu/menu.py index 102cf96..53d6b13 100755 --- a/sbapp/kivymd/uix/menu/menu.py +++ b/sbapp/kivymd/uix/menu/menu.py @@ -4,24 +4,235 @@ Components/Menu .. seealso:: - `Material Design spec, Menus `_ + `Material Design spec, Menus `_ .. rubric:: Menus display a list of choices on temporary surfaces. -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-previous.png +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-preview.png :align: center +- Menus should be easy to open, close, and interact with +- Menu content should be suited to user needs +- Menu items should be easy to scan + Usage ----- .. code-block:: python from kivy.lang import Builder + from kivy.metrics import dp from kivymd.app import MDApp from kivymd.uix.menu import MDDropdownMenu KV = ''' + MDScreen: + + MDRaisedButton: + id: button + text: "Press me" + pos_hint: {"center_x": .5, "center_y": .5} + on_release: app.menu_open() + ''' + + + class Test(MDApp): + def menu_open(self): + menu_items = [ + { + "text": f"Item {i}", + "on_release": lambda x=f"Item {i}": self.menu_callback(x), + } for i in range(5) + ] + MDDropdownMenu( + caller=self.root.ids.button, items=menu_items + ).open() + + def menu_callback(self, text_item): + print(text_item) + + def build(self): + self.theme_cls.primary_palette = "Orange" + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-usage.gif + :align: center + +Anatomy +------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-anatomy.png + :align: center + +You can combine the following parameters: +----------------------------------------- + +- leading_icon +- text +- trailing_icon +- trailing_text + +...to create the necessary types of menu items: + +.. code-block:: python + + menu_items = [ + { + "text": "Strikethrough", + "leading_icon": "check", + "trailing_icon": "apple-keyboard-command", + "trailing_text": "+Shift+X", + } + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon-trailing-icon-trailing-text.png + :align: center + +.. code-block:: python + + menu_items = [ + { + "text": "Strikethrough", + "trailing_icon": "apple-keyboard-command", + "trailing_text": "+Shift+X", + } + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-trailing-icon-trailing-text.png + :align: center + +.. code-block:: python + + menu_items = [ + { + "text": "Strikethrough", + "trailing_icon": "apple-keyboard-command", + } + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-trailing-icon.png + :align: center + +.. code-block:: python + + menu_items = [ + { + "text": "Strikethrough", + "trailing_text": "Shift+X", + } + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-trailing-text.png + :align: center + +.. code-block:: python + + menu_items = [ + { + "text": "Strikethrough", + "leading_icon": "check", + "trailing_icon": "apple-keyboard-command", + } + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon-trailing-icon.png + :align: center + +.. code-block:: python + + menu_items = [ + { + "text": "Strikethrough", + "leading_icon": "check", + } + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon.png + :align: center + +.. code-block:: python + + menu_items = [ + { + "text": "Strikethrough", + "leading_icon": "check", + "trailing_text": "Shift+X", + } + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-leading-icon-trailing-text.png + :align: center + +.. code-block:: python + + menu_items = [ + { + "text": "Strikethrough", + } + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-text.png + :align: center + +You can use the following parameters to customize the menu items: +----------------------------------------------------------------- + +- text_color +- leading_icon_color +- trailing_icon_color +- trailing_text_color + +.. code-block:: python + + menu_items = [ + { + "text": "Strikethrough", + "leading_icon": "check", + "trailing_icon": "apple-keyboard-command", + "trailing_text": "+Shift+X", + "leading_icon_color": "orange", + "trailing_icon_color": "green", + "trailing_text_color": "red", + } + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-item-customize.png + :align: center + +.. Header: +Header +------ + +.. code-block:: python + + from kivy.lang import Builder + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + from kivymd.uix.boxlayout import MDBoxLayout + + KV = ''' + + spacing: "12dp" + padding: "4dp" + adaptive_height: True + + MDIconButton: + icon: "gesture-tap-button" + pos_hint: {"center_y": .5} + + MDLabel: + text: "Actions" + adaptive_size: True + pos_hint: {"center_y": .5} + + MDScreen: MDRaisedButton: @@ -32,6 +243,184 @@ Usage ''' + class MenuHeader(MDBoxLayout): + '''An instance of the class that will be added to the menu header.''' + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [ + { + "text": f"Item {i}", + "on_release": lambda x=f"Item {i}": self.menu_callback(x), + } for i in range(5) + ] + self.menu = MDDropdownMenu( + header_cls=MenuHeader(), + caller=self.screen.ids.button, + items=menu_items, + ) + + def menu_callback(self, text_item): + print(text_item) + + def build(self): + self.theme_cls.primary_palette = "Orange" + self.theme_cls.theme_style = "Dark" + return self.screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-header.png + :align: center + +Menu with MDTopAppBar +--------------------- + +The :class:`~MDDropdownMenu` works well with the standard +:class:`~kivymd.uix.toolbar.MDTopAppBar`. Since the buttons on the Toolbar are created +by the MDTopAppBar component, it is necessary to pass the button as an argument to +the callback using `lambda x: app.callback(x)`. This example uses drop down menus +for both the righthand and lefthand menus. + +.. code-block:: python + + from kivy.lang import Builder + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + from kivymd.uix.snackbar import Snackbar + + KV = ''' + MDBoxLayout: + orientation: "vertical" + + MDTopAppBar: + title: "MDTopAppBar" + left_action_items: [["menu", lambda x: app.callback(x)]] + right_action_items: [["dots-vertical", lambda x: app.callback(x)]] + + MDLabel: + text: "Content" + halign: "center" + ''' + + + class Test(MDApp): + def build(self): + self.theme_cls.primary_palette = "Orange" + self.theme_cls.theme_style = "Dark" + menu_items = [ + { + "text": f"Item {i}", + "on_release": lambda x=f"Item {i}": self.menu_callback(x), + } for i in range(5) + ] + self.menu = MDDropdownMenu(items=menu_items) + return Builder.load_string(KV) + + def callback(self, button): + self.menu.caller = button + self.menu.open() + + def menu_callback(self, text_item): + self.menu.dismiss() + Snackbar(text=text_item).open() + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-menu.png + :align: center + +.. Position: +Position +======== + +Bottom position +--------------- + +.. seealso:: + + :attr:`~MDDropdownMenu.position` + +.. code-block:: python + + from kivy.lang import Builder + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + MDScreen: + + MDTextField: + id: field + pos_hint: {'center_x': .5, 'center_y': .6} + size_hint_x: None + width: "200dp" + hint_text: "Password" + on_focus: if self.focus: app.menu.open() + ''' + + + class Test(MDApp): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.screen = Builder.load_string(KV) + menu_items = [ + { + "text": f"Item {i}", + "on_release": lambda x=f"Item {i}": self.set_item(x), + } for i in range(5)] + self.menu = MDDropdownMenu( + caller=self.screen.ids.field, + items=menu_items, + position="bottom", + ) + + def set_item(self, text_item): + self.screen.ids.field.text = text_item + self.menu.dismiss() + + def build(self): + self.theme_cls.primary_palette = "Orange" + self.theme_cls.theme_style = "Dark" + return self.screen + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.png + :align: center + +Center position +--------------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.metrics import dp + + from kivymd.app import MDApp + from kivymd.uix.menu import MDDropdownMenu + + KV = ''' + MDScreen: + + MDDropDownItem: + id: drop_item + pos_hint: {'center_x': .5, 'center_y': .5} + text: 'Item 0' + on_release: app.menu.open() + ''' + + class Test(MDApp): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -39,44 +428,36 @@ Usage menu_items = [ { "text": f"Item {i}", - "viewclass": "OneLineListItem", - "on_release": lambda x=f"Item {i}": self.menu_callback(x), + "on_release": lambda x=f"Item {i}": self.set_item(x), } for i in range(5) ] self.menu = MDDropdownMenu( - caller=self.screen.ids.button, + caller=self.screen.ids.drop_item, items=menu_items, - width_mult=4, + position="center", ) + self.menu.bind() - def menu_callback(self, text_item): - print(text_item) + def set_item(self, text_item): + self.screen.ids.drop_item.set_item(text_item) + self.menu.dismiss() def build(self): + self.theme_cls.primary_palette = "Orange" + self.theme_cls.theme_style = "Dark" return self.screen Test().run() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-usage.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position-center.gif :align: center -.. Warning:: Do not create the :class:`~MDDropdownMenu` object when you open - the menu window. Because on a mobile device this one will be very slow! +API break +========= -Wrong ------ - -.. code-block:: python - - menu = MDDropdownMenu(caller=self.screen.ids.button, items=menu_items) - menu.open() - -Customization of menu item --------------------------- - -Menu items are created in the same way as items for the -:class:`~kivy.uix.recycleview.RecycleView` class. +1.1.1 version +------------- .. code-block:: python @@ -97,7 +478,7 @@ Menu items are created in the same way as items for the MDIconButton: icon: root.icon - user_font_size: "16sp" + icon_size: "16sp" md_bg_color_disabled: 0, 0, 0, 0 MDLabel: @@ -146,9 +527,9 @@ Menu items are created in the same way as items for the menu_items = [ { "text": f"Item {i}", - "right_text": f"R+{i}", + "right_text": "+Shift+X", "right_icon": "apple-keyboard-command", - "left_icon": "git", + "left_icon": "web", "viewclass": "Item", "height": dp(54), "on_release": lambda x=f"Item {i}": self.menu_callback(x), @@ -157,6 +538,7 @@ Menu items are created in the same way as items for the self.menu = MDDropdownMenu( caller=self.screen.ids.button, items=menu_items, + bg_color="#bdc6b0", width_mult=4, ) @@ -169,13 +551,8 @@ Menu items are created in the same way as items for the Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-right.gif - :align: center - -.. Header: -Header ------- +1.2.0 version +------------- .. code-block:: python @@ -184,28 +561,8 @@ Header from kivymd.app import MDApp from kivymd.uix.menu import MDDropdownMenu - from kivymd.uix.boxlayout import MDBoxLayout KV = ''' - - orientation: "vertical" - adaptive_size: True - padding: "4dp" - - MDBoxLayout: - spacing: "12dp" - adaptive_size: True - - MDIconButton: - icon: "gesture-tap-button" - pos_hint: {"center_y": .5} - - MDLabel: - text: "Actions" - adaptive_size: True - pos_hint: {"center_y": .5} - - MDScreen: MDRaisedButton: @@ -216,10 +573,6 @@ Header ''' - class MenuHeader(MDBoxLayout): - '''An instance of the class that will be added to the menu header.''' - - class Test(MDApp): def __init__(self, **kwargs): super().__init__(**kwargs) @@ -227,16 +580,18 @@ Header menu_items = [ { "text": f"Item {i}", - "viewclass": "OneLineListItem", - "height": dp(56), + "leading_icon": "web", + "trailing_icon": "apple-keyboard-command", + "trailing_text": "+Shift+X", + "trailing_icon_color": "grey", + "trailing_text_color": "grey", "on_release": lambda x=f"Item {i}": self.menu_callback(x), } for i in range(5) ] self.menu = MDDropdownMenu( - header_cls=MenuHeader(), + md_bg_color="#bdc6b0", caller=self.screen.ids.button, items=menu_items, - width_mult=4, ) def menu_callback(self, text_item): @@ -247,232 +602,26 @@ Header Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-header.png - :align: center - -Menu with MDTopAppBar ---------------------- - -The :class:`~MDDropdownMenu` works well with the standard -:class:`~kivymd.uix.toolbar.MDTopAppBar`. Since the buttons on the Toolbar are created -by the MDTopAppBar component, it is necessary to pass the button as an argument to -the callback using `lambda x: app.callback(x)`. - -.. note:: This example uses drop down menus for both the righthand and - lefthand menus (i.e both the 'triple bar' and 'triple dot' menus) to - illustrate that it is possible. A better solution for the 'triple bar' menu - would probably have been :class:`~kivymd.uix.MDNavigationDrawer`. - - -.. code-block:: python - - from kivy.lang import Builder - from kivy.metrics import dp - - from kivymd.app import MDApp - from kivymd.uix.menu import MDDropdownMenu - from kivymd.uix.snackbar import Snackbar - - KV = ''' - MDBoxLayout: - orientation: "vertical" - - MDTopAppBar: - title: "MDTopAppBar" - left_action_items: [["menu", lambda x: app.callback(x)]] - right_action_items: [["dots-vertical", lambda x: app.callback(x)]] - - MDLabel: - text: "Content" - halign: "center" - ''' - - - class Test(MDApp): - def build(self): - menu_items = [ - { - "viewclass": "OneLineListItem", - "text": f"Item {i}", - "height": dp(56), - "on_release": lambda x=f"Item {i}": self.menu_callback(x), - } for i in range(5) - ] - self.menu = MDDropdownMenu( - items=menu_items, - width_mult=4, - ) - return Builder.load_string(KV) - - def callback(self, button): - self.menu.caller = button - self.menu.open() - - def menu_callback(self, text_item): - self.menu.dismiss() - Snackbar(text=text_item).open() - - - Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-menu.gif - :align: center - -.. Position: -Position -======== - -Bottom position ---------------- - -.. seealso:: - - :attr:`~MDDropdownMenu.position` - -.. code-block:: python - - from kivy.lang import Builder - from kivy.metrics import dp - from kivy.properties import StringProperty - - from kivymd.uix.list import OneLineIconListItem - from kivymd.app import MDApp - from kivymd.uix.menu import MDDropdownMenu - - KV = ''' - - - IconLeftWidget: - icon: root.icon - - - MDScreen - - MDTextField: - id: field - pos_hint: {'center_x': .5, 'center_y': .6} - size_hint_x: None - width: "200dp" - hint_text: "Password" - on_focus: if self.focus: app.menu.open() - ''' - - - class IconListItem(OneLineIconListItem): - icon = StringProperty() - - - class Test(MDApp): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.screen = Builder.load_string(KV) - menu_items = [ - { - "viewclass": "IconListItem", - "icon": "git", - "height": dp(56), - "text": f"Item {i}", - "on_release": lambda x=f"Item {i}": self.set_item(x), - } for i in range(5)] - self.menu = MDDropdownMenu( - caller=self.screen.ids.field, - items=menu_items, - position="bottom", - width_mult=4, - ) - - def set_item(self, text__item): - self.screen.ids.field.text = text__item - self.menu.dismiss() - - def build(self): - return self.screen - - - Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.gif - :align: center - -Center position ---------------- - -.. code-block:: python - - from kivy.lang import Builder - from kivy.metrics import dp - from kivy.properties import StringProperty - - from kivymd.uix.list import OneLineIconListItem - from kivymd.app import MDApp - from kivymd.uix.menu import MDDropdownMenu - - KV = ''' - - - IconLeftWidget: - icon: root.icon - - - MDScreen - - MDDropDownItem: - id: drop_item - pos_hint: {'center_x': .5, 'center_y': .5} - text: 'Item 0' - on_release: app.menu.open() - ''' - - - class IconListItem(OneLineIconListItem): - icon = StringProperty() - - - class Test(MDApp): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.screen = Builder.load_string(KV) - menu_items = [ - { - "viewclass": "IconListItem", - "icon": "git", - "text": f"Item {i}", - "height": dp(56), - "on_release": lambda x=f"Item {i}": self.set_item(x), - } for i in range(5) - ] - self.menu = MDDropdownMenu( - caller=self.screen.ids.drop_item, - items=menu_items, - position="center", - width_mult=4, - ) - self.menu.bind() - - def set_item(self, text_item): - self.screen.ids.drop_item.set_item(text_item) - self.menu.dismiss() - - def build(self): - return self.screen - - - Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position-center.gif - :align: center """ -__all__ = ("MDDropdownMenu",) +from __future__ import annotations + +__all__ = ( + "BaseDropdownItem", + "MDDropdownMenu", + "MDDropdownTextItem", + "MDDropdownLeadingIconItem", + "MDDropdownTrailingIconItem", + "MDDropdownTrailingIconTextItem", + "MDDropdownTrailingTextItem", + "MDDropdownLeadingTrailingIconTextItem", + "MDDropdownLeadingIconTrailingTextItem", +) import os -from typing import Union -from kivy.animation import Animation from kivy.clock import Clock from kivy.core.window import Window -from kivy.core.window.window_sdl2 import WindowSDL from kivy.lang import Builder from kivy.metrics import dp from kivy.properties import ( @@ -481,15 +630,19 @@ from kivy.properties import ( NumericProperty, ObjectProperty, OptionProperty, - StringProperty, VariableListProperty, + StringProperty, ) -from kivy.uix.floatlayout import FloatLayout from kivy.uix.recycleview import RecycleView import kivymd.material_resources as m_res from kivymd import uix_path -from kivymd.theming import ThemableBehavior +from kivymd.uix.behaviors import StencilBehavior, RectangularRippleBehavior +from kivymd.uix.behaviors.motion_behavior import MotionDropDownMenuBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.card import MDCard +from kivymd.uix.label import MDLabel +from kivymd.uix.list import IRightBody with open( os.path.join(uix_path, "menu", "menu.kv"), encoding="utf-8" @@ -501,6 +654,8 @@ class MDMenu(RecycleView): width_mult = NumericProperty(1) """ See :attr:`~MDDropdownMenu.width_mult`. + + .. deprecated:: 1.2.0 """ drop_cls = ObjectProperty() @@ -509,8 +664,223 @@ class MDMenu(RecycleView): """ -class MDDropdownMenu(ThemableBehavior, FloatLayout): +class BaseDropdownItem(RectangularRippleBehavior, MDBoxLayout): """ + Base class for menu items. + + .. versionadded:: 1.2.0 + + For more information, see in the + :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and + :class:`~kivymd.uix.boxlayout.MDBoxLayout` classes. + """ + + text = StringProperty() + """ + The text of the menu item. + + :attr:`text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + leading_icon = StringProperty() + """ + The leading icon of the menu item. + + :attr:`leading_icon` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + trailing_icon = StringProperty() + """ + The trailing icon of the menu item. + + :attr:`trailing_icon` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + trailing_text = StringProperty() + """ + The trailing text of the menu item. + + :attr:`trailing_text` is a :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + text_color = ColorProperty(None) + """ + The color of the text in (r, g, b, a) or string format for the text of the + menu item. + + :attr:`text_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + leading_icon_color = ColorProperty(None) + """ + The color of the text in (r, g, b, a) or string format for the leading icon + of the menu item. + + :attr:`leading_icon_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + trailing_icon_color = ColorProperty(None) + """ + The color of the text in (r, g, b, a) or string format for the trailing + icon of the menu item. + + :attr:`leading_icon_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + trailing_text_color = ColorProperty(None) + """ + The color of the text in (r, g, b, a) or string format for the trailing + text of the menu item. + + :attr:`leading_icon_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + divider = OptionProperty("Full", options=["Full", None], allownone=True) + """ + Divider mode. Available options are: `'Full'`, `None` + and default to `'Full'`. + + :attr:`divider` is a :class:`~kivy.properties.OptionProperty` + and defaults to `'Full'`. + """ + + divider_color = ColorProperty(None) + """ + Divider color in (r, g, b, a) or string format. + + :attr:`divider_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + +class MDTrailingTextContainer(BaseDropdownItem, IRightBody, MDLabel): + """ + Implements a container for trailing text. + + .. versionadded:: 1.2.0 + + For more information, see in the + :class:`~BaseDropdownItem` and + :class:`~kivymd.uix.list.IRightBody` and + :class:`~kivymd.uix.label.MDLabel` classes. + """ + + +class MDTrailingIconTextContainer(BaseDropdownItem, IRightBody, MDBoxLayout): + """ + Implements a container for trailing icons and trailing text. + + .. versionadded:: 1.2.0 + + For more information, see in the + :class:`~BaseDropdownItem` and + :class:`~kivymd.uix.list.IRightBody` and + :class:`~kivymd.uix.boxlayout.MDBoxLayout` classes. + """ + + +class MDDropdownTextItem(BaseDropdownItem): + """ + Implements a menu item with text without leading and trailing icons. + + .. versionadded:: 1.2.0 + + For more information, see in the :class:`~BaseDropdownItem` class. + """ + + +class MDDropdownLeadingIconItem(BaseDropdownItem): + """ + Implements a menu item with text, leading icon and without trailing icon. + + .. versionadded:: 1.2.0 + + For more information, see in the :class:`~BaseDropdownItem` class. + """ + + +class MDDropdownTrailingIconItem(BaseDropdownItem): + """ + Implements a menu item with text, without leading icon and with trailing + icon. + + .. versionadded:: 1.2.0 + + For more information, see in the :class:`~BaseDropdownItem` class. + """ + + +class MDDropdownTrailingIconTextItem(BaseDropdownItem): + """ + Implements a menu item with text, without leading icon, with trailing + icon and with trailing text. + + .. versionadded:: 1.2.0 + + For more information, see in the :class:`~BaseDropdownItem` class. + """ + + +class MDDropdownTrailingTextItem(BaseDropdownItem): + """ + Implements a menu item with text, without leading icon, without trailing + icon and with trailing text. + + .. versionadded:: 1.2.0 + + For more information, see in the :class:`~BaseDropdownItem` class. + """ + + +class MDDropdownLeadingIconTrailingTextItem(BaseDropdownItem): + """ + Implements a menu item with text, leading icon and with trailing text. + + .. versionadded:: 1.2.0 + + For more information, see in the :class:`~BaseDropdownItem` class. + """ + + +class MDDropdownLeadingTrailingIconTextItem(BaseDropdownItem): + """ + Implements a menu item with text, with leading icon, with trailing + icon and with trailing text. + + .. versionadded:: 1.2.0 + + For more information, see in the :class:`~BaseDropdownItem` class. + """ + + +class MDDropdownLeadingTrailingIconItem(BaseDropdownItem): + """ + Implements a menu item with text, with leading icon, with trailing icon. + + .. versionadded:: 1.2.0 + + For more information, see in the :class:`~BaseDropdownItem` class. + """ + + +class MDDropdownMenu(MotionDropDownMenuBehavior, StencilBehavior, MDCard): + """ + Dropdown menu class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.MotionDropDownMenuBehavior` and + :class:`~kivymd.uix.behaviors.StencilBehavior` and + :class:`~kivymd.uix.card.MDCard` + classes documentation. + :Events: `on_release` The method that will be called when you click menu items. @@ -525,40 +895,19 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): See Header_ for more information. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-header-cls.png - :align: center - :attr:`header_cls` is a :class:`~kivy.properties.ObjectProperty` and defaults to `None`. """ items = ListProperty() """ - See :attr:`~kivy.uix.recycleview.RecycleView.data`. - - .. code-block:: python - - items = [ - { - "viewclass": "OneLineListItem", - "height": dp(56), - "text": f"Item {i}", - } - for i in range(5) - ] - self.menu = MDDropdownMenu( - items=items, - ..., - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-items.png - :align: center + List of dictionaries with properties for menu items. :attr:`items` is a :class:`~kivy.properties.ListProperty` and defaults to `[]`. """ - width_mult = NumericProperty(1) + width_mult = NumericProperty(1, deprecated=True) """ This number multiplied by the standard increment ('56dp' on mobile, '64dp' on desktop), determines the width of the menu items. @@ -566,54 +915,27 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): If the resulting number were to be too big for the application Window, the multiplier will be adjusted for the biggest possible one. - .. code-block:: python + .. deprecated:: 1.2.0 - self.menu = MDDropdownMenu( - width_mult=4, - ..., - ) + Use `width` instead. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-width-mult-4.png - :align: center + .. code-block:: python - .. code-block:: python - - self.menu = MDDropdownMenu( - width_mult=8, - ..., - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-width-mult-8.png - :align: center + self.menu = MDDropdownMenu( + width=dp(240), + ..., + ) :attr:`width_mult` is a :class:`~kivy.properties.NumericProperty` and defaults to `1`. """ + min_height = NumericProperty(dp(48)) + max_height = NumericProperty() """ The menu will grow no bigger than this number. Set to 0 for no limit. - .. code-block:: python - - self.menu = MDDropdownMenu( - max_height=dp(112), - ..., - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-max-height-112.png - :align: center - - .. code-block:: python - - self.menu = MDDropdownMenu( - max_height=dp(224), - ..., - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-max-height-224.png - :align: center - :attr:`max_height` is a :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ @@ -622,16 +944,6 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): """ Margin between Window border and menu. - .. code-block:: python - - self.menu = MDDropdownMenu( - border_margin=dp(4), - ..., - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-border-margin-4.png - :align: center - .. code-block:: python self.menu = MDDropdownMenu( @@ -658,7 +970,7 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): ..., ) - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-up.gif + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-up.png :align: center .. code-block:: python @@ -668,7 +980,7 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): ..., ) - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-down.gif + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-ver-growth-down.png :align: center :attr:`ver_growth` is a :class:`~kivy.properties.OptionProperty` @@ -687,7 +999,7 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): ..., ) - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-left.gif + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-left.png :align: center .. code-block:: python @@ -697,48 +1009,25 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): ..., ) - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-right.gif + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-hor-growth-right.png :align: center :attr:`hor_growth` is a :class:`~kivy.properties.OptionProperty` and defaults to `None`. """ - background_color = ColorProperty(None) + background_color = ColorProperty(None, deprecated=True) """ - Color of the background of the menu. + Color in (r, g, b, a) or string format of the background of the menu. - .. code-block:: python + .. deprecated:: 1.2.0 - self.menu = MDDropdownMenu( - background_color=self.theme_cls.primary_light, - ..., - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-background-color.png - :align: center + Use `md_bg_color` instead. :attr:`background_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ - opening_transition = StringProperty("out_cubic") - """ - Type of animation for opening a menu window. - - :attr:`opening_transition` is a :class:`~kivy.properties.StringProperty` - and defaults to `'out_cubic'`. - """ - - opening_time = NumericProperty(0.2) - """ - Menu window opening animation time and you can set it to 0 - if you don't want animation of menu opening. - - :attr:`opening_time` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0.2`. - """ - caller = ObjectProperty() """ The widget object that calls the menu window. @@ -752,13 +1041,10 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): ) """ Menu window position relative to parent element. - Available options are: `'auto'`, `'center'`, `'bottom'`. + Available options are: `'auto'`, `'top'`, `'center'`, `'bottom'`. See Position_ for more information. - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-position.png - :align: center - :attr:`position` is a :class:`~kivy.properties.OptionProperty` and defaults to `'auto'`. """ @@ -767,282 +1053,333 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): """ Menu radius. - .. code-block:: python - - self.menu = MDDropdownMenu( - radius=[24, 0, 24, 0], - ..., - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-radius.png - :align: center - :attr:`radius` is a :class:`~kivy.properties.VariableListProperty` and defaults to `'[dp(7)]'`. """ - elevation = NumericProperty(4) + elevation = NumericProperty(m_res.DROP_DOWN_MENU_ELEVATION) """ - Elevation value of menu dialog. - - .. versionadded:: 1.0.0 - - .. code-block:: python - - self.menu = MDDropdownMenu( - elevation=4, - ..., - ) - - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/menu-elevation.png - :align: center + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.elevation` + attribute. :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` - and defaults to `4`. + and defaults to `2`. """ + shadow_radius = VariableListProperty([6], length=4) + """ + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_radius` + attribute. + + :attr:`shadow_radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[6]`. + """ + + shadow_softness = NumericProperty(m_res.DROP_DOWN_MENU_SOFTNESS) + """ + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_softness` + attribute. + + :attr:`shadow_softness` is an :class:`~kivy.properties.NumericProperty` + and defaults to `6`. + """ + + shadow_offset = ListProperty(m_res.DROP_DOWN_MENU_OFFSET) + """ + See :attr:`kivymd.uix.behaviors.elevation.CommonElevationBehavior.shadow_offset` + attribute. + + :attr:`shadow_offset` is an :class:`~kivy.properties.ListProperty` + and defaults to `(0, -2)`. + """ + + _items = [] _start_coords = [] - _calculate_complete = False - _calculate_process = False + _tar_x = 0 + _tar_y = 0 def __init__(self, **kwargs): super().__init__(**kwargs) - Window.bind(on_resize=self.check_position_caller) - Window.bind(on_maximize=self.set_menu_properties) - Window.bind(on_restore=self.set_menu_properties) - Clock.schedule_once(self.ajust_radius) + Window.bind( + on_resize=self._remove_menu, + on_maximize=self._remove_menu, + on_restore=self._remove_menu, + ) self.register_event_type("on_dismiss") self.menu = self.ids.md_menu self.target_height = 0 - def check_position_caller( - self, instance_window: WindowSDL, width: int, height: int - ) -> None: - """Called when the application root window is resized.""" + def adjust_width(self) -> None: + """ + Adjust the width of the menu if the width of the menu goes beyond + the boundaries of the parent window from starting point. + """ - # FIXME: Menu position is not recalculated when changing the size of - # the root application window. - self.set_menu_properties(0) + if self._start_coords[0] >= Window.width / 2: + if self.width > self._start_coords[0]: + self.width = ( + self._start_coords[0] + - self.border_margin + - ( + (self.caller.width / 2 + self.border_margin) + if self.position in ["right", "left"] + else 0 + ) + ) + else: + if Window.width - self._start_coords[0] < self.width: + self.width = ( + Window.width - self._start_coords[0] - self.border_margin + ) - def set_menu_properties(self, interval: Union[int, float] = 0) -> None: + def check_ver_growth(self) -> None: + """ + Checks whether the height of the lower/upper borders of the menu + exceeds the limits borders of the parent window. + """ + + if self.target_height > self._start_coords[1] - self.border_margin: + self.ver_growth = "up" + else: + if self._start_coords[1] > Window.height - self._start_coords[1]: + self.ver_growth = "down" + + def check_hor_growth(self) -> None: + """ + Checks whether the width of the left/right menu borders exceeds the + boundaries of the parent window. + """ + + if ( + Window.width - (self._start_coords[0] + self.border_margin) + <= self.width + ): + self.hor_growth = "left" + elif self.width >= self._start_coords[0] + self.border_margin: + self.hor_growth = "right" + + def get_target_pos(self) -> [float, float]: + self._tar_x, self._tar_y = self._start_coords + + if self.ver_growth == "up": + self._tar_y = self._start_coords[1] + self.height + else: + self._tar_y = self._start_coords[1] + + if self.hor_growth == "left": + self._tar_x = self._start_coords[0] - self.width + else: + self._tar_x = self._start_coords[0] + + return self._tar_x, self._tar_y + + def set_target_height(self) -> None: + """ + Set the target height of the menu depending on the size of each item. + """ + + self.target_height = 0 + for item in self.menu.data: + self.target_height += item.get("height", self.min_height) + + if 0 < self.max_height < self.target_height: + self.target_height = self.max_height + + if self._start_coords[1] >= Window.height / 2: + if self.target_height > self._start_coords[1]: + self.target_height = ( + self._start_coords[1] + - self.border_margin + - ( + (self.caller.height / 2 + self.border_margin) + if self.position in ["top", "bottom"] + else 0 + ) + ) + else: + if Window.height - self._start_coords[1] < self.target_height: + self.target_height = ( + Window.height - self._start_coords[1] - self.border_margin + ) + + def set_menu_properties(self, *args) -> None: """Sets the size and position for the menu window.""" if self.caller: - self.ids.md_menu.data = self.items + self.menu.data = self._items # We need to pick a starting point, see how big we need to be, # and where to grow to. - self._start_coords = self.caller.to_window( - self.caller.center_x, self.caller.center_y + self._start_coords = self.caller.to_window(*self.caller.center) + + self.adjust_width() + self.set_target_height() + self.check_ver_growth() + self.check_hor_growth() + + def set_menu_pos(self, *args) -> None: + if self.position == "auto": + self.menu.x = self._tar_x + self.menu.y = self._tar_y - ( + self.header_cls.height if self.header_cls else 0 ) - self.target_width = self.width_mult * m_res.STANDARD_INCREMENT - - # If we're wider than the Window... - if self.target_width > Window.width: - # ...reduce our multiplier to max allowed. - self.target_width = ( - int(Window.width / m_res.STANDARD_INCREMENT) - * m_res.STANDARD_INCREMENT + else: + if self.position == "center": + self.pos = ( + self._start_coords[0] - self.width / 2, + self._start_coords[1] - self.height / 2, + ) + elif self.position == "bottom": + self.pos = ( + (self._start_coords[0] - self.width / 2) + if not self.hor_growth + else ( + (self._start_coords[0] - self.width) + if self.hor_growth == "left" + else (self._start_coords[0]) + ), + self._start_coords[1] + - ( + self.height + + self.border_margin + + self.caller.height / 2 + ), + ) + elif self.position == "top": + self.pos = ( + (self._start_coords[0] - self.width / 2) + if not self.hor_growth + else ( + (self._start_coords[0] - self.width) + if self.hor_growth == "left" + else (self._start_coords[0]) + ), + self._start_coords[1] + + self.caller.height / 2 + + self.border_margin, ) - - # Set the target_height of the menu depending on the size of - # each MDMenuItem or MDMenuItemIcon. - self.target_height = 0 - for item in self.ids.md_menu.data: - self.target_height += item.get("height", dp(72)) - - # If we're over max_height... - if 0 < self.max_height < self.target_height: - self.target_height = self.max_height - - # Establish vertical growth direction. - if self.ver_growth is not None: - ver_growth = self.ver_growth - else: - # If there's enough space below us: - if ( - self.target_height - <= self._start_coords[1] - self.border_margin - ): - ver_growth = "down" - # if there's enough space above us: - elif ( - self.target_height - < Window.height - self._start_coords[1] - self.border_margin - ): - ver_growth = "up" - # Otherwise, let's pick the one with more space and adjust - # ourselves. - else: - # If there"s more space below us: - if ( - self._start_coords[1] - >= Window.height - self._start_coords[1] - ): - ver_growth = "down" - self.target_height = ( - self._start_coords[1] - self.border_margin - ) - # If there's more space above us: - else: - ver_growth = "up" - self.target_height = ( - Window.height - - self._start_coords[1] - - self.border_margin - ) - - if self.hor_growth is not None: - hor_growth = self.hor_growth - else: - # If there's enough space to the right: - if ( - self.target_width - <= Window.width - self._start_coords[0] - self.border_margin - ): - hor_growth = "right" - # if there's enough space to the left: - elif ( - self.target_width - < self._start_coords[0] - self.border_margin - ): - hor_growth = "left" - # Otherwise, let's pick the one with more space and adjust - # ourselves. - else: - # if there"s more space to the right: - if ( - Window.width - self._start_coords[0] - >= self._start_coords[0] - ): - hor_growth = "right" - self.target_width = ( - Window.width - - self._start_coords[0] - - self.border_margin - ) - # if there"s more space to the left: - else: - hor_growth = "left" - self.target_width = ( - self._start_coords[0] - self.border_margin - ) - - if ver_growth == "down": - self.tar_y = self._start_coords[1] - self.target_height - else: # should always be "up" - self.tar_y = self._start_coords[1] - - if hor_growth == "right": - self.tar_x = self._start_coords[0] - else: # should always be "left" - self.tar_x = self._start_coords[0] - self.target_width - self._calculate_complete = True - - def ajust_radius(self, interval: Union[int, float]) -> None: - """ - Adjusts the radius of the first and last items in the menu list - according to the radius that is set for the menu. - """ - - if self.items: - radius_for_firt_item = self.radius[:2] - radius_for_last_item = self.radius[2:] - - firt_data_item = self.items[0] - last_data_item = self.items[-1] - - firt_data_item["radius"] = radius_for_firt_item + [0, 0] - last_data_item["radius"] = [0, 0] + radius_for_last_item - last_data_item["divider"] = None - - self.items[0] = firt_data_item - self.items[-1] = last_data_item - - # For all other elements of the list, except for the first and - # last, we set the value of the radius to `0`. - for i, data_item in enumerate(self.items): - if "radius" not in data_item: - data_item["radius"] = 0 - self.items[i] = data_item def adjust_position(self) -> str: """ - Returns value 'auto' for the menu position if the menu position is out + Return value 'auto' for the menu position if the menu position is out of screen. """ - target_width = self.target_width - target_height = self.target_height - caller = self.caller position = self.position - if ( - caller.x < target_width - or caller.x + target_width > Window.width - or caller.y + target_height > Window.height - or (caller.y < target_height and position == "center") - ): - position = "auto" - if self.hor_growth or self.ver_growth: - self.hor_growth = None - self.ver_growth = None - self.set_menu_properties() + if position == "bottom": + if ( + self._start_coords[1] + - (self.height + self.border_margin + self.caller.height / 2) + < 0 + ): + position = "auto" + elif position == "top": + if ( + self._start_coords[1] + + self.caller.height / 2 + + self.border_margin + > Window.height + ): + position = "auto" + elif position == "center": + if ( + ( + self._start_coords[1] + self.height / 2 > Window.height + or self._start_coords[1] - self.height / 2 < 0 + ) + or Window.width - (self._start_coords[0] + self.border_margin) + < self.width / 2 + or self._start_coords[0] + self.border_margin < self.width / 2 + ): + position = "auto" + return position def open(self) -> None: """Animate the opening of a menu window.""" - def open(interval): - if not self._calculate_complete: - return - - position = self.adjust_position() - - if position == "auto": - self.menu.pos = self._start_coords - anim = Animation( - x=self.tar_x, - y=self.tar_y - - (self.header_cls.height if self.header_cls else 0), - width=self.target_width, - height=self.target_height, - duration=self.opening_time, - opacity=1, - transition=self.opening_transition, - ) - anim.start(self.menu) - else: - if position == "center": - self.menu.pos = ( - self._start_coords[0] - self.target_width / 2, - self._start_coords[1] - self.target_height / 2, - ) - elif position == "bottom": - self.menu.pos = ( - self._start_coords[0] - self.target_width / 2, - self.caller.pos[1] - self.target_height, - ) - elif position == "top": - self.menu.pos = ( - self._start_coords[0] - self.target_width / 2, - self.caller.pos[1] + self.caller.height, - ) - anim = Animation( - width=self.target_width, - height=self.target_height, - duration=self.opening_time, - opacity=1, - transition=self.opening_transition, - ) - anim.start(self.menu) - Window.add_widget(self) - Clock.unschedule(open) - self._calculate_process = False - self.set_menu_properties() - if not self._calculate_process: - self._calculate_process = True - Clock.schedule_interval(open, 0) + Window.add_widget(self) + self.position = self.adjust_position() + + if self.width <= 100: + self.width = dp(240) + + self.height = self.target_height + self._tar_x, self._tar_y = self.get_target_pos() + self.x = self._tar_x + self.y = self._tar_y - self.target_height + self.scale_value_center = self.caller.center + self.set_menu_pos() + self.on_open() + + def on_items(self, instance, value: list) -> None: + """ + The method sets the class that will be used to create the menu item. + """ + + items = [] + viewclass = "MDDropdownTextItem" + + for data in value: + if "viewclass" not in data: + if ( + "leading_icon" not in data + and "trailing_icon" not in data + and "trailing_text" not in data + ): + viewclass = "MDDropdownTextItem" + elif ( + "leading_icon" in data + and "trailing_icon" not in data + and "trailing_text" not in data + ): + viewclass = "MDDropdownLeadingIconItem" + elif ( + "leading_icon" not in data + and "trailing_icon" in data + and "trailing_text" not in data + ): + viewclass = "MDDropdownTrailingIconItem" + elif ( + "leading_icon" not in data + and "trailing_icon" in data + and "trailing_text" in data + ): + viewclass = "MDDropdownTrailingIconTextItem" + elif ( + "leading_icon" in data + and "trailing_icon" in data + and "trailing_text" in data + ): + viewclass = "MDDropdownLeadingTrailingIconTextItem" + elif ( + "leading_icon" in data + and "trailing_icon" in data + and "trailing_text" not in data + ): + viewclass = "MDDropdownLeadingTrailingIconItem" + elif ( + "leading_icon" not in data + and "trailing_icon" not in data + and "trailing_text" in data + ): + viewclass = "MDDropdownTrailingTextItem" + elif ( + "leading_icon" in data + and "trailing_icon" not in data + and "trailing_text" in data + ): + viewclass = "MDDropdownLeadingIconTrailingTextItem" + + data["viewclass"] = viewclass + + if "height" not in data: + data["height"] = dp(48) + + items.append(data) + + self._items = items def on_header_cls( self, instance_dropdown_menu, instance_user_menu_header @@ -1070,19 +1407,15 @@ class MDDropdownMenu(ThemableBehavior, FloatLayout): super().on_touch_up(touch) return True - def on_dismiss(self) -> None: - """Called when the menu is closed.""" - - Window.remove_widget(self) - self.menu.width = 0 - self.menu.height = 0 - self.menu.opacity = 0 - def dismiss(self, *args) -> None: """Closes the menu.""" self.on_dismiss() + def _remove_menu(self, *args): + Window.remove_widget(self) + self.set_scale() + if __name__ == "__main__": # To test the correct menu position. @@ -1097,14 +1430,7 @@ if __name__ == "__main__": def __init__(self, **kwargs): super().__init__(**kwargs) self.screen = MDScreen() - menu_items = [ - { - "viewclass": "OneLineListItem", - "height": dp(56), - "text": f"Item {i}", - } - for i in range(5) - ] + menu_items = [{"text": f"Item {i}"} for i in range(55)] self.menu = MDDropdownMenu(items=menu_items, width_mult=4) def open_menu(self, caller): diff --git a/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py b/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py index 9ac5823..ac2eac3 100755 --- a/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py +++ b/sbapp/kivymd/uix/navigationdrawer/navigationdrawer.py @@ -575,8 +575,8 @@ class NavigationDrawerContentError(Exception): class MDNavigationLayout(MDFloatLayout): """ - For more information, see in the :class:`~kivymd.uix.floatlayout.MDFloatLayout` - class documentation. + For more information, see in the + :class:`~kivymd.uix.floatlayout.MDFloatLayout` class documentation. """ _scrim_color = ObjectProperty(None) @@ -737,7 +737,7 @@ class MDNavigationDrawerDivider(MDBoxLayout): color = ColorProperty(None) """ - Divider color in ``rgba`` format. + Divider color in (r, g, b, a) or string format. :attr:`color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -811,7 +811,7 @@ class MDNavigationDrawerHeader(MDBoxLayout): title_color = ColorProperty(None) """ - Title text color. + Title text color in (r, g, b, a) or string format. :attr:`title_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -851,7 +851,7 @@ class MDNavigationDrawerHeader(MDBoxLayout): text_color = ColorProperty(None) """ - Title text color. + Title text color in (r, g, b, a) or string format. :attr:`text_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -893,7 +893,9 @@ class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior): Implements an item for the :class:`~MDNavigationDrawer` menu list. For more information, see in the - :class:`~kivymd.uix.list.OneLineAvatarIconListItem` class documentation. + :class:`~kivymd.uix.list.OneLineAvatarIconListItem` and + :class:`~kivymd.uix.behaviors.FocusBehavior` + class documentation. .. versionadded:: 1.0.0 @@ -936,7 +938,7 @@ class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior): icon_color = ColorProperty(None) """ - Icon color item. + Icon color in (r, g, b, a) or string format item. :attr:`icon_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -944,7 +946,8 @@ class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior): selected_color = ColorProperty([0, 0, 0, 1]) """ - The color of the icon and text of the selected item. + The color in (r, g, b, a) or string format of the icon and text of the + selected item. :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 1]`. @@ -960,7 +963,7 @@ class MDNavigationDrawerItem(OneLineAvatarIconListItem, FocusBehavior): text_right_color = ColorProperty(None) """ - Right text color item. + Right text color item in (r, g, b, a) or string format. :attr:`text_right_color` is a :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -1095,9 +1098,9 @@ class MDNavigationDrawer(MDCard): # FIXME: Doesn't work in Kivy v2.1.0. scrim_color = ColorProperty([0, 0, 0, 0.5]) """ - Color for scrim. Alpha channel will be multiplied with - :attr:`_scrim_alpha`. Set fourth channel to 0 if you want to disable - scrim. + Color for scrim in (r, g, b, a) or string format. Alpha channel will be + multiplied with :attr:`_scrim_alpha`. Set fourth channel to 0 if you want + to disable scrim. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-drawer-scrim-color.png :align: center diff --git a/sbapp/kivymd/uix/navigationrail/navigationrail.py b/sbapp/kivymd/uix/navigationrail/navigationrail.py index 42e2e24..762c74d 100644 --- a/sbapp/kivymd/uix/navigationrail/navigationrail.py +++ b/sbapp/kivymd/uix/navigationrail/navigationrail.py @@ -8,26 +8,14 @@ Components/NavigationRail `Material Design spec, Navigation rail `_ -.. rubric:: - - Navigation rails provide access to primary destinations in apps when using - tablet and desktop screens. +.. rubric:: Navigation rails provide access to primary destinations in apps + when using tablet and desktop screens. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail.png :align: center Usage -===== - -.. code-block:: kv - - MDNavigationRail: - - MDNavigationRailItem: - - MDNavigationRailItem: - - MDNavigationRailItem: +----- .. tabs:: @@ -113,6 +101,21 @@ Usage .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-usage.png :align: center +Anatomy +------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/navigation-rail-anatomy.png + :align: center + +1. Container +2. Label text (optional) +3. Icon +4. Active indicator +5. Badge (optional) +6. Large badge (optional) +7. Large badge label (optional) +8. Menu icon (optional) + Example ======= @@ -137,9 +140,8 @@ Example - elevation: 3.5 + elevation: 1 shadow_radius: 12 - shadow_softness: 4 -height: "56dp" @@ -207,9 +209,9 @@ Example MDNavigationDrawer: id: nav_drawer - radius: (0, 16, 16, 0) + radius: 0, 16, 16, 0 md_bg_color: "#fffcf4" - elevation: 4 + elevation: 2 width: "240dp" MDNavigationDrawerMenu: @@ -218,14 +220,18 @@ Example orientation: "vertical" adaptive_height: True spacing: "12dp" - padding: "3dp", 0, 0, "12dp" + padding: 0, 0, 0, "12dp" MDIconButton: icon: "menu" - ExtendedButton: - text: "Compose" - icon: "pencil" + MDBoxLayout: + adaptive_height: True + padding: "12dp", 0, 0, 0 + + ExtendedButton: + text: "Compose" + icon: "pencil" DrawerClickableItem: text: "Python" @@ -269,7 +275,9 @@ Example def set_radius(self, *args): if self.rounded_button: - self._radius = self.radius = self.height / 4 + value = self.height / 4 + self.radius = [value, value, value, value] + self._radius = value class Example(MDApp): @@ -357,9 +365,8 @@ Example def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.padding = "16dp" - self.elevation = 3.5 + self.elevation = 1 self.shadow_radius = 12 - self.shadow_softness = 4 self.height = dp(56) Clock.schedule_once(self.set_spacing) @@ -439,9 +446,13 @@ Example MDIconButton( icon="menu", ), - ExtendedButton( - text="Compose", - icon="pencil", + MDBoxLayout( + ExtendedButton( + text="Compose", + icon="pencil", + ), + adaptive_height=True, + padding=["12dp", 0, 0, 0], ), orientation="vertical", adaptive_height=True, @@ -540,6 +551,7 @@ from typing import Union from kivy.animation import Animation from kivy.clock import Clock +from kivy.core.window import Window from kivy.lang import Builder from kivy.logger import Logger from kivy.metrics import dp @@ -556,7 +568,6 @@ from kivy.properties import ( from kivy.uix.behaviors import ButtonBehavior from kivymd import uix_path -from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import ScaleBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import MDFloatingActionButton, MDIconButton @@ -591,7 +602,12 @@ class RippleWidget(MDWidget, ScaleBehavior): class MDNavigationRailFabButton(MDFloatingActionButton): - """Implements an optional floating action button (FAB).""" + """ + Implements an optional floating action button (FAB). + + For more information, see in the + :class:`~kivymd.uix.button.MDFloatingActionButton` class documentation. + """ icon = StringProperty("pencil") """ @@ -613,7 +629,12 @@ class MDNavigationRailFabButton(MDFloatingActionButton): class MDNavigationRailMenuButton(MDIconButton): - """Implements a menu button.""" + """ + Implements a menu button. + + For more information, see in the + :class:`~kivymd.uix.button.MDIconButton` classes documentation. + """ icon = StringProperty("menu") """ @@ -634,8 +655,15 @@ class MDNavigationRailMenuButton(MDIconButton): """ -class MDNavigationRailItem(ThemableBehavior, ButtonBehavior, MDBoxLayout): - """Implements a menu item with an icon and text.""" +class MDNavigationRailItem(ButtonBehavior, MDBoxLayout): + """ + Implements a menu item with an icon and text. + + For more information, see in the + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.boxlayout.MDBoxLayout` + classes documentation. + """ navigation_rail = ObjectProperty() """ @@ -814,6 +842,11 @@ class MDNavigationRailItem(ThemableBehavior, ButtonBehavior, MDBoxLayout): class MDNavigationRail(MDCard): """ + Navigation rail class. + + For more information, see in the + :class:`~kivymd.uix.card.MDCard` class documentation. + :Events: :attr:`on_item_press` Called on the `on_press` event of menu item - @@ -941,7 +974,8 @@ class MDNavigationRail(MDCard): text_color_item_normal = ColorProperty(None) """ - The text color of the normal menu item (:class:`~MDNavigationRailItem`). + The text color in (r, g, b, a) or string format of the normal menu item + (:class:`~MDNavigationRailItem`). .. code-block:: kv @@ -960,7 +994,8 @@ class MDNavigationRail(MDCard): text_color_item_active = ColorProperty(None) """ - The text color of the active menu item (:class:`~MDNavigationRailItem`). + The text color in (r, g, b, a) or string format of the active menu item + (:class:`~MDNavigationRailItem`). .. code-block:: kv @@ -979,7 +1014,8 @@ class MDNavigationRail(MDCard): icon_color_item_normal = ColorProperty(None) """ - The icon color of the normal menu item (:class:`~MDNavigationRailItem`). + The icon color in (r, g, b, a) or string format of the normal menu item + (:class:`~MDNavigationRailItem`). .. code-block:: kv @@ -998,7 +1034,8 @@ class MDNavigationRail(MDCard): icon_color_item_active = ColorProperty(None) """ - The icon color of the active menu item (:class:`~MDNavigationRailItem`). + The icon color in (r, g, b, a) or string format of the active menu item + (:class:`~MDNavigationRailItem`). .. code-block:: kv @@ -1110,6 +1147,9 @@ class MDNavigationRail(MDCard): self.register_event_type("on_item_press") self.register_event_type("on_item_release") + def on_size(self, *args): + Clock.schedule_once(self.set_pos_menu_fab_buttons) + def on_item_press(self, *args) -> None: """ Called on the `on_press` event of menu item - @@ -1188,7 +1228,7 @@ class MDNavigationRail(MDCard): items[index].dispatch("on_press") items[index].dispatch("on_release") - def set_pos_menu_fab_buttons(self, interval: Union[int, float]) -> None: + def set_pos_menu_fab_buttons(self, *args) -> None: """ Sets the position of the :class:`~MDNavigationRailFabButton` and :class:`~MDNavigationRailMenuButton` buttons on the panel. diff --git a/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py b/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py index 37a0e52..4372702 100644 --- a/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py +++ b/sbapp/kivymd/uix/pickers/colorpicker/colorpicker.py @@ -100,7 +100,6 @@ from PIL import ImageDraw from kivymd import uix_path from kivymd.color_definitions import colors as _colors from kivymd.color_definitions import text_colors -from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import RectangularRippleBehavior from kivymd.uix.behaviors.toggle_behavior import MDToggleButton from kivymd.uix.boxlayout import MDBoxLayout @@ -123,9 +122,11 @@ class TypeColorButton(MDRaisedButton, MDToggleButton): 'RGBA', 'HEX', 'RGB'. """ - theme_text_color = "Custom" - text_color = (0, 0, 0, 1) - elevation = 0 + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.theme_text_color = "Custom" + self.text_color = (0, 0, 0, 1) + self.elevation = 0 class SelectAlphaChannelWidget(MDBoxLayout): @@ -186,7 +187,7 @@ class SliderTab(MDBoxLayout): """Basic event handler for changing the slider value.""" -class GradientTab(ThemableBehavior, MDBoxLayout): +class GradientTab(MDBoxLayout): """ The class implements a tab with a gradient, a color selection scale and a scale for setting the transparency value of the selected color. @@ -398,8 +399,8 @@ class MDColorPicker(BaseDialog): default_color = ColorProperty(None, allownone=True) """ - Default color value The set color value will be used when you open the - dialog. + Default color value in (r, g, b, a) or string format. The set color value + will be used when you open the dialog. :attr:`default_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -416,7 +417,8 @@ class MDColorPicker(BaseDialog): background_down_button_selected_type_color = ColorProperty([1, 1, 1, 0.3]) """ - Button background for choosing a color type ('RGBA', 'HEX', 'HSL', 'RGB'). + Button background for choosing a color type ('RGBA', 'HEX', 'HSL', 'RGB') + in (r, g, b, a) or string format. .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/color-picker-background-down-button-selected-type-color.png :align: center diff --git a/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv b/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv index 13835de..7fe2866 100644 --- a/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv +++ b/sbapp/kivymd/uix/pickers/datepicker/datepicker.kv @@ -31,9 +31,7 @@ canvas: Color: - rgb: - app.theme_cls.primary_color \ - if not root.primary_color else root.primary_color + rgb: root.primary_color or app.theme_cls.primary_color RoundedRectangle: size: (dp(328), dp(120)) \ @@ -48,9 +46,7 @@ if root.theme_cls.device_orientation == "portrait" \ else (root.radius[0], dp(0), dp(0), root.radius[3]) Color: - rgba: - app.theme_cls.bg_normal \ - if not root.accent_color else root.accent_color + rgba: root.accent_color or app.theme_cls.bg_normal RoundedRectangle: size: (dp(328), dp(512) - dp(120) - root._shift_dialog_height) \ @@ -79,9 +75,7 @@ (dp(24), root.height - self.height - dp(18)) \ if root.theme_cls.device_orientation == "portrait" \ else (dp(24), root.height - self.height - dp(24)) - text_color: - root.specific_text_color \ - if not root.text_toolbar_color else root.text_toolbar_color + text_color: root.text_toolbar_color or root.specific_text_color MDLabel: id: label_full_date @@ -100,28 +94,13 @@ dp(24) if not root._input_date_dialog_open else dp(168) + dp(24), \ root.height - self.height - dp(96) \ ) - text: - root.set_text_full_date(root.sel_year, root.sel_month, root.sel_day, \ - root.theme_cls.device_orientation) + text: root._date_label_text text_color: - ( \ - root.specific_text_color \ - if not root.text_toolbar_color else root.text_toolbar_color \ - ) \ - if root.theme_cls.device_orientation == "portrait" \ - else \ - ( \ - ( \ - self.theme_cls.primary_color \ - if not root.primary_color else root.primary_color \ - ) \ - if root._input_date_dialog_open \ - else \ - ( \ - root.specific_text_color \ - if not root.text_toolbar_color else root.text_toolbar_color \ - ) \ - ) + root.text_toolbar_color or root.specific_text_color \ + if root.theme_cls.device_orientation == "portrait" else \ + root.primary_color or self.theme_cls.primary_color \ + if root._input_date_dialog_open else \ + root.text_toolbar_color or root.specific_text_color RecycleView: id: _year_layout @@ -164,9 +143,7 @@ (root.height - dp(120) + dp(12)) \ if root.theme_cls.device_orientation == "portrait" \ else dp(12) - text_color: - root.specific_text_color \ - if not root.text_toolbar_color else root.text_toolbar_color + text_color: root.text_toolbar_color or root.specific_text_color MDLabel: id: label_month_selector @@ -180,9 +157,7 @@ (dp(24), root.height - dp(120) - self.height - dp(20)) \ if root.theme_cls.device_orientation == "portrait" \ else (dp(168) + dp(24), label_title.y) - text_color: - app.theme_cls.text_color \ - if not root.text_color else root.text_color + text_color: root.text_color or app.theme_cls.text_color DatePickerIconTooltipButton: id: triangle @@ -199,9 +174,7 @@ (label_month_selector.width + dp(14), root.height - dp(123) - self.height) \ if root.theme_cls.device_orientation == "portrait" \ else (dp(180) + label_month_selector.width, label_title.y - dp(14)) - text_color: - app.theme_cls.text_color \ - if not root.text_color else root.text_color + text_color: root.text_color or app.theme_cls.text_color md_bg_color_disabled: 0, 0, 0, 0 DatePickerIconTooltipButton: @@ -218,9 +191,7 @@ root.height - dp(120) - self.height / 2 - dp(30) \ if root.theme_cls.device_orientation == "portrait" \ else dp(272) - text_color: - app.theme_cls.text_color \ - if not root.text_color else root.text_color + text_color: root.text_color or app.theme_cls.text_color DatePickerIconTooltipButton: id: chevron_right @@ -236,9 +207,7 @@ root.height - dp(120) - self.height / 2 - dp(30) \ if root.theme_cls.device_orientation == "portrait" \ else dp(272) - text_color: - app.theme_cls.text_color \ - if not root.text_color else root.text_color + text_color: root.text_color or app.theme_cls.text_color # TODO: Replace the GridLayout with a RecycleView # if it improves performance. @@ -280,10 +249,7 @@ text: "OK" theme_text_color: "Custom" font_name: root.font_name - text_color: - root.theme_cls.primary_color \ - if not root.text_button_color else \ - root.text_button_color + text_color: root.text_button_color or root.theme_cls.primary_color on_release: root.on_ok_button_pressed() MDFlatButton: @@ -293,10 +259,7 @@ theme_text_color: "Custom" pos: root.width - self.width - ok_button.width - dp(10), dp(10) font_name: root.font_name - text_color: - root.theme_cls.primary_color \ - if not root.text_button_color else \ - root.text_button_color + text_color: root.text_button_color or root.theme_cls.primary_color @@ -312,14 +275,8 @@ canvas.before: Color: rgba: - (\ - self.owner.selector_color[:-1] + [.3] \ - if self.owner.selector_color \ - else self.theme_cls.primary_color[:-1] + [.3] \ - ) \ - if not self.disabled \ - and self.text \ - and self.check_date(self.owner.year, self.owner.month, int(self.text)) \ + (self.owner.selector_color or self.theme_cls.primary_color)[:-1] + [.3] \ + if self.is_in_range \ else (0, 0, 0, 0) RoundedRectangle: size: @@ -327,55 +284,24 @@ if root.theme_cls.device_orientation == "portrait" \ else \ (dp(32), dp(28)) \ - if self.index in [6, 13, 20, 27, 34] or self.owner._date_range \ - and self.text and self.owner._date_range[-1] == date( \ - self.current_year, \ - self.current_month, \ - int(self.text) \ - ) \ - or self.text and int(self.text) == \ - calendar.monthrange(self.current_year, self.current_month)[1] \ + if self.is_range_end or self.is_week_end or self.is_month_end \ else (dp(46), dp(28)) pos: (self.x - dp(1.5), self.y + dp(5)) \ if root.theme_cls.device_orientation == "portrait" else \ (self.x, self.y + 1) radius: - [0, 0, 0, 0] if not self.owner._date_range else \ - ( \ - [self.width / 2, 0, 0, self.width / 2] \ - if self.text and self.owner._date_range[0] == date( \ - self.current_year, \ - self.current_month, \ - int(self.text) \ - ) \ - or (self.index in [0, 7, 14, 21, 28] and root.is_selected) \ - else \ - ( \ - [0, 0, 0, 0] if self.text \ - and self.owner._date_range[-1] != date( \ - self.current_year, \ - self.current_month, \ - int(self.text) \ - ) \ - and self.index not in [6, 13, 20, 27, 30] \ - else [0, self.width / 2, self.width, 0] \ - if root.is_selected or self.text \ - and self.owner._date_range[-1] == date( \ - self.current_year, \ - self.current_month, \ - int(self.text) \ - ) \ - else [0, 0, 0, 0]) \ - ) + [ + self.width / 2 if self.is_range_start else 0, + self.width / 2 if self.is_range_end else 0, + self.width / 2 if self.is_range_end else 0, + self.width / 2 if self.is_range_start else 0, + ] # Selection circle. Color: rgba: - ( \ - self.theme_cls.primary_color if not root.owner.selector_color \ - else root.owner.selector_color \ - ) \ + root.owner.selector_color or self.theme_cls.primary_color \ if root.is_selected and not self.disabled \ else (0, 0, 0, 0) Ellipse: @@ -393,25 +319,11 @@ font_name: root.owner.font_name theme_text_color: "Custom" text_color: - ( \ - root.theme_cls.primary_color \ - if not root.owner.text_current_color \ - else root.owner.text_current_color \ - ) \ - if root.is_today and not root.is_selected \ - else ( \ - ( \ - root.theme_cls.text_color \ - if not root.is_selected or root.owner.mode == "range" \ - else (1, 1, 1, 1) \ - ) \ - if not root.owner.text_color \ - else \ - ( \ - root.owner.text_color \ - if not root.is_selected else (1, 1, 1, 1)) \ - ) - + root.owner.accent_color or root.theme_cls.bg_normal \ + if root.is_selected else \ + root.owner.text_current_color or root.theme_cls.primary_color \ + if root.is_today else \ + root.owner.text_color or root.theme_cls.text_color font_style: "Caption" @@ -425,9 +337,7 @@ size: (dp(40), dp(40)) if root.theme_cls.device_orientation == "portrait" \ else (dp(32), dp(32)) - text_color: - app.theme_cls.disabled_hint_text_color \ - if not root.owner.text_weekday_color else root.owner.text_weekday_color + text_color: root.owner.text_weekday_color or app.theme_cls.disabled_hint_text_color @@ -436,13 +346,21 @@ valign: "middle" halign: "center" text: root.text + theme_text_color: "Custom" + text_color: + (0, 0, 0, 0) \ + if self.owner is None else \ + self.owner.accent_color or self.owner.theme_cls.bg_normal \ + if self.selected else \ + self.owner.text_color or self.owner.theme_cls.text_color on_text: root.font_name = root.owner.font_name canvas.before: Color: rgba: - root.selected_color if root.selected_color \ - else self.theme_cls.primary_color + self.owner.selector_color or self.theme_cls.primary_color \ + if self.selected else \ + (0, 0, 0, 0) RoundedRectangle: pos: self.x + dp(12), self.y size: self.width - dp(24), self.height @@ -453,6 +371,7 @@ adaptive_height: True size_hint_x: None spacing: dp(8) + opacity: 0 width: self.owner.width - dp(48) \ if root.owner.theme_cls.device_orientation == "portrait" \ @@ -468,10 +387,6 @@ mode: "fill" - opacity: 0 hint_text: "dd/mm/yyyy" input_filter: root.input_filter - fill_color: - (0, 0, 0, .15) \ - if not self.owner.input_field_background_color \ - else root.owner.input_field_background_color + fill_color: root.owner.input_field_background_color or (0, 0, 0, .15) diff --git a/sbapp/kivymd/uix/pickers/datepicker/datepicker.py b/sbapp/kivymd/uix/pickers/datepicker/datepicker.py index 3188408..3b24f9e 100644 --- a/sbapp/kivymd/uix/pickers/datepicker/datepicker.py +++ b/sbapp/kivymd/uix/pickers/datepicker/datepicker.py @@ -203,7 +203,6 @@ from datetime import date from itertools import zip_longest from typing import Union -from kivy import Logger from kivy.animation import Animation from kivy.lang import Builder from kivy.metrics import dp @@ -253,6 +252,12 @@ class BaseDialogPicker( Base class for :class:`~kivymd.uix.picker.MDDatePicker` and :class:`~kivymd.uix.picker.MDTimePicker` classes. + For more information, see in the + :class:`~kivymd.uix.dialog.BaseDialog` and + :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and + :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior` + classes documentation. + :Events: `on_save` Events called when the "OK" dialog box button is clicked. @@ -644,11 +649,30 @@ class DatePickerTypeDateError(Exception): class DatePickerInputField(MDTextField): - """Implements date input in dd/mm/yyyy format.""" + """ + Implements date input in dd/mm/yyyy format. + + For more information, see in the + :class:`~kivymd.uix.textfield.MDTextField` class documentation. + """ helper_text_mode = StringProperty("on_error") owner = ObjectProperty() # MDDatePicker object + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.bind(text=self._on_text_check_errors) + + def _on_text_check_errors(self, widget, text): + if text == "": + self.error = False + return + try: + datetime.datetime.strptime(text, "%d/%m/%Y") + self.error = False + except ValueError: + self.error = True + def set_error(self): """Sets a text field to an error state.""" @@ -699,54 +723,14 @@ class DatePickerDaySelectableItem( owner = ObjectProperty() is_today = BooleanProperty(False) is_selected = BooleanProperty(False) - current_month = NumericProperty() - current_year = NumericProperty() - index = NumericProperty(0) - - def check_date(self, year: int, month: int, day: int): - try: - return date(year, month, day) in self.owner._date_range - except ValueError as error: - if str(error) == "day is out of range for month": - return False + is_in_range = BooleanProperty(False) + is_range_start = BooleanProperty(False) + is_range_end = BooleanProperty(False) + is_month_end = BooleanProperty(False) + is_week_end = BooleanProperty(False) def on_release(self): - if ( - self.owner.mode == "range" - and self.owner._end_range_date - and self.owner._start_range_date - ): - return - if ( - not self.owner._input_date_dialog_open - and not self.owner._select_year_dialog_open - ): - if self.owner.mode == "range" and not self.owner._start_range_date: - self.owner._start_range_date = date( - self.current_year, self.current_month, int(self.text) - ) - self.owner.min_date = self.owner._start_range_date - elif ( - self.owner.mode == "range" - and not self.owner._end_range_date - and self.owner._start_range_date - ): - self.owner._end_range_date = date( - self.current_year, self.current_month, int(self.text) - ) - if self.owner._end_range_date <= self.owner.min_date: - toast(self.owner.date_range_text_error) - Logger.error( - "`Data Picker: max_date` value cannot be less than " - "or equal to 'min_date' value." - ) - self.owner._start_range_date = 0 - self.owner._end_range_date = 0 - return - self.owner.max_date = self.owner._end_range_date - self.owner.update_calendar_for_date_range() - - self.owner.set_selected_widget(self) + self.owner.set_selected_widget(self) def on_touch_down(self, touch): # If year_layout is active don't dispatch on_touch_down events, @@ -760,7 +744,7 @@ class DatePickerYearSelectableItem(RecycleDataViewBehavior, MDLabel): """Implements an item for a pick list of the year.""" index = None - selected_color = ColorProperty([0, 0, 0, 0]) + selected = BooleanProperty(False) owner = ObjectProperty() def refresh_view_attrs(self, rv, index, data): @@ -772,32 +756,10 @@ class DatePickerYearSelectableItem(RecycleDataViewBehavior, MDLabel): return True if self.collide_point(*touch.pos): self.owner.year = int(self.text) - # self.owner.sel_year = self.owner.year - self.owner.ids.label_full_date.text = self.owner.set_text_full_date( - self.owner.sel_year, - self.owner.sel_month, - self.owner.sel_day, - self.owner.theme_cls.device_orientation, - ) return self.parent.select_with_touch(self.index, touch) def apply_selection(self, table_data, index, is_selected): - if is_selected: - self.selected_color = ( - self.owner.selector_color - if self.owner.selector_color - else self.theme_cls.primary_color - ) - self.text_color = (1, 1, 1, 1) - else: - if int(self.text) == self.owner.sel_year: - self.text_color = ( - self.theme_cls.primary_color - if not self.owner.text_current_color - else self.owner.text_current_color - ) - self.selected_color = [0, 0, 0, 0] - self.text_color = (0, 0, 0, 1) + self.selected = is_selected # TODO: Add the feature to embed the `MDDatePicker` class in other layouts @@ -889,7 +851,7 @@ class MDDatePicker(BaseDialogPicker): and defaults to `picker`. """ - min_date = ObjectProperty() + min_date = ObjectProperty(allownone=True) """ The minimum value of the date range for the `'mode`' parameter. Must be an object . @@ -900,7 +862,7 @@ class MDDatePicker(BaseDialogPicker): and defaults to `None`. """ - max_date = ObjectProperty() + max_date = ObjectProperty(allownone=True) """ The minimum value of the date range for the `'mode`' parameter. Must be an object . @@ -955,17 +917,13 @@ class MDDatePicker(BaseDialogPicker): _calendar_layout = ObjectProperty() _calendar_list = None - _enter_data_field = None - _enter_data_field_two = None - _enter_data_field_container = None - _date_range = [] + _fields_container = None _scale_calendar_layout = NumericProperty(1) _scale_year_layout = NumericProperty(0) _shift_dialog_height = NumericProperty(0) _input_date_dialog_open = BooleanProperty(False) _select_year_dialog_open = False - _start_range_date = 0 - _end_range_date = 0 + _date_label_text = StringProperty() def __init__( self, @@ -996,7 +954,6 @@ class MDDatePicker(BaseDialogPicker): "'max_date' must be of class " ) self.compare_date_range() - self._date_range = self.get_date_range() self.generate_list_widgets_days() self.update_calendar(self.sel_year, self.sel_month) @@ -1006,6 +963,8 @@ class MDDatePicker(BaseDialogPicker): ) -> None: """Called when the device's screen orientation changes.""" + # Separators of the label text depend on the orientation. + self._update_date_label_text() if self._input_date_dialog_open: if orientation_value == "portrait": self._shift_dialog_height = dp(250) @@ -1017,21 +976,12 @@ class MDDatePicker(BaseDialogPicker): Called when the 'OK' button is pressed to confirm the date entered. """ - if self._enter_data_field and not self.is_date_valaid( - self._enter_data_field.text - ): - self._enter_data_field.set_error() + if self._input_date_dialog_open and not self._try_apply_input(): return - if self._enter_data_field_two and not self.is_date_valaid( - self._enter_data_field_two.text - ): - self._enter_data_field_two.set_error() - return - self.dispatch( "on_save", date(self.sel_year, self.sel_month, self.sel_day), - self._date_range, + self.get_date_range(), ) def is_date_valaid(self, date: str) -> bool: @@ -1085,56 +1035,25 @@ class MDDatePicker(BaseDialogPicker): self.ids._year_layout.children[0].clear_selection() def transformation_to_dialog_input_date(self) -> None: - def set_date_to_input_field(): - if not self._enter_data_field_two: - # Date of current day. - self._enter_data_field.text = ( - f"{'' if self.sel_day >= 10 else '0'}" - f"{self.sel_day}/" - f"{'' if self.sel_month >= 10 else '0'}" - f"{self.sel_month}/{self.sel_year}" - ) - else: - # Range start date. - self._enter_data_field.text = ( - f"{'' if self.min_date.day >= 10 else '0'}" - f"{self.min_date.day}/" - f"{'' if self.min_date.month >= 10 else '0'}" - f"{self.min_date.month}/{self.min_date.year}" - ) - - def set_date_to_input_field_two() -> None: - # Range end date. - self._enter_data_field_two.text = ( - f"{'' if self.max_date.day >= 10 else '0'}" - f"{self.max_date.day}/" - f"{'' if self.max_date.month >= 10 else '0'}" - f"{self.max_date.month}/{self.max_date.year}" - ) - self.ids.triangle.disabled = True if self._select_year_dialog_open: self.transformation_from_dialog_select_year() self._input_date_dialog_open = True - - self._enter_data_field_container = DatePickerInputFieldContainer( - owner=self - ) - self._enter_data_field = self.get_field() - if self.min_date and self.max_date: - self._enter_data_field_two = self.get_field() - set_date_to_input_field_two() - set_date_to_input_field() - self._enter_data_field_container.add_widget(self._enter_data_field) - if self._enter_data_field_two: - self._enter_data_field_container.add_widget( - self._enter_data_field_two - ) - - self.ids.container.add_widget(self._enter_data_field_container) self.ids.edit_icon.icon = "calendar" self.ids.label_title.text = self.title_input + self._fields_container = DatePickerInputFieldContainer(owner=self) + if self.mode == "picker": + selected_date = date(self.sel_year, self.sel_month, self.sel_day) + selected_dates = [selected_date] + else: + selected_dates = [self.min_date, self.max_date] + for selected_date in selected_dates: + field = self.get_field(selected_date) + field.bind(text=self._on_date_field_text_changes) + self._fields_container.add_widget(field) + self.ids.container.add_widget(self._fields_container) + Animation( _shift_dialog_height=dp(250) if self.theme_cls.device_orientation == "portrait" @@ -1152,28 +1071,22 @@ class MDDatePicker(BaseDialogPicker): ).start(self.ids.chevron_right) Animation(opacity=0, d=0.15).start(self.ids.label_month_selector) Animation(opacity=0, d=0.15).start(self.ids.triangle) - Animation(opacity=1, d=0.15).start(self._enter_data_field) - if self._enter_data_field_two: - Animation(opacity=1, d=0.15).start(self._enter_data_field_two) - self.ids.label_full_date.text = self.set_text_full_date( - self.sel_year, - self.sel_month, - self.sel_day, - self.theme_cls.device_orientation, - ) + Animation(opacity=1, d=0.15).start(self._fields_container) + # The label text separator in landscape orientation depends on the + # open dialog. + self._update_date_label_text() def transformation_from_dialog_input_date( self, interval: Union[int, float] ) -> None: + if not self._try_apply_input(): + return self._input_date_dialog_open = False - self.ids.label_full_date.text = self.set_text_full_date( - self.sel_year, - self.sel_month, - self.sel_day, - self.theme_cls.device_orientation, - ) self.ids.triangle.disabled = False - self.ids.container.remove_widget(self._enter_data_field_container) + self.ids.edit_icon.icon = "pencil" + self.ids.label_title.text = self.title + self.ids.container.remove_widget(self._fields_container) + self._fields_container = None Animation( _shift_dialog_height=dp(0), _scale_calendar_layout=1, d=0.15 ).start(self) @@ -1187,41 +1100,67 @@ class MDDatePicker(BaseDialogPicker): ).start(self.ids.chevron_right) Animation(opacity=1, d=0.15).start(self.ids.label_month_selector) Animation(opacity=1, d=0.15).start(self.ids.triangle) - Animation(opacity=0, d=0.15).start(self._enter_data_field) - self.ids.edit_icon.icon = "pencil" - self.ids.label_title.text = self.title + # The label text separator in landscape orientation depends on the + # open dialog. + self._update_date_label_text() - if not self.min_date and not self.max_date: - list_date = self._enter_data_field.get_list_date() - if len(list_date) == 3 and len(list_date[2]) == 4: - self.sel_day = int(list_date[0]) - self.sel_month = int(list_date[1]) - self.sel_year = int(list_date[2]) - self.update_calendar(self.sel_year, self.sel_month) - elif self.min_date and self.max_date: - list_min_date = self._enter_data_field.get_list_date() - list_max_date = self._enter_data_field_two.get_list_date() + def _get_dates_from_fields(self): + """ + Return a list of dates entered by the user in the input fields. - if len(list_min_date) == 3 and len(list_min_date[2]) == 4: - self.min_date = date( - int(list_min_date[2]), - int(list_min_date[1]), - int(list_min_date[0]), - ) - if len(list_max_date) == 3 and len(list_max_date[2]) == 4: - self.max_date = date( - int(list_max_date[2]), - int(list_max_date[1]), - int(list_max_date[0]), - ) + If there is an error in the field or the field is empty, None will be + in its place in the list. The length of the list will be 0 if the input + dialog is closed, otherwise 1 in picker mode or 2 in range mode. + """ - self.update_calendar_for_date_range() - self.ids.label_full_date.text = self.set_text_full_date( - int(list_max_date[2]), - int(list_max_date[1]), - int(list_max_date[0]), - self.theme_cls.device_orientation, - ) + if not self._fields_container: + return [] + + dates = [] + # Widgets are arranged in the reverse order of their addition. + for field in reversed(self._fields_container.children): + try: + date = datetime.datetime.strptime(field.text, "%d/%m/%Y").date() + except ValueError: + date = None + dates.append(date) + + return dates + + def _try_apply_input(self) -> bool: + """ + Apply the dates entered by the user, update the calendar and return + True. If there are errors in the fields, do nothing and return False. + """ + + dates = self._get_dates_from_fields() + if not dates: + return True + + # Widgets are arranged in the reverse order of their addition. + fields = reversed(self._fields_container.children) + if any(d is None and f.text for f, d in zip(fields, dates)): + return False + + if self.mode == "picker": + selected_date = date(self.sel_year, self.sel_month, self.sel_day) + selected_date = dates[0] or selected_date + self.sel_year = selected_date.year + self.sel_month = selected_date.month + self.sel_day = selected_date.day + self.update_calendar(self.sel_year, self.sel_month) + elif self.mode == "range": + date1, date2 = dates[0] or self.min_date, dates[1] or self.max_date + ends = list(filter(bool, [date1, date2])) + if ends: + self.min_date = min(ends) + self.max_date = max(ends) + self.update_calendar(self.year, self.month) + + return True + + def _on_date_field_text_changes(self, *args): + self._update_date_label_text() def compare_date_range(self) -> None: # TODO: Add behavior if the minimum date range exceeds the maximum @@ -1233,8 +1172,7 @@ class MDDatePicker(BaseDialogPicker): ) def update_calendar_for_date_range(self) -> None: - # self.compare_date_range() - self._date_range = self.get_date_range() + # This method is no longer used, use update_calendar instead. self.update_calendar(self.year, self.month) def update_text_full_date(self, list_date) -> None: @@ -1243,27 +1181,13 @@ class MDDatePicker(BaseDialogPicker): in an open date input dialog. """ - if len(list_date) == 1 and len(list_date[0]) == 2: - self.ids.label_full_date.text = self.set_text_full_date( - self.sel_year, - self.sel_month, - list_date[0], - self.theme_cls.device_orientation, - ) - if len(list_date) == 2 and len(list_date[1]) == 2: - self.ids.label_full_date.text = self.set_text_full_date( - self.sel_year, - int(list_date[1]), - int(list_date[0]), - self.theme_cls.device_orientation, - ) - if len(list_date) == 3 and len(list_date[2]) == 4: - self.ids.label_full_date.text = self.set_text_full_date( - int(list_date[2]), - int(list_date[1]), - int(list_date[0]), - self.theme_cls.device_orientation, - ) + # This method no longer used, use update_calendar instead. + year = int(list_date[2]) if len(list_date) > 2 else self.sel_year + month = int(list_date[1]) if len(list_date) > 1 else self.sel_month + day = int(list_date[0]) if len(list_date) > 0 else self.sel_day + day = min(day, calendar.monthrange(year, month)[1]) + self.sel_year, self.sel_month, self.sel_day = year, month, day + self.update_calendar(year, month) def update_calendar(self, year, month) -> None: self.year, self.month = year, month @@ -1271,7 +1195,10 @@ class MDDatePicker(BaseDialogPicker): selected_date = date(self.sel_year, self.sel_month, self.sel_day) selected_dates = {selected_date} else: - selected_dates = {self._start_range_date, self._end_range_date} + selected_dates = {self.min_date, self.max_date} + # The label text depends on the selected date or date range. + self._update_date_label_text() + month_end = date(year, month, calendar.monthrange(year, month)[1]) dates = self.calendar.itermonthdates(year, month) for widget, widget_date in zip_longest(self._calendar_list, dates): # Only widgets whose dates are in the displayed month are visible. @@ -1281,21 +1208,31 @@ class MDDatePicker(BaseDialogPicker): and widget_date.year == year ) widget.text = str(widget_date.day) if visible else "" - widget.current_year = year - widget.current_month = month widget.is_today = visible and widget_date == self.today widget.is_selected = visible and widget_date in selected_dates # I don't understand why, but this line is important. Without this # line, some widgets that we are trying to disable remain enabled. widget.disabled = False - widget.disabled = ( - not visible - or self.mode == "range" - and self._date_range - and widget_date not in self._date_range + widget.disabled = not visible + widget.is_in_range = ( + visible + and self.min_date is not None + and self.max_date is not None + and self.min_date <= widget_date <= self.max_date ) + widget.is_range_start = ( + visible + and self.min_date is not None + and widget_date == self.min_date + ) + widget.is_range_end = ( + visible + and self.max_date is not None + and widget_date == self.max_date + ) + widget.is_month_end = widget_date == month_end - def get_field(self) -> MDTextField: + def get_field(self, date=None) -> MDTextField: """Creates and returns a text field object used to enter dates.""" if issubclass(self.input_field_cls, MDTextField): @@ -1322,6 +1259,7 @@ class MDDatePicker(BaseDialogPicker): field = self.input_field_cls( owner=self, + text=date.strftime("%d/%m/%Y") if date else "", helper_text=self.helper_text, fill_color_normal=fill_color_normal, fill_color_focus=fill_color_focus, @@ -1340,6 +1278,8 @@ class MDDatePicker(BaseDialogPicker): ) def get_date_range(self) -> list: + if not self.min_date or not self.max_date: + return [] date_range = [ self.min_date + datetime.timedelta(days=x) for x in range((self.max_date - self.min_date).days + 1) @@ -1353,118 +1293,73 @@ class MDDatePicker(BaseDialogPicker): a date range. """ - if 12 < int(month) < 0: - raise ValueError( - "set_text_full_date:\n\t" f"Month [{month}] out of range." - ) - if int(day) > calendar.monthrange(int(year), (month))[1]: - return "" - date = datetime.date(int(year), int(month), int(day)) - separator = ( - "\n" - if (orientation == "landscape" and not self._input_date_dialog_open) - else " " + # In portrait orientation, the label is stretched in width, so we + # should not insert line breaks. When the input dialog is open, the + # label moves to the right and also stretches in width. + horizontal = orientation == "portrait" or self._input_date_dialog_open + + def date_repr(date): + return date.strftime("%b").capitalize() + " " + str(date.day) + + input_dates = self._get_dates_from_fields() + if self.mode == "picker": + selected_date = date(self.sel_year, self.sel_month, self.sel_day) + if input_dates: + selected_date = input_dates[0] or selected_date + weekday_repr = selected_date.strftime("%a").capitalize() + separator = ", " if horizontal else ",\n" + return weekday_repr + separator + date_repr(selected_date) + elif self.mode == "range": + start, end = self.min_date, self.max_date + if input_dates: + start, end = input_dates[0] or start, input_dates[1] or end + ends = [end for end in (start, end) if end] + if len(ends) == 0: + start_repr, end_repr = "Start", "End" + else: + start, end = min(ends), max(ends) + start_repr, end_repr = date_repr(start), date_repr(end) + separator = " — " if horizontal else ",\n" + return start_repr + separator + end_repr + + def _update_date_label_text(self): + self._date_label_text = self.set_text_full_date( + self.sel_year, + self.sel_month, + self.sel_day, + self.theme_cls.device_orientation, ) - if self.mode == "picker": - if not self.min_date and not self.max_date: - return ( - date.strftime("%a,").capitalize() - + separator - + date.strftime("%b ").capitalize() - + str(day).lstrip("0") - ) - else: - return ( - self.min_date.strftime("%b ").capitalize() - + str(self.min_date.day).lstrip("0") - + ( - " - " - if orientation == "portrait" - else ( - ",\n" if not self._input_date_dialog_open else ", " - ) - ) - + self.max_date.strftime("%b ").capitalize() - + str(self.max_date.day).lstrip("0") - ) - elif self.mode == "range": - if self._start_range_date and self._end_range_date: - if ( - orientation == "landscape" - and "-" in self.ids.label_full_date.text - ): - return ( - self.ids.label_full_date.text.split("-")[0].strip() - + (",\n" if not self._input_date_dialog_open else " - ") - + date.strftime("%b ").capitalize() - + str(day).lstrip("0") - ) - else: - if ( - orientation == "landscape" - and "," in self.ids.label_full_date.text - ): - return ( - self.ids.label_full_date.text.split(",")[0].strip() - + ( - ",\n" - if not self._input_date_dialog_open - else "-" - ) - + date.strftime("%b ").capitalize() - + str(day).lstrip("0") - ) - if ( - orientation == "portrait" - and "," in self.ids.label_full_date.text - ): - return ( - self.ids.label_full_date.text.split(",")[0].strip() - + "-" - + date.strftime("%b ").capitalize() - + str(day).lstrip("0") - ) - if ( - orientation == "portrait" - and "-" in self.ids.label_full_date.text - ): - return ( - self.ids.label_full_date.text.split("-")[0].strip() - + " - " - + date.strftime("%b ").capitalize() - + str(day).lstrip("0") - ) - elif self._start_range_date and not self._end_range_date: - return ( - ( - date.strftime("%b ").capitalize() - + str(day).lstrip("0") - + " - End" - ) - if orientation != "landscape" - else ( - date.strftime("%b ").capitalize() - + str(day).lstrip("0") - + "{}End".format( - ",\n" if not self._input_date_dialog_open else " - " - ) - ) - ) - elif not self._start_range_date and not self._end_range_date: - return ( - "Start - End" - if orientation != "landscape" - else "Start{}End".format( - ",\n" if not self._input_date_dialog_open else " - " - ) - ) - def set_selected_widget(self, widget) -> None: - self.sel_year = self.year - self.sel_month = self.month - self.sel_day = int(widget.text) - self.update_calendar(self.sel_year, self.sel_month) + if self._select_year_dialog_open or self._input_date_dialog_open: + return + try: + widget_date = date(self.year, self.month, int(widget.text)) + except ValueError: + return + if self.mode == "picker": + self.sel_year = widget_date.year + self.sel_month = widget_date.month + self.sel_day = widget_date.day + self.update_calendar(self.sel_year, self.sel_month) + elif self.mode == "range": + ends = [end for end in (self.min_date, self.max_date) if end] + if widget_date in ends: + ends = [end for end in ends if end != widget_date] + elif len(ends) < 2: + ends.append(widget_date) + else: + start, end = min(ends), max(ends) + if abs(widget_date - start).days < abs(widget_date - end).days: + start = widget_date + else: + end = widget_date + ends = [start, end] + if len(ends) == 0: + self.min_date, self.max_date = None, None + else: + self.min_date, self.max_date = min(ends), max(ends) + self.update_calendar(self.year, self.month) def set_month_day(self, day) -> None: # This method is no longer used. The code bellow repeats the behavior @@ -1525,12 +1420,10 @@ class MDDatePicker(BaseDialogPicker): ) weekday_label.font_name = self.font_name self._calendar_layout.add_widget(weekday_label) - for i, j in enumerate(range(6 * 7)): # 6 weeks, 7 days a week + for i in range(6 * 7): # 6 weeks, 7 days a week day_selectable_item = DatePickerDaySelectableItem( - index=i, + is_week_end=i % 7 == 6, owner=self, - current_month=int(self.month), - current_year=int(self.year), ) calendar_list.append(day_selectable_item) self._calendar_layout.add_widget(day_selectable_item) @@ -1541,9 +1434,11 @@ class MDDatePicker(BaseDialogPicker): Called when "chevron-left" and "chevron-right" buttons are pressed. Switches the calendar to the previous/next month. """ + month_delta = 1 if operation == "next" else -1 year = self.year + (self.month - 1 + month_delta) // 12 month = (self.month - 1 + month_delta) % 12 + 1 + if year <= 0: year, month = 1, 1 self.update_calendar(year, month) diff --git a/sbapp/kivymd/uix/pickers/timepicker/timepicker.py b/sbapp/kivymd/uix/pickers/timepicker/timepicker.py index 2b646d8..e45a784 100644 --- a/sbapp/kivymd/uix/pickers/timepicker/timepicker.py +++ b/sbapp/kivymd/uix/pickers/timepicker/timepicker.py @@ -166,7 +166,6 @@ from kivy.uix.behaviors import ButtonBehavior from kivy.vector import Vector from kivymd import uix_path -from kivymd.theming import ThemableBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.circularlayout import MDCircularLayout from kivymd.uix.label import MDLabel @@ -185,7 +184,7 @@ class AmPmSelectorLabel(ButtonBehavior, MDLabel): pass -class AmPmSelector(ThemableBehavior, MDBoxLayout): +class AmPmSelector(MDBoxLayout): border_radius = NumericProperty() border_color = ColorProperty() bg_color = ColorProperty() diff --git a/sbapp/kivymd/uix/progressbar/progressbar.kv b/sbapp/kivymd/uix/progressbar/progressbar.kv index 00028cf..89aafb8 100644 --- a/sbapp/kivymd/uix/progressbar/progressbar.kv +++ b/sbapp/kivymd/uix/progressbar/progressbar.kv @@ -6,7 +6,8 @@ self.theme_cls.divider_color \ if not self.back_color else \ self.back_color - Rectangle: + RoundedRectangle: + radius: root.radius size: (self.width, self.height) \ if self.orientation == "horizontal" else \ @@ -18,7 +19,8 @@ Color: rgba: self.theme_cls.primary_color if not self.color else self.color - Rectangle: + RoundedRectangle: + radius: root.radius size: (self.width * self.value_normalized, self.height if self.height else dp(4)) \ if self.orientation == "horizontal" else \ diff --git a/sbapp/kivymd/uix/progressbar/progressbar.py b/sbapp/kivymd/uix/progressbar/progressbar.py index 9cedee8..b785f03 100644 --- a/sbapp/kivymd/uix/progressbar/progressbar.py +++ b/sbapp/kivymd/uix/progressbar/progressbar.py @@ -145,6 +145,7 @@ from kivy.properties import ( NumericProperty, OptionProperty, StringProperty, + VariableListProperty, ) from kivy.uix.progressbar import ProgressBar @@ -158,6 +159,25 @@ with open( class MDProgressBar(ThemableBehavior, ProgressBar): + """ + Progressbar class. + + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.progressbar.ProgressBar` + classes documentation. + """ + + radius = VariableListProperty([0], length=4) + """ + Progress line radius. + + .. versionadded:: 1.2.0 + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[0, 0, 0, 0]`. + """ + reversed = BooleanProperty(False) """ Reverse the direction the progressbar moves. @@ -179,7 +199,7 @@ class MDProgressBar(ThemableBehavior, ProgressBar): color = ColorProperty(None) """ - Progress bar color in ``rgba`` format. + Progress bar color in (r, g, b, a) or string format. :attr:`color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -187,7 +207,7 @@ class MDProgressBar(ThemableBehavior, ProgressBar): back_color = ColorProperty(None) """ - Progress bar back color in ``rgba`` format. + Progress bar back color in (r, g, b, a) or string format. .. versionadded:: 1.0.0 diff --git a/sbapp/kivymd/uix/recyclegridlayout.py b/sbapp/kivymd/uix/recyclegridlayout.py index d3ed891..e519af5 100644 --- a/sbapp/kivymd/uix/recyclegridlayout.py +++ b/sbapp/kivymd/uix/recyclegridlayout.py @@ -85,12 +85,13 @@ Equivalent from kivy.uix.recyclegridlayout import RecycleGridLayout +from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior class MDRecycleGridLayout( - DeclarativeBehavior, RecycleGridLayout, MDAdaptiveWidget + DeclarativeBehavior, ThemableBehavior, RecycleGridLayout, MDAdaptiveWidget ): """ Recycle grid layout layout class. For more information, see in the diff --git a/sbapp/kivymd/uix/recycleview.py b/sbapp/kivymd/uix/recycleview.py index 9792103..ddcf16c 100644 --- a/sbapp/kivymd/uix/recycleview.py +++ b/sbapp/kivymd/uix/recycleview.py @@ -34,10 +34,14 @@ __all__ = ("MDRecycleView",) from kivy.uix.recycleview import RecycleView +from kivymd.theming import ThemableBehavior +from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior -class MDRecycleView(DeclarativeBehavior, RecycleView): +class MDRecycleView( + DeclarativeBehavior, ThemableBehavior, RecycleView, MDAdaptiveWidget +): """ Recycle view class. For more information, see in the :class:`~kivy.uix.recycleview.RecycleView` class documentation. diff --git a/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv b/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv index 26dc488..e58b7f6 100644 --- a/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv +++ b/sbapp/kivymd/uix/refreshlayout/refreshlayout.kv @@ -15,7 +15,7 @@ canvas: Clear Color: - rgba: root.theme_cls.primary_dark + rgba: root.circle_color Ellipse: pos: self.pos size: self.size @@ -24,4 +24,4 @@ id: spinner size_hint: None, None size: dp(30), dp(30) - color: 1, 1, 1, 1 + color: root.spinner_color diff --git a/sbapp/kivymd/uix/refreshlayout/refreshlayout.py b/sbapp/kivymd/uix/refreshlayout/refreshlayout.py index 170b76e..c11672d 100755 --- a/sbapp/kivymd/uix/refreshlayout/refreshlayout.py +++ b/sbapp/kivymd/uix/refreshlayout/refreshlayout.py @@ -43,6 +43,8 @@ Example id: refresh_layout refresh_callback: app.refresh_callback root_layout: root + spinner_color: "brown" + circle_color: "white" MDGridLayout: id: box @@ -66,6 +68,8 @@ Example y = 15 def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" self.screen = Factory.Example() self.set_list() @@ -81,8 +85,10 @@ Example asynckivy.start(set_list()) def refresh_callback(self, *args): - '''A method that updates the state of your application - while the spinner remains on the screen.''' + ''' + A method that updates the state of your application + while the spinner remains on the screen. + ''' def refresh_callback(interval): self.screen.ids.box.clear_widgets() @@ -110,7 +116,12 @@ from kivy.core.window import Window from kivy.effects.dampedscroll import DampedScrollEffect from kivy.lang import Builder from kivy.metrics import dp -from kivy.properties import ColorProperty, NumericProperty, ObjectProperty +from kivy.properties import ( + ColorProperty, + NumericProperty, + ObjectProperty, + StringProperty, +) from kivy.uix.floatlayout import FloatLayout from kivymd import uix_path @@ -150,7 +161,16 @@ class _RefreshScrollEffect(DampedScrollEffect): return False -class MDScrollViewRefreshLayout(MDScrollView): +class MDScrollViewRefreshLayout(ThemableBehavior, MDScrollView): + """ + Refresh layout class. + + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.scrollview.MDScrollView` + class documentation. + """ + root_layout = ObjectProperty() """ The spinner will be attached to this layout. @@ -168,8 +188,70 @@ class MDScrollViewRefreshLayout(MDScrollView): and defaults to `None`. """ + spinner_color = ColorProperty([1, 1, 1, 1]) + """ + Color of the spinner in (r, g, b, a) or string format. + + .. versionadded:: 1.2.0 + + :attr:`spinner_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `[1, 1, 1, 1]`. + """ + + circle_color = ColorProperty(None) + """ + Color of the ellipse around the spinner in (r, g, b, a) or string format. + + .. versionadded:: 1.2.0 + + :attr:`circle_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + show_transition = StringProperty("out_elastic") + """ + Transition of the spinner's opening. + + .. versionadded:: 1.2.0 + + :attr:`show_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_elastic'`. + """ + + show_duration = NumericProperty(0.8) + """ + Duration of the spinner display. + + .. versionadded:: 1.2.0 + + :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.8`. + """ + + hide_transition = StringProperty("out_elastic") + """ + Transition of hiding the spinner. + + .. versionadded:: 1.2.0 + + :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'out_elastic'`. + """ + + hide_duration = NumericProperty(0.8) + """ + Duration of hiding the spinner. + + .. versionadded:: 1.2.0 + + :attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.8`. + """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + if not self.circle_color: + self.circle_color = self.theme_cls.primary_dark self.effect_cls = _RefreshScrollEffect self._work_spinner = False self._did_overscroll = False @@ -180,7 +262,15 @@ class MDScrollViewRefreshLayout(MDScrollView): if self.refresh_callback: self.refresh_callback() if not self.refresh_spinner: - self.refresh_spinner = RefreshSpinner(_refresh_layout=self) + self.refresh_spinner = RefreshSpinner( + _refresh_layout=self, + spinner_color=self.spinner_color, + circle_color=self.circle_color, + show_transition=self.show_transition, + show_duration=self.show_duration, + hide_transition=self.hide_transition, + hide_duration=self.hide_duration, + ) self.root_layout.add_widget(self.refresh_spinner) self.refresh_spinner.start_anim_spinner() self._work_spinner = True @@ -195,13 +285,18 @@ class MDScrollViewRefreshLayout(MDScrollView): class RefreshSpinner(ThemableBehavior, FloatLayout): + # Color of the spinner in (r, g, b, a) or string format. spinner_color = ColorProperty([1, 1, 1, 1]) - """ - Color of spinner. - - :attr:`spinner_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `[1, 1, 1, 1]`. - """ + # Color of the ellipse around the spinner in (r, g, b, a) or string format. + circle_color = ColorProperty() + # Transition of the spinner's opening. + show_transition = StringProperty() + # The duration of the spinner display. + show_duration = NumericProperty(0.8) + # Transition of hiding the spinner. + hide_transition = StringProperty() + # Duration of hiding the spinner. + hide_duration = NumericProperty(0.8) # kivymd.refreshlayout.MDScrollViewRefreshLayout object _refresh_layout = ObjectProperty() @@ -210,13 +305,15 @@ class RefreshSpinner(ThemableBehavior, FloatLayout): spinner = self.ids.body_spinner Animation( y=spinner.y - self.theme_cls.standard_increment * 2 + dp(10), - d=0.8, - t="out_elastic", + d=self.show_duration, + t=self.show_transition, ).start(spinner) def hide_anim_spinner(self) -> None: spinner = self.ids.body_spinner - anim = Animation(y=Window.height, d=0.8, t="out_elastic") + anim = Animation( + y=Window.height, d=self.hide_duration, t=self.hide_transition + ) anim.bind(on_complete=self.set_spinner) anim.start(spinner) diff --git a/sbapp/kivymd/uix/relativelayout.py b/sbapp/kivymd/uix/relativelayout.py index 64f4658..b0e58e1 100644 --- a/sbapp/kivymd/uix/relativelayout.py +++ b/sbapp/kivymd/uix/relativelayout.py @@ -31,11 +31,14 @@ MDRelativeLayout from kivy.uix.relativelayout import RelativeLayout +from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior -class MDRelativeLayout(DeclarativeBehavior, RelativeLayout, MDAdaptiveWidget): +class MDRelativeLayout( + DeclarativeBehavior, ThemableBehavior, RelativeLayout, MDAdaptiveWidget +): """ Relative layout class. For more information, see in the :class:`~kivy.uix.relativelayout.RelativeLayout` class documentation. diff --git a/sbapp/kivymd/uix/screen.py b/sbapp/kivymd/uix/screen.py index 2e399ec..1f24676 100644 --- a/sbapp/kivymd/uix/screen.py +++ b/sbapp/kivymd/uix/screen.py @@ -32,12 +32,13 @@ MDScreen from kivy.properties import ListProperty, ObjectProperty from kivy.uix.screenmanager import Screen +from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior from kivymd.uix.hero import MDHeroTo -class MDScreen(DeclarativeBehavior, Screen, MDAdaptiveWidget): +class MDScreen(DeclarativeBehavior, ThemableBehavior, Screen, MDAdaptiveWidget): """ Screen is an element intended to be used with a :class:`~kivymd.uix.screenmanager.MDScreenManager`. For more information, diff --git a/sbapp/kivymd/uix/segmentedbutton/__init__.py b/sbapp/kivymd/uix/segmentedbutton/__init__.py new file mode 100644 index 0000000..f516e1c --- /dev/null +++ b/sbapp/kivymd/uix/segmentedbutton/__init__.py @@ -0,0 +1,4 @@ +from .segmentedbutton import ( # NOQA F401 + MDSegmentedButton, + MDSegmentedButtonItem, +) diff --git a/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.kv b/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.kv new file mode 100644 index 0000000..c4b63ff --- /dev/null +++ b/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.kv @@ -0,0 +1,32 @@ + + size_hint: None, None + height: "40dp" + opacity: 0 + + + + size_hint: None, None + height: self.parent.height + line_color: + self.theme_cls.disabled_hint_text_color \ + if self.parent.line_color == [0, 0, 0, 0] else \ + self.parent.line_color + + SegmentButtonIcon: + id: scale_icon + icon: root.icon + size_hint: None, None + size: "24dp", "24dp" + pos_hint: {"center_y": .5} + scale_value_x: 1 if root.icon else 0 + scale_value_y: 1 if root.icon else 0 + x: label_text.x - dp(32) + + MDLabel: + id: label_text + text: root.text + adaptive_size: True + pos_hint: {"center_y": .5} + x: + root.center_x - (self.texture_size[0] / 2) \ + + (dp(16) if root.icon else 0) diff --git a/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.py b/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.py new file mode 100644 index 0000000..0729dba --- /dev/null +++ b/sbapp/kivymd/uix/segmentedbutton/segmentedbutton.py @@ -0,0 +1,653 @@ +""" +Components/SegmentedButton +========================== + +.. versionadded:: 1.2.0 + +.. seealso:: + + `Material Design spec, Segmented buttons `_ + + `Segmented control `_ + +.. rubric:: Segmented buttons help people select options, switch views, + or sort elements. + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-preview.png + :align: center + +Usage +----- + +.. code-block:: kv + + MDScreen: + + MDSegmentedButton: + + MDSegmentedButtonItem: + icon: ... + text: ... + + MDSegmentedButtonItem: + icon: ... + text: ... + + MDSegmentedButtonItem: + icon: ... + text: ... + +Example +------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDScreen: + + MDSegmentedButton: + pos_hint: {"center_x": .5, "center_y": .5} + + MDSegmentedButtonItem: + text: "Walking" + + MDSegmentedButtonItem: + text: "Transit" + + MDSegmentedButtonItem: + text: "Driving" + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + +By default, segmented buttons support single marking of elements: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-multiselect-false.gif + :align: center + +For multiple marking of elements, use the +:attr:`kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButton.multiselect` +parameter: + +.. code-block:: kv + + MDSegmentedButton: + multiselect: True + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-multiselect-true.gif + :align: center + +Control width +------------- + +The width of the panel of segmented buttons will be equal to the width +of the texture of the widest button multiplied by the number of buttons: + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-width-by-default.png + :align: center + +But you can use the `size_hint_x` parameter to specify the relative width: + +.. code-block:: kv + + MDSegmentedButton: + size_hint_x: .9 + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-width-size-hint-x.png + :align: center + +Customization +------------- + +You can see below in the documentation from which classes the +:class:`~kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButton` and +:class:`~kivymd.uix.segmentedbutton.segmentedbutton.MDSegmentedButtonItem` +classes are inherited and use all their attributes such as +`md_bg_color`, `md_bg_color` etc. for additional customization of segments. + +Events +------ + +- on_marked + The method is called when a segment is marked. + +- on_unmarked + The method is called when a segment is unmarked. + +.. code-block:: kv + + MDSegmentedButton: + on_marked: app.on_marked(*args) + +.. code-block:: python + + def on_marked( + self, + segment_button: MDSegmentedButton, + segment_item: MDSegmentedButtonItem, + marked: bool, + ) -> None: + print(segment_button) + print(segment_item) + print(marked) + +A practical example +------------------- + +.. code-block:: python + + import os + + from faker import Faker + + from kivy.clock import Clock + from kivy.lang import Builder + from kivy.properties import StringProperty + + from kivymd.app import MDApp + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.segmentedbutton import MDSegmentedButton, MDSegmentedButtonItem + from kivymd.utils import asynckivy + + KV = ''' + + adaptive_height: True + md_bg_color: "#343930" + radius: 16 + + TwoLineAvatarListItem: + id: item + divider: None + _no_ripple_effect: True + text: root.name + secondary_text: root.path_to_file + theme_text_color: "Custom" + text_color: "#8A8D79" + secondary_theme_text_color: self.theme_text_color + secondary_text_color: self.text_color + on_size: + self.ids._left_container.size = (item.height, item.height) + self.ids._left_container.x = dp(6) + self._txt_right_pad = item.height + dp(12) + + ImageLeftWidget: + source: root.album + radius: root.radius + + + MDScreen: + md_bg_color: "#151514" + + MDBoxLayout: + orientation: "vertical" + padding: "12dp" + spacing: "12dp" + + MDLabel: + adaptive_height: True + text: "Your downloads" + font_style: "H5" + theme_text_color: "Custom" + text_color: "#8A8D79" + + MDSegmentedButton: + size_hint_x: 1 + selected_color: "#303A29" + line_color: "#343930" + on_marked: app.on_marked(*args) + + MDSegmentedButtonItem: + text: "Songs" + active: True + + MDSegmentedButtonItem: + text: "Albums" + + MDSegmentedButtonItem: + text: "Podcasts" + + RecycleView: + id: card_list + viewclass: "UserCard" + bar_width: 0 + + RecycleBoxLayout: + orientation: 'vertical' + spacing: "16dp" + padding: "16dp" + default_size: None, dp(72) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + ''' + + + class UserCard(MDBoxLayout): + name = StringProperty() + path_to_file = StringProperty() + album = StringProperty() + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + def on_marked( + self, + segment_button: MDSegmentedButton, + segment_item: MDSegmentedButtonItem, + marked: bool, + ) -> None: + self.generate_card() + + def generate_card(self): + async def generate_card(): + for i in range(10): + await asynckivy.sleep(0) + self.root.ids.card_list.data.append( + { + "name": fake.name(), + "path_to_file": f"{os.path.splitext(fake.file_path())[0]}.mp3", + "album": fake.image_url(), + } + ) + + fake = Faker() + self.root.ids.card_list.data = [] + Clock.schedule_once(lambda x: asynckivy.start(generate_card())) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/segmented-button-practical-example.gif + :align: center +""" + +from __future__ import annotations + +__all__ = ("MDSegmentedButton", "MDSegmentedButtonItem") + +import os + +from kivy.animation import Animation +from kivy.clock import Clock +from kivy.lang import Builder +from kivy.metrics import dp +from kivy.properties import ( + BooleanProperty, + ColorProperty, + ListProperty, + NumericProperty, + StringProperty, + VariableListProperty, +) +from kivy.uix.behaviors import ButtonBehavior + +from kivymd import uix_path +from kivymd.uix.behaviors import RectangularRippleBehavior, ScaleBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.floatlayout import MDFloatLayout +from kivymd.uix.label import MDIcon + +with open( + os.path.join(uix_path, "segmentedbutton", "segmentedbutton.kv"), + encoding="utf-8", +) as kv_file: + Builder.load_string(kv_file.read()) + + +class MDSegmentedButtonItem( + RectangularRippleBehavior, ButtonBehavior, MDFloatLayout +): + """ + Segment button item. + + For more information, see in the + :class:`~kivymd.uix.behaviors.RectangularRippleBehavior` and + :class:`~kivy.uix.behaviors.ButtonBehavior` and + :class:`~kivymd.uix.boxlayout.MDBoxLayout` + class documentation. + """ + + icon = StringProperty() + """ + Icon segment. + + :attr:`icon` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + text = StringProperty() + """ + Text segment. + + :attr:`text` is an :class:`~kivy.properties.StringProperty` + and defaults to `''`. + """ + + active = BooleanProperty(False) + """ + Background color of an disabled segment. + + :attr:`active` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + disabled_color = ColorProperty(None) + """ + Is active segment. + + :attr:`active` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + _no_ripple_effect = BooleanProperty(True) + _current_icon = "" + _current_md_bg_color = None + + def on_disabled(self, instance, value: bool) -> None: + def on_disabled(*args): + if value: + if not self._current_md_bg_color: + self._current_md_bg_color = self.md_bg_color + self.md_bg_color = ( + self.theme_cls.disabled_hint_text_color + if not self.disabled_color + else self.disabled_color + ) + else: + if self._current_md_bg_color: + self.md_bg_color = self._current_md_bg_color + self._current_md_bg_color = None + + Clock.schedule_once(on_disabled) + + def on_icon(self, instance, icon_name: str): + if icon_name != "check": + self._current_icon = icon_name + + +# TODO: +# Add the feature to use both text and icons in segments - +# https://m3.material.io/components/segmented-buttons/guidelines#26abac1c-c6bd-44c1-a969-8c910c880b98 +# Icons: optional check icon to indicate selected state - +# https://m3.material.io/components/segmented-buttons/overview#7b80f313-7d3a-4865-b26c-1f7ec98ba694 +# Hovered: add a color for the hovered segment - +# https://m3.material.io/components/segmented-buttons/specs#d730b3ba-c59e-4ef8-b652-20979fe20b67 +# Density: Each step down in density removes 4dp from the height - +# https://m3.material.io/components/segmented-buttons/specs#2d5cab36-1deb-40bd-9e37-bc2bb1657009 + + +class MDSegmentedButton(MDBoxLayout): + """ + Segment button panel. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. + + :Events: + `on_marked` + The method is called when a segment is marked. + `on_unmarked` + The method is called when a segment is unmarked. + """ + + radius = VariableListProperty([20], length=4) + """ + Panel radius. + + :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` + and defaults to `[20, 20, 20, 20]`. + """ + + multiselect = BooleanProperty(False) + """ + Do I allow multiple segment selection. + + :attr:`multiselect` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + hiding_icon_transition = StringProperty("linear") + """ + Name of the transition hiding the current icon. + + :attr:`hiding_icon_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'linear'`. + """ + + hiding_icon_duration = NumericProperty(0.05) + """ + Duration of hiding the current icon. + + :attr:`hiding_icon_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.05`. + """ + + opening_icon_transition = StringProperty("linear") + """ + The name of the transition that opens a new icon of the "marked" type. + + :attr:`opening_icon_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'linear'`. + """ + + opening_icon_duration = NumericProperty(0.05) + """ + The duration of opening a new icon of the "marked" type. + + :attr:`opening_icon_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.05`. + """ + + selected_items = ListProperty() + """ + The list of :class:`~MDSegmentedButtonItem` objects that are currently + marked. + + :attr:`selected_items` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + selected_color = ColorProperty(None) + """ + Color of the marked segment. + + :attr:`selected_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_event_type("on_marked") + self.register_event_type("on_unmarked") + Clock.schedule_once(self.mark_segment) + Clock.schedule_once(self.adjust_segment_radius) + Clock.schedule_once(self.adjust_segment_panel_width, 2) + + def mark_segment(self, *args) -> None: + """Programmatically marks a segment.""" + + for widget in self.children: + if widget.active: + widget.active = False + widget.dispatch("on_release") + + if not self.multiselect: + break + + def adjust_segment_radius(self, *args) -> None: + """Rounds off the first and last elements.""" + + if self.children[0].radius == [0, 0, 0, 0]: + self.children[0].radius = (0, self.height / 2, self.height / 2, 0) + if self.children[-1].radius == [0, 0, 0, 0]: + self.children[-1].radius = (self.height / 2, 0, 0, self.height / 2) + + def adjust_segment_panel_width(self, *args) -> None: + """ + Sets the width of all segments and the width of the panel + by the widest segment. + """ + + if not self.size_hint_x: + width_list = [ + widget.ids.label_text.texture_size[0] + + (dp(72) if widget.icon else dp(48)) + for widget in self.children + ] + max_width = max(width_list) + self.width = max_width * len(width_list) + else: + max_width = self.width / len(self.children) + + for widget in self.children: + widget.width = max_width + + self.opacity = 1 + + for widget in self.children: + if widget.active: + widget.dispatch("on_release") + + def shift_segment_text(self, segment_item: MDSegmentedButtonItem) -> None: + """ + Shifts the segment text to the right, thus freeing up space + for the icon (when the segment is marked). + """ + + Animation( + x=( + segment_item.ids.label_text.x + + ( + dp(16) + if not segment_item.icon and not segment_item.active + else 0 + ) + ) + if not segment_item.active + else ( + segment_item.ids.label_text.x + - ( + dp(16) + if not segment_item.icon and segment_item.active + else 0 + ) + ), + d=0.2, + ).start(segment_item.ids.label_text) + + def show_icon_marked_segment( + self, segment_item: MDSegmentedButtonItem + ) -> None: + """ + Sets the icon for the marked segment and changes the icon scale + to the normal scale. + """ + + segment_item.ids.scale_icon.icon = "check" + if segment_item.ids.scale_icon.icon == "check" and segment_item.active: + segment_item.ids.scale_icon.icon = segment_item._current_icon + + Animation( + scale_value_x=1, + scale_value_y=1, + d=self.opening_icon_duration, + t=self.opening_icon_transition, + ).start(segment_item.ids.scale_icon) + + self.shift_segment_text(segment_item) + self.set_selected_segment_list(segment_item) + self.set_bg_marked_segment(segment_item) + + def hide_icon_marked_segment( + self, segment_item: MDSegmentedButtonItem + ) -> None: + """Changes the scale of the icon of the marked segment to zero.""" + + anim = Animation( + scale_value_x=0, + scale_value_y=0, + d=self.hiding_icon_duration, + t=self.hiding_icon_transition, + ) + anim.bind( + on_complete=lambda x, y: self.show_icon_marked_segment(segment_item) + ) + anim.start(segment_item.ids.scale_icon) + + def restore_bg_segment(self, segment_item) -> None: + Animation(md_bg_color=self.md_bg_color, d=0.2).start(segment_item) + + def set_bg_marked_segment(self, segment_item) -> None: + if segment_item.active: + Animation( + md_bg_color=self.selected_color + if self.selected_color + else self.theme_cls.primary_color, + d=0.2, + ).start(segment_item) + + def set_selected_segment_list(self, segment_item) -> None: + segment_item.active = not segment_item.active + + if segment_item.active: + self.selected_items.append(segment_item) + self.dispatch("on_marked", segment_item, segment_item.active) + else: + if segment_item in self.selected_items: + self.selected_items.remove(segment_item) + self.dispatch("on_unmarked", segment_item, segment_item.active) + + def mark_item(self, segment_item: MDSegmentedButtonItem) -> None: + if segment_item.active and not self.multiselect: + return + if not self.multiselect and self.selected_items: + self.uncheck_item() + else: + if segment_item.active: + self.restore_bg_segment(segment_item) + + self.hide_icon_marked_segment(segment_item) + + def uncheck_item(self) -> None: + for item in self.children: + if item.active: + self.hide_icon_marked_segment(item) + self.restore_bg_segment(item) + break + + def add_widget(self, widget, *args, **kwargs): + if isinstance(widget, MDSegmentedButtonItem): + widget.bind(on_release=self.mark_item) + return super().add_widget(widget) + + def on_size(self, instance_segment_button, size: list) -> None: + """Called when the root screen is resized.""" + + if self.size_hint_x: + max_width = size[0] / len(self.children) + for widget in self.children: + widget.width = max_width + + def on_marked(self, *args): + """The method is called when a segment is marked.""" + + def on_unmarked(self, *args): + """The method is called when a segment is unmarked.""" + + +class SegmentButtonIcon(MDIcon, ScaleBehavior): + """Implements an icon with scaling behavior.""" diff --git a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv index 3ac3656..8ceb004 100644 --- a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv +++ b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.kv @@ -1,3 +1,6 @@ +#:import SEGMENT_CONTROL_SEGMENT_SWITCH_ELEVATION kivymd.material_resources.SEGMENT_CONTROL_SEGMENT_SWITCH_ELEVATION + + adaptive_height: True halign: "center" @@ -15,8 +18,9 @@ pos_hint: {"center_y": .5} x: root._segment_switch_x md_bg_color: root.segment_color - elevation: 2 + elevation: SEGMENT_CONTROL_SEGMENT_SWITCH_ELEVATION _radius: root.radius[0] - 4 + shadow_radius: self._radius width: segment_panel.width / segment_panel.children_number \ - segment_panel.spacing diff --git a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py index 5e9c7f4..90db85c 100644 --- a/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py +++ b/sbapp/kivymd/uix/segmentedcontrol/segmentedcontrol.py @@ -121,7 +121,6 @@ from kivy.properties import ( ) from kivymd import uix_path -from kivymd.theming import ThemableBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.button import MDRaisedButton from kivymd.uix.card import MDSeparator @@ -145,12 +144,12 @@ class MDSegmentedControlItem(MDLabel): # TODO: Add an attribute for the color of the active segment label. -class MDSegmentedControl(MDRelativeLayout, ThemableBehavior): +class MDSegmentedControl(MDRelativeLayout): """ Implements a segmented control panel. - Relative layout class. For more information, see in the - :class:`~kivy.uix.relativelayout.RelativeLayout` class documentation. + For more information, see in the + :class:`~kivymd.uix.relativelayout.MDRelativeLayout` class documentation. :Events: `on_active` @@ -159,7 +158,7 @@ class MDSegmentedControl(MDRelativeLayout, ThemableBehavior): md_bg_color = ColorProperty([0, 0, 0, 0]) """ - Background color of the segment panel. + Background color of the segment panel in (r, g, b, a) or string format. .. code-block:: kv @@ -175,7 +174,7 @@ class MDSegmentedControl(MDRelativeLayout, ThemableBehavior): segment_color = ColorProperty([0, 0, 0, 0]) """ - Color of the active segment. + Color of the active segment in (r, g, b, a) or string format. .. code-block:: kv @@ -220,7 +219,8 @@ class MDSegmentedControl(MDRelativeLayout, ThemableBehavior): separator_color = ColorProperty(None) """ - The color of the separator between the segments. + The color of the separator between the segments in (r, g, b, a) or string + format. .. code-block:: kv diff --git a/sbapp/kivymd/uix/selection/selection.py b/sbapp/kivymd/uix/selection/selection.py index a82804f..1ee8676 100644 --- a/sbapp/kivymd/uix/selection/selection.py +++ b/sbapp/kivymd/uix/selection/selection.py @@ -276,7 +276,6 @@ from kivy.properties import ( ) from kivymd import uix_path -from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import TouchBehavior from kivymd.uix.button import MDIconButton from kivymd.uix.list import MDList @@ -295,7 +294,7 @@ class SelectionIconCheck(MDIconButton): icon_check_color = ColorProperty([0, 0, 0, 1]) -class SelectionItem(ThemableBehavior, MDRelativeLayout, TouchBehavior): +class SelectionItem(MDRelativeLayout, TouchBehavior): selected = BooleanProperty(False) """ Whether or not an item is checked. @@ -514,6 +513,11 @@ class SelectionItem(ThemableBehavior, MDRelativeLayout, TouchBehavior): class MDSelectionList(MDList): """ + Selection list class. + + For more information, see in the + :class:`~kivymd.uix.list.MDList` classes documentation. + :Events: `on_selected` Called when a list item is selected. @@ -548,7 +552,8 @@ class MDSelectionList(MDList): icon_bg_color = ColorProperty([1, 1, 1, 1]) """ - Background color of the icon that will mark the selected list item. + Background color in (r, g, b, a) or string format of the icon that will + mark the selected list item. :attr:`icon_bg_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[1, 1, 1, 1]`. @@ -556,7 +561,8 @@ class MDSelectionList(MDList): icon_check_color = ColorProperty([0, 0, 0, 1]) """ - Color of the icon that will mark the selected list item. + Color in (r, g, b, a) or string format of the icon that will mark the + selected list item. :attr:`icon_check_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[1, 1, 1, 1]`. @@ -564,7 +570,7 @@ class MDSelectionList(MDList): overlay_color = ColorProperty([0, 0, 0, 0.2]) """ - The overlay color of the selected list item.. + The overlay color in (r, g, b, a) or string format of the selected list item. :attr:`overlay_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0.2]]`. @@ -580,7 +586,8 @@ class MDSelectionList(MDList): progress_round_color = ColorProperty(None) """ - Color of the spinner for switching of `selected_mode` mode. + Color in (r, g, b, a) or string format of the spinner for switching of + `selected_mode` mode. :attr:`progress_round_color` is an :class:`~kivy.properties.NumericProperty` and defaults to `None`. diff --git a/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py b/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py index 031fd3a..6bcefe6 100755 --- a/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py +++ b/sbapp/kivymd/uix/selectioncontrol/selectioncontrol.py @@ -4,13 +4,12 @@ Components/SelectionControls .. seealso:: - `Material Design spec, Selection controls `_ + `Material Design spec, Checkbox `_ + + `Material Design spec, Switch `_ .. rubric:: Selection controls allow the user to select options. -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/selection-controll.png - :align: center - `KivyMD` provides the following selection controls classes for use: - MDCheckbox_ @@ -20,6 +19,12 @@ Components/SelectionControls MDCheckbox ---------- +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox.png + :align: center + +Usage +----- + .. code-block:: python from kivy.lang import Builder @@ -37,18 +42,20 @@ MDCheckbox ''' - class Test(MDApp): + class Example(MDApp): def build(self): + self.theme_cls.primary_palette = "Green" + self.theme_cls.theme_style = "Dark" return Builder.load_string(KV) - Test().run() + Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox.gif :align: center .. Note:: Be sure to specify the size of the checkbox. By default, it is - ``(dp(48), dp(48))``, but the ripple effect takes up all the available + `(dp(48), dp(48))`, but the ripple effect takes up all the available space. Control state @@ -94,20 +101,138 @@ MDCheckbox with group ''' - class Test(MDApp): + class Example(MDApp): def build(self): + self.theme_cls.primary_palette = "Green" + self.theme_cls.theme_style = "Dark" return Builder.load_string(KV) - Test().run() + Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox-group.gif :align: center +Parent and child checkboxes +--------------------------- + +Checkboxes can have a parent-child relationship with other checkboxes. When +the parent checkbox is checked, all child checkboxes are checked. If a parent +checkbox is unchecked, all child checkboxes are unchecked. If some, but not all, +child checkboxes are checked, the parent checkbox becomes an indeterminate +checkbox. + +Usage +----- + +.. code-block:: kv + + MDCheckbox: + group: "root" # this is a required name for the parent checkbox group + + MDCheckbox: + group: "child" # this is a required name for a group of child checkboxes + + MDCheckbox: + group: "child" # this is a required name for a group of child checkboxes + +Example +------- + +.. code-block:: python + + from kivy.lang import Builder + from kivy.properties import StringProperty + + from kivymd.app import MDApp + from kivymd.uix.boxlayout import MDBoxLayout + + KV = ''' + + adaptive_height: True + + MDCheckbox: + size_hint: None, None + size: "48dp", "48dp" + group: root.group + + MDLabel: + text: root.text + adaptive_height: True + theme_text_color: "Custom" + text_color: "#B2B6AE" + pos_hint: {"center_y": .5} + + + MDBoxLayout: + orientation: "vertical" + md_bg_color: "#141612" + + MDTopAppBar: + md_bg_color: "#21271F" + specific_text_color: "#B2B6AE" + elevation: 0 + title: "Meal options" + left_action_items: [["arrow-left", lambda x: x]] + anchor_title: "left" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + padding: "12dp", "36dp", 0, 0 + + CheckItem: + text: "Recieve emails" + group: "root" + + MDBoxLayout: + orientation: "vertical" + adaptive_height: True + padding: "24dp", 0, 0, 0 + + CheckItem: + text: "Daily" + group: "child" + + CheckItem: + text: "Weekly" + group: "child" + + CheckItem: + text: "Monthly" + group: "child" + + MDWidget: + ''' + + + class CheckItem(MDBoxLayout): + text = StringProperty() + group = StringProperty() + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Teal" + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox-parent-child.gif + :align: center + .. MDSwitch: MDSwitch -------- +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/switch.png + :align: center + +Usage +----- + .. code-block:: python from kivy.lang import Builder @@ -122,58 +247,20 @@ MDSwitch ''' - class Test(MDApp): + class Example(MDApp): def build(self): + self.theme_cls.primary_palette = "Green" + self.theme_cls.theme_style = "Dark" return Builder.load_string(KV) - Test().run() + Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-switch.gif :align: center -.. Note:: For :class:`~MDSwitch` size is not required. By default it is - ``(dp(36), dp(48))``, but you can increase the width if you want. - -.. code-block:: kv - - MDSwitch: - width: dp(64) - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/md-switch_width.png - :align: center - .. Note:: Control state of :class:`~MDSwitch` same way as in :class:`~MDCheckbox`. - -MDSwitch in M3 style --------------------- - -.. code-block:: python - - from kivy.lang import Builder - - from kivymd.app import MDApp - - KV = ''' - MDScreen: - - MDSwitch: - pos_hint: {'center_x': .5, 'center_y': .5} - active: True - ''' - - - class Test(MDApp): - def build(self): - self.theme_cls.material_style = "M3" - return Builder.load_string(KV) - - - Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/checkbox-m3.gif - :align: center """ __all__ = ("MDCheckbox", "MDSwitch") @@ -195,9 +282,14 @@ from kivy.uix.floatlayout import FloatLayout from kivymd import uix_path from kivymd.theming import ThemableBehavior -from kivymd.uix.behaviors import CircularRippleBehavior, CommonElevationBehavior +from kivymd.uix.behaviors import ( + CircularRippleBehavior, + CommonElevationBehavior, + ScaleBehavior, +) from kivymd.uix.floatlayout import MDFloatLayout from kivymd.uix.label import MDIcon +from kivymd.utils import asynckivy with open( os.path.join(uix_path, "selectioncontrol", "selectioncontrol.kv"), @@ -206,7 +298,22 @@ with open( Builder.load_string(kv_file.read()) -class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): +class MDCheckbox( + CircularRippleBehavior, ScaleBehavior, ToggleButtonBehavior, MDIcon +): + """ + Checkbox class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.CircularRippleBehavior` and + :class:`~kivy.uix.behaviors.ToggleButtonBehavior` and + :class:`~kivymd.uix.label.MDIcon` + classes documentation. + """ + + __allow_child_checkboxes_active = True + __allow_root_checkbox_active = True + active = BooleanProperty(False) """ Indicates if the checkbox is active or inactive. @@ -235,7 +342,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): radio_icon_normal = StringProperty("checkbox-blank-circle-outline") """ - Background icon (when using the ``group`` option) of the checkbox used for + Background icon (when using the `group` option) of the checkbox used for the default graphical representation when the checkbox is not pressed. :attr:`radio_icon_normal` is a :class:`~kivy.properties.StringProperty` @@ -244,7 +351,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): radio_icon_down = StringProperty("checkbox-marked-circle") """ - Background icon (when using the ``group`` option) of the checkbox used for + Background icon (when using the `group` option) of the checkbox used for the default graphical representation when the checkbox is pressed. :attr:`radio_icon_down` is a :class:`~kivy.properties.StringProperty` @@ -253,7 +360,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): color_active = ColorProperty(None) """ - Color when the checkbox is in the active state. + Color in (r, g, b, a) or string format when the checkbox is in the active state. .. versionadded:: 1.0.0 @@ -271,7 +378,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): color_inactive = ColorProperty(None) """ - Color when the checkbox is in the inactive state. + Color in (r, g, b, a) or string format when the checkbox is in the inactive state. .. versionadded:: 1.0.0 @@ -289,7 +396,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): disabled_color = ColorProperty(None) """ - Color when the checkbox is in the disabled state. + Color in (r, g, b, a) or string format when the checkbox is in the disabled state. .. code-block:: kv @@ -309,7 +416,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): selected_color = ColorProperty(None, deprecated=True) """ - Color when the checkbox is in the active state. + Color in (r, g, b, a) or string format when the checkbox is in the active state. .. deprecated:: 1.0.0 Use :attr:`color_active` instead. @@ -320,7 +427,7 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): unselected_color = ColorProperty(None, deprecated=True) """ - Color when the checkbox is in the inactive state. + Color in (r, g, b, a) or string format when the checkbox is in the inactive state. .. deprecated:: 1.0.0 Use :attr:`color_inactive` instead. @@ -332,9 +439,11 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): _current_color = ColorProperty([0.0, 0.0, 0.0, 0.0]) def __init__(self, **kwargs): - self.check_anim_out = Animation(font_size=0, duration=0.1, t="out_quad") + self.check_anim_out = Animation( + scale_value_x=0, scale_value_y=0, duration=0.1, t="out_quad" + ) self.check_anim_in = Animation( - font_size=sp(24), duration=0.1, t="out_quad" + scale_value_x=1, scale_value_y=1, duration=0.1, t="out_quad" ) super().__init__(**kwargs) self.color_active = self.theme_cls.primary_color @@ -364,6 +473,13 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): self.update_color() def update_primary_color(self, instance, value) -> None: + """ + Called when the values of + :attr:`kivymd.theming.ThemableBehavior.theme_cls.theme_style` and + :attr:`kivymd.theming.ThemableBehavior.theme_cls.primary_color` + change. + """ + if value in ("Dark", "Light"): if not self.disabled: self.color = self.theme_cls.primary_color @@ -373,18 +489,41 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): self.color_active = value def update_icon(self, *args) -> None: + """ + Called when the values of + :attr:`checkbox_icon_normal` and + :attr:`checkbox_icon_down` and + :attr:`radio_icon_normal` and + :attr:`group` + change. + """ + if self.state == "down": self.icon = ( - self.radio_icon_down if self.group else self.checkbox_icon_down + self.radio_icon_down + if self.group and self.group not in ["root", "child"] + else self.checkbox_icon_down + if self.group != "root" + else "minus-box" ) else: self.icon = ( self.radio_icon_normal - if self.group + if self.group and self.group not in ["root", "child"] else self.checkbox_icon_normal ) def update_color(self, *args) -> None: + """ + Called when the values of + :attr:`color_active` and + :attr:`color_inactive` and + :attr:`disabled_color` and + :attr:`disabled` and + :attr:`state` + change. + """ + if self.disabled: self._current_color = self.disabled_color elif self.state == "down": @@ -393,6 +532,8 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): self._current_color = self.color_inactive def on_state(self, *args) -> None: + """Called when the values of :attr:`state` change.""" + if self.state == "down": self.check_anim_in.cancel(self) self.check_anim_out.start(self) @@ -408,8 +549,45 @@ class MDCheckbox(CircularRippleBehavior, ToggleButtonBehavior, MDIcon): self.active = False def on_active(self, *args) -> None: + """Called when the values of :attr:`active` change.""" + self.state = "down" if self.active else "normal" + if ( + self.group + and self.group == "root" + and MDCheckbox.__allow_root_checkbox_active + ): + self.set_child_active(self.active) + elif self.group and self.group == "child": + if MDCheckbox.__allow_child_checkboxes_active: + self.set_root_active() + + def set_root_active(self) -> None: + root_checkbox = self.get_widgets("root") + if root_checkbox: + MDCheckbox.__allow_root_checkbox_active = False + root_checkbox[0].active = True in [ + child.active for child in self.get_widgets("child") + ] + MDCheckbox.__allow_root_checkbox_active = True + + def set_child_active(self, active: bool): + for child in self.get_widgets("child"): + child.active = active + MDCheckbox.__allow_child_checkboxes_active = True + + def on_touch_down(self, touch): + if self.collide_point(touch.x, touch.y): + if self.group and self.group == "root": + MDCheckbox.__allow_child_checkboxes_active = False + return super().on_touch_down(touch) + + def _release_group(self, current): + if self.group and self.group in ["root", "child"]: + return + super()._release_group(current) + class ThumbIcon(MDIcon): """ @@ -419,14 +597,8 @@ class ThumbIcon(MDIcon): """ -class Thumb( - CommonElevationBehavior, - CircularRippleBehavior, - MDFloatLayout, -): - """ - Implements a thumb for the :class:`~MDSwitch` widget. - """ +class Thumb(CommonElevationBehavior, CircularRippleBehavior, MDFloatLayout): + """Implements a thumb for the :class:`~MDSwitch` widget.""" def _set_ellipse(self, instance, value): self.ellipse.size = (self._ripple_rad, self._ripple_rad) @@ -443,6 +615,14 @@ class Thumb( class MDSwitch(ThemableBehavior, FloatLayout): + """ + Switch class. + + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.floatlayout.FloatLayout` classes documentation. + """ + active = BooleanProperty(False) """ Indicates if the switch is active or inactive. @@ -490,7 +670,8 @@ class MDSwitch(ThemableBehavior, FloatLayout): icon_active_color = ColorProperty(None) """ - Thumb icon color when the switch is in the active state (only M3 style). + Thumb icon color in (r, g, b, a) or string format when the switch is in the + active state (only M3 style). .. versionadded:: 1.0.0 @@ -510,7 +691,8 @@ class MDSwitch(ThemableBehavior, FloatLayout): icon_inactive_color = ColorProperty(None) """ - Thumb icon color when the switch is in an inactive state (only M3 style). + Thumb icon color in (r, g, b, a) or string format when the switch is in an + inactive state (only M3 style). .. versionadded:: 1.0.0 @@ -529,7 +711,7 @@ class MDSwitch(ThemableBehavior, FloatLayout): thumb_color_active = ColorProperty(None) """ - The color of the thumb when the switch is active. + The color in (r, g, b, a) or string format of the thumb when the switch is active. .. versionadded:: 1.0.0 @@ -548,7 +730,7 @@ class MDSwitch(ThemableBehavior, FloatLayout): thumb_color_inactive = ColorProperty(None) """ - The color of the thumb when the switch is inactive. + The color in (r, g, b, a) or string format of the thumb when the switch is inactive. .. versionadded:: 1.0.0 @@ -566,7 +748,8 @@ class MDSwitch(ThemableBehavior, FloatLayout): thumb_color_disabled = ColorProperty(None) """ - The color of the thumb when the switch is in the disabled state. + The color in (r, g, b, a) or string format of the thumb when the switch is + in the disabled state. .. code-block:: kv @@ -584,7 +767,7 @@ class MDSwitch(ThemableBehavior, FloatLayout): track_color_active = ColorProperty(None) """ - The color of the track when the switch is active. + The color in (r, g, b, a) or string format of the track when the switch is active. .. code-block:: kv @@ -601,7 +784,7 @@ class MDSwitch(ThemableBehavior, FloatLayout): track_color_inactive = ColorProperty(None) """ - The color of the track when the switch is inactive. + The color in (r, g, b, a) or string format of the track when the switch is inactive. .. versionadded:: 1.0.0 @@ -619,7 +802,8 @@ class MDSwitch(ThemableBehavior, FloatLayout): track_color_disabled = ColorProperty(None) """ - The color of the track when the switch is in the disabled state. + The color in (r, g, b, a) or string format of the track when the switch is + in the disabled state. .. code-block:: kv @@ -646,6 +830,11 @@ class MDSwitch(ThemableBehavior, FloatLayout): Clock.schedule_once(lambda x: self.on_active(self, self.active)) def set_icon(self, instance_switch, icon_value: str) -> None: + """ + Called when the values of + :attr:`icon_active` and :attr:`icon_inactive` change. + """ + def set_icon(*args): icon = icon_value if icon_value else "blank" self.ids.thumb.ids.icon.icon = icon @@ -653,6 +842,8 @@ class MDSwitch(ThemableBehavior, FloatLayout): Clock.schedule_once(set_icon, 0.2) def on_active(self, instance_switch, active_value: bool) -> None: + """Called when the values of :attr:`active` change.""" + if self.theme_cls.material_style == "M3" and self.widget_style != "ios": size = ( ( diff --git a/sbapp/kivymd/uix/slider/slider.py b/sbapp/kivymd/uix/slider/slider.py index 152a602..f0824ae 100644 --- a/sbapp/kivymd/uix/slider/slider.py +++ b/sbapp/kivymd/uix/slider/slider.py @@ -52,7 +52,7 @@ class MDSlider(ThemableBehavior, Slider): color = ColorProperty(None) """ - Color slider. + Color slider in (r, g, b, a) or string format. .. code-block:: kv @@ -84,7 +84,7 @@ class MDSlider(ThemableBehavior, Slider): hint_bg_color = ColorProperty(None) """ - Hint rectangle color in (r.g.b.a) format. + Hint rectangle color in (r, g, b, a) or string format. .. code-block:: kv @@ -101,7 +101,7 @@ class MDSlider(ThemableBehavior, Slider): hint_text_color = ColorProperty(None) """ - Hint text color in (r.g.b.a) format. + Hint text color in in (r, g, b, a) or string format. .. code-block:: kv @@ -138,7 +138,7 @@ class MDSlider(ThemableBehavior, Slider): thumb_color_active = ColorProperty(None) """ - The color of the thumb when the slider is active. + The color in (r, g, b, a) or string format of the thumb when the slider is active. .. versionadded:: 1.0.0 @@ -156,7 +156,7 @@ class MDSlider(ThemableBehavior, Slider): thumb_color_inactive = ColorProperty(None) """ - The color of the thumb when the slider is inactive. + The color in (r, g, b, a) or string format of the thumb when the slider is inactive. .. versionadded:: 1.0.0 @@ -174,7 +174,8 @@ class MDSlider(ThemableBehavior, Slider): thumb_color_disabled = ColorProperty(None) """ - The color of the thumb when the slider is in the disabled state. + The color in (r, g, b, a) or string format of the thumb when the slider is + in the disabled state. .. versionadded:: 1.0.0 @@ -194,7 +195,7 @@ class MDSlider(ThemableBehavior, Slider): track_color_active = ColorProperty(None) """ - The color of the track when the slider is active. + The color in (r, g, b, a) or string format of the track when the slider is active. .. versionadded:: 1.0.0 @@ -212,7 +213,7 @@ class MDSlider(ThemableBehavior, Slider): track_color_inactive = ColorProperty(None) """ - The color of the track when the slider is inactive. + The color in (r, g, b, a) or string format of the track when the slider is inactive. .. versionadded:: 1.0.0 @@ -230,7 +231,8 @@ class MDSlider(ThemableBehavior, Slider): track_color_disabled = ColorProperty(None) """ - The color of the track when the slider is in the disabled state. + The color in (r, g, b, a) or string format of the track when the slider is + in the disabled state. .. versionadded:: 1.0.0 diff --git a/sbapp/kivymd/uix/sliverappbar/sliverappbar.py b/sbapp/kivymd/uix/sliverappbar/sliverappbar.py index 6316315..f8e8ab5 100644 --- a/sbapp/kivymd/uix/sliverappbar/sliverappbar.py +++ b/sbapp/kivymd/uix/sliverappbar/sliverappbar.py @@ -97,7 +97,7 @@ Example class CardItem(MDCard): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.elevation = 3 + self.elevation = 1 class Example(MDApp): @@ -130,7 +130,6 @@ from kivy.properties import ( ) from kivymd import uix_path -from kivymd.theming import ThemableBehavior from kivymd.uix.boxlayout import MDBoxLayout from kivymd.uix.toolbar import MDTopAppBar @@ -144,8 +143,13 @@ class MDSliverAppbarException(Exception): pass -class MDSliverAppbarContent(ThemableBehavior, MDBoxLayout): - """Implements a box for a scrollable list of custom items.""" +class MDSliverAppbarContent(MDBoxLayout): + """ + Implements a box for a scrollable list of custom items. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. + """ md_bg_color = ColorProperty([0, 0, 0, 0]) """ @@ -165,13 +169,20 @@ class MDSliverAppbarContent(ThemableBehavior, MDBoxLayout): class MDSliverAppbarHeader(MDBoxLayout): - pass - - -class MDSliverAppbar(MDBoxLayout, ThemableBehavior): """ - MDSliverAppbar class. - See module documentation for more information. + Sliver app bar header class. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. + """ + + +class MDSliverAppbar(MDBoxLayout): + """ + Sliver app bar class. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. :Events: :attr:`on_scroll_content` @@ -254,7 +265,7 @@ class MDSliverAppbar(MDBoxLayout, ThemableBehavior): class CardItem(MDCard): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.elevation = 3 + self.elevation = 1 class SliverToolbar(MDTopAppBar): @@ -292,7 +303,7 @@ class MDSliverAppbar(MDBoxLayout, ThemableBehavior): background_color = ColorProperty(None) """ - Background color of toolbar in (r, g, b, a) format. + Background color of toolbar in (r, g, b, a) or string format. .. code-block:: kv diff --git a/sbapp/kivymd/uix/snackbar/__init__.py b/sbapp/kivymd/uix/snackbar/__init__.py index ed3c68d..a6fd8ec 100644 --- a/sbapp/kivymd/uix/snackbar/__init__.py +++ b/sbapp/kivymd/uix/snackbar/__init__.py @@ -1 +1,6 @@ -from .snackbar import BaseSnackbar, Snackbar # NOQA F401 +from .snackbar import ( # NOQA F401 + MDSnackbar, + MDSnackbarActionButton, + MDSnackbarCloseButton, + Snackbar, +) diff --git a/sbapp/kivymd/uix/snackbar/snackbar.kv b/sbapp/kivymd/uix/snackbar/snackbar.kv index 557ceda..4334bbf 100644 --- a/sbapp/kivymd/uix/snackbar/snackbar.kv +++ b/sbapp/kivymd/uix/snackbar/snackbar.kv @@ -1,34 +1,28 @@ -#:import window kivy.core.window +#:import SNACK_BAR_ELEVATION kivymd.material_resources.SNACK_BAR_ELEVATION +#:import SNACK_BAR_OFFSET kivymd.material_resources.SNACK_BAR_OFFSET - + + padding: 0, 0, "8dp", 0 size_hint_y: None - height: "58dp" - spacing: "10dp" - padding: "10dp", "10dp", "10dp", "10dp" - md_bg_color: "323232" if not root.bg_color else root.bg_color - radius: root.radius - elevation: 4 if root.padding else 0 + height: self.minimum_height + md_bg_color: "#323232" + elevation: SNACK_BAR_ELEVATION + shadow_offset: SNACK_BAR_OFFSET - canvas: - Color: - rgba: self.md_bg_color - RoundedRectangle: - size: self.size - pos: self.pos - radius: self.radius - - - - MDLabel: - id: text_bar - size_hint_y: None - height: self.texture_size[1] - text: root.text - font_size: root.font_size - theme_text_color: "Custom" - text_color: "ffffff" - shorten: True - shorten_from: "right" - markup: True + SnackbarLabelContainer: + id: label_container + padding: "16dp", "15dp", 0, "15dp" + orientation: "vertical" + adaptive_height: True pos_hint: {"center_y": .5} + spacing: "4dp" + + SnackbarActionButtonContainer: + id: action_container + size_hint_x: None + + SnackbarCloseButtonContainer: + id: close_container + size_hint_x: None + width: "38dp" diff --git a/sbapp/kivymd/uix/snackbar/snackbar.py b/sbapp/kivymd/uix/snackbar/snackbar.py index c4fbdc9..fdb7ebe 100755 --- a/sbapp/kivymd/uix/snackbar/snackbar.py +++ b/sbapp/kivymd/uix/snackbar/snackbar.py @@ -4,7 +4,7 @@ Components/Snackbar .. seealso:: - `Material Design spec, Snackbars `_ + `Material Design spec, Snackbars `_ .. rubric:: Snackbars provide brief messages about app processes at the bottom of the screen. @@ -15,261 +15,281 @@ Components/Snackbar Usage ----- +.. code-block:: python + + MDSnackbar( + MDLabel( + text="First string", + theme_text_color="Custom", + text_color="#393231", + ), + ).open() + +Example +------- + .. code-block:: python from kivy.lang import Builder from kivymd.app import MDApp + from kivymd.uix.label import MDLabel + from kivymd.uix.snackbar import MDSnackbar + KV = ''' - #:import Snackbar kivymd.uix.snackbar.Snackbar - - MDScreen: MDRaisedButton: text: "Create simple snackbar" - on_release: Snackbar(text="This is a snackbar!").open() + on_release: app.open_snackbar() pos_hint: {"center_x": .5, "center_y": .5} ''' - class Test(MDApp): + class Example(MDApp): + def open_snackbar(self): + MDSnackbar( + MDLabel( + text="First string", + ), + ).open() + def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) - Test().run() + Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-simple.gif :align: center -Usage with snackbar_x, snackbar_y ---------------------------------- +Control width and pos +--------------------- .. code-block:: python - Snackbar( - text="This is a snackbar!", - snackbar_x="10dp", - snackbar_y="10dp", - size_hint_x=( - Window.width - (dp(10) * 2) - ) / Window.width + MDSnackbar( + MDLabel( + text="First string", + ), + pos=(dp(24), dp(56)), + size_hint_x=0.5, ).open() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-padding.gif +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-widith-and-pos.gif :align: center -Control width +On mobile, use up to two lines of text to communicate the snackbar message: + +.. code-block:: python + + MDSnackbar( + MDLabel( + text="First string", + theme_text_color="Custom", + text_color="#393231", + ), + MDLabel( + text="Second string", + theme_text_color="Custom", + text_color="#393231", + ), + y=dp(24), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + md_bg_color="#E8D8D7", + ).open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-two-line.gif + :align: center + +Usage action button +------------------- + +A snackbar can contain a single action. "Dismiss" or "cancel" actions are +optional: + +.. code-block:: python + + MDSnackbar( + MDLabel( + text="First string", + theme_text_color="Custom", + text_color="#393231", + ), + MDSnackbarActionButton( + text="Done", + theme_text_color="Custom", + text_color="#8E353C", + ), + y=dp(24), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + md_bg_color="#E8D8D7", + ).open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-action-button.gif + :align: center + +Callback action button +---------------------- + +.. code-block:: python + + def snackbar_action_button_callback(self, *args): + print("Snackbar callback action button") + + def open_snackbar(self): + self.snackbar = MDSnackbar( + MDLabel( + text="First string", + theme_text_color="Custom", + text_color="#393231", + ), + MDSnackbarActionButton( + text="Done", + theme_text_color="Custom", + text_color="#8E353C", + _no_ripple_effect=True, + on_release=self.snackbar_action_button_callback, + ), + y=dp(24), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + md_bg_color="#E8D8D7", + ) + self.snackbar.open() + +If an action is long, it can be displayed on a third line: + +.. code-block:: python + + MDSnackbar( + MDLabel( + text="If an action is long, it can be displayed", + theme_text_color="Custom", + text_color="#393231", + ), + MDLabel( + text="on a third line.", + theme_text_color="Custom", + text_color="#393231", + ), + MDLabel( + text=" ", + ), + MDSnackbarActionButton( + text="Action button", + theme_text_color="Custom", + text_color="#8E353C", + y=dp(8), + _no_ripple_effect=True, + ), + y=dp(24), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + md_bg_color="#E8D8D7", + ).open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-action-button-on-thrid-line.gif + :align: center + +Icon (optional close affordance): + +.. code-block:: python + + def snackbar_close(self, *args): + self.snackbar.dismiss() + + def open_snackbar(self): + self.snackbar = MDSnackbar( + MDLabel( + text="Icon (optional close affordance)", + theme_text_color="Custom", + text_color="#393231", + ), + MDSnackbarActionButton( + text="Action button", + theme_text_color="Custom", + text_color="#8E353C", + _no_ripple_effect=True, + ), + MDSnackbarCloseButton( + icon="close", + theme_text_color="Custom", + text_color="#8E353C", + _no_ripple_effect=True, + on_release=self.snackbar_close, + ), + y=dp(24), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + md_bg_color="#E8D8D7", + ) + self.snackbar.open() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-optional-close-affordance.gif + :align: center + +API break +========= + +1.1.1 version ------------- -.. code-block:: python - - Snackbar( - text="This is a snackbar!", - snackbar_x="10dp", - snackbar_y="10dp", - size_hint_x=.5 - ).open() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-percent-width.png - :align: center - -Custom text color ------------------ - -.. code-block:: python - - Snackbar( - text="[color=#ddbb34]This is a snackbar![/color]", - snackbar_y="10dp", - snackbar_y="10dp", - size_hint_x=.7 - ).open() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-custom-color.png - :align: center - -Usage with button ------------------ - .. code-block:: python snackbar = Snackbar( - text="This is a snackbar!", + text="First string", snackbar_x="10dp", - snackbar_y="10dp", + snackbar_y="24dp", ) snackbar.size_hint_x = ( Window.width - (snackbar.snackbar_x * 2) ) / Window.width snackbar.buttons = [ MDFlatButton( - text="UPDATE", - text_color=(1, 1, 1, 1), - on_release=snackbar.dismiss, - ), - MDFlatButton( - text="CANCEL", - text_color=(1, 1, 1, 1), + text="Done", + theme_text_color="Custom", + text_color="#8E353C", on_release=snackbar.dismiss, ), ] snackbar.open() -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-button.png - :align: center - -Using a button with custom color --------------------------------- +1.2.0 version +------------- .. code-block:: python - Snackbar( - ... - bg_color=(0, 0, 1, 1), + MDSnackbar( + MDLabel( + text="First string", + ), + MDSnackbarActionButton( + text="Done", + theme_text_color="Custom", + text_color="#8E353C", + ), + y=dp(24), + pos_hint={"center_x": 0.5}, + size_hint_x=0.5, + md_bg_color="#E8D8D7", ).open() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-button-custom-color.png - :align: center - -Custom usage ------------- - -.. code-block:: python - - from kivy.lang import Builder - from kivy.animation import Animation - from kivy.clock import Clock - from kivy.metrics import dp - - from kivymd.app import MDApp - from kivymd.uix.snackbar import Snackbar - - - KV = ''' - MDScreen: - - MDFloatingActionButton: - id: button - x: root.width - self.width - dp(10) - y: dp(10) - on_release: app.snackbar_show() - ''' - - - class Test(MDApp): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.screen = Builder.load_string(KV) - self.snackbar = None - self._interval = 0 - - def build(self): - return self.screen - - def wait_interval(self, interval): - self._interval += interval - if self._interval > self.snackbar.duration + 0.5: - anim = Animation(y=dp(10), d=.2) - anim.start(self.screen.ids.button) - Clock.unschedule(self.wait_interval) - self._interval = 0 - self.snackbar = None - - def snackbar_show(self): - if not self.snackbar: - self.snackbar = Snackbar(text="This is a snackbar!") - self.snackbar.open() - anim = Animation(y=dp(72), d=.2) - anim.bind(on_complete=lambda *args: Clock.schedule_interval( - self.wait_interval, 0)) - anim.start(self.screen.ids.button) - - - Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-custom-usage.gif - :align: center - -Custom Snackbar ---------------- - -.. code-block:: python - - from kivy.lang import Builder - from kivy.core.window import Window - from kivy.properties import StringProperty, NumericProperty - - from kivymd.app import MDApp - from kivymd.uix.button import MDFlatButton - from kivymd.uix.snackbar import BaseSnackbar - - KV = ''' - - - MDIconButton: - pos_hint: {'center_y': .5} - icon: root.icon - opposite_colors: True - - MDLabel: - id: text_bar - size_hint_y: None - height: self.texture_size[1] - text: root.text - font_size: root.font_size - theme_text_color: 'Custom' - text_color: 'ffffff' - shorten: True - shorten_from: 'right' - pos_hint: {'center_y': .5} - - - MDScreen: - - MDRaisedButton: - text: "SHOW" - pos_hint: {"center_x": .5, "center_y": .45} - on_press: app.show() - ''' - - - class CustomSnackbar(BaseSnackbar): - text = StringProperty(None) - icon = StringProperty(None) - font_size = NumericProperty("15sp") - - - class Test(MDApp): - def build(self): - return Builder.load_string(KV) - - def show(self): - snackbar = CustomSnackbar( - text="This is a snackbar!", - icon="information", - snackbar_x="10dp", - snackbar_y="10dp", - buttons=[MDFlatButton(text="ACTION", text_color=(1, 1, 1, 1))] - ) - snackbar.size_hint_x = ( - Window.width - (snackbar.snackbar_x * 2) - ) / Window.width - snackbar.open() - - - Test().run() - -.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/snackbar-custom.png - :align: center """ -__all__ = ("Snackbar", "BaseSnackbar") +__all__ = ( + "MDSnackbar", + "MDSnackbarActionButton", + "MDSnackbarCloseButton", +) import os +from kivy import Logger from kivy.animation import Animation from kivy.clock import Clock from kivy.core.window import Window @@ -284,8 +304,12 @@ from kivy.properties import ( ) from kivymd import uix_path -from kivymd.uix.button import BaseButton +from kivymd.uix.behaviors import MotionShackBehavior +from kivymd.uix.boxlayout import MDBoxLayout +from kivymd.uix.button import MDFlatButton, MDIconButton from kivymd.uix.card import MDCard +from kivymd.uix.label import MDLabel +from kivymd.uix.relativelayout import MDRelativeLayout with open( os.path.join(uix_path, "snackbar", "snackbar.kv"), encoding="utf-8" @@ -293,31 +317,63 @@ with open( Builder.load_string(kv_file.read()) -class BaseSnackbar(MDCard): +class SnackbarLabelContainer(MDBoxLayout): + """Container for placing snackbar text.""" + + +class SnackbarActionButtonContainer(MDRelativeLayout): + """Container for placing snackbar action button.""" + + +class SnackbarCloseButtonContainer(MDRelativeLayout): + """Container for placing snackbar close button.""" + + +class MDSnackbarCloseButton(MDIconButton): """ + Snackbar closed button class. + + For more information, see in the + :class:`~kivymd.uix.button.MDIconButton` class documentation. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.y and not self.pos_hint: + self.pos_hint = {"center_y": 0.5} + + +class MDSnackbarActionButton(MDFlatButton): + """ + Snackbar action button class. + + For more information, see in the + :class:`~kivymd.uix.button.MDFlatButton` class documentation. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.y and not self.pos_hint: + self.pos_hint = {"center_y": 0.5} + + +class MDSnackbar(MotionShackBehavior, MDCard): + """ + Snackbar class. + + .. versionchanged:: 1.2.0 + Rename `BaseSnackbar` to `MDSnackbar` class. + + For more information, see in the + :class:`~kivymd.uix.card.MDCard` and + :class:`~kivymd.uix.behaviors.StencilBehavior` + class documentation. + :Events: :attr:`on_open` - Called when a dialog is opened. + Called when a snackbar opened. :attr:`on_dismiss` - When the front layer rises. - - Abstract base class for all Snackbars. - This class handles sizing, positioning, shape and events for Snackbars - - All Snackbars will be made off of this `BaseSnackbar`. - - `BaseSnackbar` will always try to fill the remainder of the screen with - your Snackbar. - - To make your Snackbar dynamic and symetric with snackbar_x. - - Set size_hint_x like below: - - .. code-block:: python - - size_hint_z = ( - Window.width - (snackbar_x * 2) - ) / Window.width + Called when a snackbar closes. """ duration = NumericProperty(3) @@ -333,23 +389,7 @@ class BaseSnackbar(MDCard): Whether to use automatic closing of the snackbar or not. :attr:`auto_dismiss` is a :class:`~kivy.properties.BooleanProperty` - and defaults to `'True'`. - """ - - bg_color = ColorProperty(None) - """ - Snackbar background. - - :attr:`bg_color` is a :class:`~kivy.properties.ColorProperty` - and defaults to `None`. - """ - - buttons = ListProperty() - """ - Snackbar buttons. - - :attr:`buttons` is a :class:`~kivy.properties.ListProperty` - and defaults to `'[]'` + and defaults to `True`. """ radius = ListProperty([5, 5, 5, 5]) @@ -357,256 +397,153 @@ class BaseSnackbar(MDCard): Snackbar radius. :attr:`radius` is a :class:`~kivy.properties.ListProperty` - and defaults to `'[5, 5, 5, 5]'` + and defaults to `[5, 5, 5, 5]` + """ + + bg_color = ColorProperty(None, deprecated=True) + """ + Snackbar background color in (r, g, b, a) or string format. + + .. deprecated:: 1.2.0 + Use 'md_bg_color` instead. + + :attr:`bg_color` is a :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + buttons = ListProperty(deprecated=True) + """ + Snackbar buttons. + + .. deprecated:: 1.2.0 + + :attr:`buttons` is a :class:`~kivy.properties.ListProperty` + and defaults to `[]` """ snackbar_animation_dir = OptionProperty( "Bottom", options=["Top", "Bottom", "Left", "Right"], + deprecated=True, ) """ Snackbar animation direction. + Available options are: `'Top'`, `'Bottom'`, `'Left'`, `'Right'`. - Available options are: `"Top"`, `"Bottom"`, `"Left"`, `"Right"` + .. deprecated:: 1.2.0 :attr:`snackbar_animation_dir` is an :class:`~kivy.properties.OptionProperty` and defaults to `'Bottom'`. """ - snackbar_x = NumericProperty("0dp") + snackbar_x = NumericProperty(0, deprecated=True) """ The snackbar x position in the screen + .. deprecated:: 1.2.0 + :attr:`snackbar_x` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0dp`. + and defaults to `0`. """ - snackbar_y = NumericProperty("0dp") + snackbar_y = NumericProperty(0, deprecated=True) """ The snackbar x position in the screen + .. deprecated:: 1.2.0 + :attr:`snackbar_y` is a :class:`~kivy.properties.NumericProperty` - and defaults to `0dp`. + and defaults to `0`. """ - _interval = 0 - - def __init__(self, **kwargs): - super().__init__(**kwargs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.register_event_type("on_open") self.register_event_type("on_dismiss") + self.opacity = 0 - def dismiss(self, *args): + def dismiss(self, *args) -> None: """Dismiss the snackbar.""" - def dismiss(interval): - if self.snackbar_animation_dir == "Top": - anim = Animation(y=(Window.height + self.height), d=0.2) - elif self.snackbar_animation_dir == "Left": - anim = Animation(x=-self.width, d=0.2) - elif self.snackbar_animation_dir == "Right": - anim = Animation(x=Window.width, d=0.2) - else: - anim = Animation(y=-self.height, d=0.2) + super().on_dismiss() - anim.bind( - on_complete=lambda *args: Window.parent.remove_widget(self) - ) - anim.start(self) - - Clock.schedule_once(dismiss, 0.5) - self.dispatch("on_dismiss") - - def open(self): + def open(self) -> None: """Show the snackbar.""" - def wait_interval(interval): - self._interval += interval - if self._interval > self.duration: - self.dismiss() - Clock.unschedule(wait_interval) - self._interval = 0 - - for c in Window.parent.children: - if isinstance(c, BaseSnackbar): + for widget in Window.parent.children: + if widget.__class__ is MDSnackbar: return - if self.snackbar_y > (Window.height - self.height): - self.snackbar_y = Window.height - self.height + Window.parent.add_widget(self) + super().on_open() - self._calc_radius() + def add_widget(self, widget, *args, **kwargs): + def check_color(color): + if not widget.text_color: + widget.theme_text_color = "Custom" + widget.text_color = color - if self.size_hint_x == 1: - self.size_hint_x = (Window.width - self.snackbar_x) / Window.width - - if ( - self.snackbar_animation_dir == "Top" - or self.snackbar_animation_dir == "Bottom" - ): - self.x = self.snackbar_x - - if self.snackbar_animation_dir == "Top": - self.y = Window.height + self.height - else: - self.y = -self.height - - Window.parent.add_widget(self) - - if self.snackbar_animation_dir == "Top": - anim = Animation( - y=self.snackbar_y - if self.snackbar_y != 0 - else Window.height - self.height, - d=0.2, + if isinstance(widget, MDSnackbarCloseButton): + widget.icon_size = "20sp" + check_color("white") + self.ids.close_container.add_widget(widget) + if len(self.ids.close_container.children) >= 2: + Logger.warning( + "KivyMD: " + "Do not use more than one button to close the snackbar. " + "This is contrary to the material design rules " + "of version 3" ) - else: - anim = Animation( - y=self.snackbar_y if self.snackbar_y != 0 else 0, d=0.2 + if isinstance(widget, MDSnackbarActionButton): + self.ids.action_container.add_widget(widget) + check_color(self.theme_cls.primary_color) + if len(self.ids.action_container.children) >= 2: + Logger.warning( + "KivyMD: " + "Do not use more than one action button. " + "This is contrary to the material design rules " + "of version 3" ) - - elif ( - self.snackbar_animation_dir == "Left" - or self.snackbar_animation_dir == "Right" - ): - self.y = self.snackbar_y - - if self.snackbar_animation_dir == "Left": - self.x = -Window.width - else: - self.x = Window.width - - Window.parent.add_widget(self) - anim = Animation( - x=self.snackbar_x if self.snackbar_x != 0 else 0, d=0.2 - ) - - if self.auto_dismiss: - anim.bind( - on_complete=lambda *args: Clock.schedule_interval( - wait_interval, 0 + if isinstance(widget, MDLabel): + widget.adaptive_height = True + widget.pos_hint = {"center_y": 0.5} + check_color("white") + self.ids.label_container.add_widget(widget) + if len(self.ids.label_container.children) >= 4: + Logger.warning( + "KivyMD: " + "Do not use more than three lines in the snackbar. " + "This is contrary to the material design rules " + "of version 3" ) - ) - anim.start(self) - self.dispatch("on_open") - - def on_open(self, *args): - """Called when a dialog is opened.""" - - def on_dismiss(self, *args): - """Called when the dialog is closed.""" - - def on_buttons(self, instance, value): - def on_buttons(interval): - for button in value: - if issubclass(button.__class__, (BaseButton,)): - self.add_widget(button) - else: - raise ValueError( - f"The {button} object must be inherited from the base class " - ) - - Clock.schedule_once(on_buttons) - - def _calc_radius(self): - if ( - self.snackbar_animation_dir == "Top" - or self.snackbar_animation_dir == "Bottom" + elif isinstance( + widget, + ( + SnackbarLabelContainer, + SnackbarActionButtonContainer, + SnackbarCloseButtonContainer, + ), ): + return super().add_widget(widget) - if self.snackbar_y == 0 and self.snackbar_x == 0: + def on_open(self, *args) -> None: + """Called when a snackbar opened.""" - if self.size_hint_x == 1: - self.radius = [0, 0, 0, 0] - else: - if self.snackbar_animation_dir == "Top": - self.radius = [0, 0, self.radius[2], 0] - else: - self.radius = [0, self.radius[1], 0, 0] - - elif self.snackbar_y != 0 and self.snackbar_x == 0: - - if self.size_hint_x == 1: - self.radius = [0, 0, 0, 0] - else: - if self.snackbar_y >= Window.height - self.height: - self.radius = [0, 0, self.radius[2], 0] - else: - self.radius = [0, self.radius[1], self.radius[2], 0] - - elif self.snackbar_y == 0 and self.snackbar_x != 0: - - if self.size_hint_x == 1: - if self.snackbar_animation_dir == "Top": - self.radius = [0, 0, 0, self.radius[3]] - else: - self.radius = [self.radius[0], 0, 0, 0] - else: - if self.snackbar_animation_dir == "Top": - self.radius = [0, 0, self.radius[2], self.radius[3]] - else: - self.radius = [self.radius[0], self.radius[1], 0, 0] - - else: # self.snackbar_y != 0 and self.snackbar_x != 0 - - if self.size_hint_x == 1: - self.radius = [self.radius[0], 0, 0, self.radius[3]] - elif self.snackbar_y >= Window.height - self.height: - self.radius = [0, 0, self.radius[2], self.radius[3]] - - elif ( - self.snackbar_animation_dir == "Left" - or self.snackbar_animation_dir == "Right" - ): - - if self.snackbar_y == 0 and self.snackbar_x == 0: - - if self.size_hint_x == 1: - self.radius = [0, 0, 0, 0] - else: - self.radius = [0, self.radius[1], 0, 0] - - elif self.snackbar_y != 0 and self.snackbar_x == 0: - - if self.size_hint_x == 1: - self.radius = [0, 0, 0, 0] - else: - self.radius = [0, self.radius[1], self.radius[2], 0] - - elif self.snackbar_y == 0 and self.snackbar_x != 0: - - if self.size_hint_x == 1: - self.radius = [self.radius[0], 0, 0, 0] - else: - self.radius = [self.radius[0], self.radius[1], 0, 0] - - else: # self.snackbar_y != 0 and self.snackbar_x != 0 - - if self.size_hint_x == 1: - if self.snackbar_y >= Window.height - self.height: - self.radius = [0, 0, 0, self.radius[3]] - else: - self.radius = [self.radius[0], 0, 0, self.radius[3]] - elif self.snackbar_y >= Window.height - self.height: - self.radius = [0, 0, self.radius[2], self.radius[3]] + def on_dismiss(self, *args) -> None: + """Called when a snackbar closed.""" -class Snackbar(BaseSnackbar): +class Snackbar(MDSnackbar): """ - Snackbar inherits all its functionality from `BaseSnackbar` + .. deprecated:: 1.2.0 + Use :class:`~kivymd.uix.snackbar.MDSnackbar` + class instead. """ - text = StringProperty() - """ - The text that will appear in the snackbar. - - :attr:`text` is a :class:`~kivy.properties.StringProperty` - and defaults to `''`. - """ - - font_size = NumericProperty("15sp") - """ - The font size of the text that will appear in the snackbar. - - :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and - defaults to `'15sp'`. - """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + Logger.warning( + "KivyMD: " + "The `Snackbar` class has been deprecated. " + "Use the `MDSnackbar` class instead." + ) diff --git a/sbapp/kivymd/uix/spinner/spinner.py b/sbapp/kivymd/uix/spinner/spinner.py index fd6b214..6bf3c33 100755 --- a/sbapp/kivymd/uix/spinner/spinner.py +++ b/sbapp/kivymd/uix/spinner/spinner.py @@ -138,6 +138,10 @@ class MDSpinner(ThemableBehavior, Widget): :class:`MDSpinner` is an implementation of the circular progress indicator in `Google's Material Design`. + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.widget.Widget` classes documentation. + It can be used either as an indeterminate indicator that loops while the user waits for something to happen, or as a determinate indicator. @@ -184,7 +188,7 @@ class MDSpinner(ThemableBehavior, Widget): color = ColorProperty(None, allownone=True) """ - Spinner color. + Spinner color in (r, g, b, a) or string format. :attr:`color` is a :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0]`. diff --git a/sbapp/kivymd/uix/stacklayout.py b/sbapp/kivymd/uix/stacklayout.py index 8988e8b..c5028ca 100644 --- a/sbapp/kivymd/uix/stacklayout.py +++ b/sbapp/kivymd/uix/stacklayout.py @@ -87,11 +87,14 @@ __all__ = ("MDStackLayout",) from kivy.uix.stacklayout import StackLayout +from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior -class MDStackLayout(DeclarativeBehavior, StackLayout, MDAdaptiveWidget): +class MDStackLayout( + DeclarativeBehavior, ThemableBehavior, StackLayout, MDAdaptiveWidget +): """ Stack layout class. For more information, see in the :class:`~kivy.uix.stacklayout.StackLayout` class documentation. diff --git a/sbapp/kivymd/uix/swiper/swiper.py b/sbapp/kivymd/uix/swiper/swiper.py index b89e2e5..e169c91 100644 --- a/sbapp/kivymd/uix/swiper/swiper.py +++ b/sbapp/kivymd/uix/swiper/swiper.py @@ -250,8 +250,10 @@ class _ItemsBox(AnchorLayout): class MDSwiperItem(MDBoxLayout): """ - :class:`MDSwiperItem` is a :class:`BoxLayout` but it's size is adjusted - automatically. + Swiper item class. + + For more information, see in the + :class:`~kivymd.uix.boxlayout.MDBoxLayout` class documentation. """ _root = ObjectProperty() @@ -293,6 +295,13 @@ class MDSwiperItem(MDBoxLayout): class MDSwiper(MDScrollView): + """ + Swiper class. + + For more information, see in the + :class:`~kivymd.uix.scrollview.MDScrollView` class documentation. + """ + items_spacing = NumericProperty("20dp") """ The space between each :class:`MDSwiperItem`. @@ -506,7 +515,6 @@ class MDSwiper(MDScrollView): self.dispatch("on_swipe_right") def on_scroll_start(self, touch, check_children=True): - if platform in ["ios", "android"]: return super().on_scroll_start(touch) diff --git a/sbapp/kivymd/uix/tab/tab.py b/sbapp/kivymd/uix/tab/tab.py index 816711e..4e97bef 100755 --- a/sbapp/kivymd/uix/tab/tab.py +++ b/sbapp/kivymd/uix/tab/tab.py @@ -1412,8 +1412,16 @@ class MDTabs( AnchorLayout, ): """ + Tabs class. You can use this class to create your own tabbed panel. + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior` and + :class:`~kivy.uix.anchorlayout.AnchorLayout` + classes documentation. + :Events: `on_tab_switch` Called when switching tabs. @@ -1518,7 +1526,7 @@ class MDTabs( background_color = ColorProperty(None) """ - Background color of tabs in ``rgba`` format. + Background color of tabs in (r, g, b, a) or string format. :attr:`background_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -1526,7 +1534,7 @@ class MDTabs( underline_color = ColorProperty([0, 0, 0, 0]) """ - Underline color of tabs in ``rgba`` format. + Underline color of tabs in (r, g, b, a) or string format. :attr:`underline_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0]`. @@ -1534,7 +1542,7 @@ class MDTabs( text_color_normal = ColorProperty(None) """ - Text color of the label when it is not selected. + Text color in (r, g, b, a) or string format of the label when it is not selected. :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -1542,7 +1550,7 @@ class MDTabs( text_color_active = ColorProperty(None) """ - Text color of the label when it is selected. + Text color in (r, g, b, a) or string format of the label when it is selected. :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -1592,7 +1600,7 @@ class MDTabs( indicator_color = ColorProperty(None) """ - Color indicator in ``rgba`` format. + Color indicator in (r, g, b, a) or string format. :attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. diff --git a/sbapp/kivymd/uix/textfield/textfield.kv b/sbapp/kivymd/uix/textfield/textfield.kv index 6f11abb..1ed7597 100644 --- a/sbapp/kivymd/uix/textfield/textfield.kv +++ b/sbapp/kivymd/uix/textfield/textfield.kv @@ -7,6 +7,7 @@ # "round" mode. Color: + group: "round-color" rgba: self._fill_color if self.mode == "round" else (0, 0, 0, 0) Ellipse: angle_start: 180 @@ -19,49 +20,32 @@ pos: (self.width - dp(18)) + self.x - self.height / 2.0, self.y size: self.height, self.height Rectangle: - pos: self.x + dp(9), self.y - size: self.width - dp(18), self.height + pos: self.x + dp(14), self.y + size: self.width - dp(28), self.height Color: rgba: ( \ (self.line_color_focus if not self.error else self.error_color) \ - if self.focus \ - else self.theme_cls.disabled_hint_text_color \ + if self.focus else ( \ + self.theme_cls.disabled_hint_text_color \ + if not self.line_color_normal else \ + self.line_color_normal) \ ) \ if self.mode == "round" else \ (0, 0, 0, 0) - Line: - points: - self.x + dp(18), \ - self.y, \ - self.x + self.width - dp(18), \ - self.y - Line: - points: - self.x + dp(18), \ - self.y + self.height, \ - self.x + self.width - dp(18), \ - self.y + self.height - Line: - ellipse: - self.x - self.height / 2 + dp(18), \ + SmoothLine: + width: dp(1) + rounded_rectangle: + self.x, \ self.y, \ + self.width, \ self.height, \ - self.height, \ - 180, \ - 360 - Line: - ellipse: - self.width + self.x - self.height / 2.0 - dp(18), \ - self.y, \ - self.height, \ - self.height, \ - 360, \ - 540 + self.height / 2 # "fill" mode. Color: + group: "fill-color" rgba: self._fill_color if self.mode == "fill" else (0, 0, 0, 0) RoundedRectangle: pos: self.x, self.y @@ -70,6 +54,7 @@ # Static underline texture. Color: + group: "static-underline-color" rgba: (self._line_color_normal \ if self.line_color_normal else self.theme_cls.divider_color) \ @@ -82,6 +67,7 @@ # Active underline (on focus) texture. Color: + group: "active-underline-color" rgba: self._line_color_focus \ if self.mode in ("line", "fill") and self.active_line \ @@ -94,7 +80,10 @@ # Helper text texture. Color: + group: "helper-text-color" rgba: + self.theme_cls.disabled_hint_text_color \ + if self.disabled else \ self._helper_text_color Rectangle: texture: self._helper_text_label.texture @@ -106,7 +95,11 @@ # Right/left icon texture. Color: - rgba: self._icon_right_color if self.icon_right else self._icon_left_color + group: "right-left-icons-color" + rgba: + self.theme_cls.disabled_hint_text_color \ + if self.disabled else \ + (self._icon_right_color if self.icon_right else self._icon_left_color) Rectangle: texture: self._icon_right_label.texture if self.icon_right else self._icon_left_label.texture @@ -137,7 +130,11 @@ # Max length texture. Color: - rgba: self._max_length_text_color + group: "max-length-color" + rgba: + self.theme_cls.disabled_hint_text_color \ + if self.disabled else \ + self._max_length_text_color Rectangle: texture: self._max_length_label.texture size: self._max_length_label.texture_size @@ -148,16 +145,87 @@ # Cursor blink. Color: rgba: - (self.text_color_focus if self.focus else self._text_color_normal) \ + ( \ + (self.text_color_focus if not self.error else self.error_color) \ + if self.focus \ + else self._text_color_normal \ + ) \ if self.focus and not self._cursor_blink \ - else (0, 0, 0, 0) + else \ + (0, 0, 0, 0) Rectangle: pos: (int(x) for x in self.cursor_pos) size: 1, -self.line_height - # Hint text texture. + # "rectangle" mode + Color: + group: "rectangle-color" + rgba: + ( \ + (self.line_color_focus if not self.error else self.error_color) \ + if self.focus else \ + self.line_color_normal \ + ) \ + if self.mode == "rectangle" else \ + (0, 0, 0, 0) + SmoothLine: + width: dp(1) + rounded_rectangle: + self.x, \ + self.y, \ + self.width, \ + self.height - self._hint_text_label.texture_size[1] // 2, \ + root.radius[0] + + # The background color line of the widget on which the text field + # is placed (for background hint text texture). Color: rgba: + ( \ + ( \ + self.parent.md_bg_color \ + if hasattr(self.parent, "md_bg_color") \ + and self.parent.md_bg_color != [1, 1, 1, 0] else \ + self.theme_cls.bg_normal \ + ) \ + if self.focus else \ + ( \ + (0, 0, 0, 0) if not self.text else \ + ( \ + self.parent.md_bg_color \ + if hasattr(self.parent, "md_bg_color") \ + and self.parent.md_bg_color != [1, 1, 1, 0] else \ + self.theme_cls.bg_normal \ + ) \ + ) \ + ) \ + if self.mode == "rectangle" else \ + (0, 0, 0, 0) + SmoothLine: + width: dp(2) + points: + self.x + dp(10), \ + self.top - self._hint_text_label.texture_size[1] // 2, \ + self.x + dp(16) + self._hint_text_label.texture_size[0], \ + self.top - self._hint_text_label.texture_size[1] // 2 + + # Text color. + Color: + group: "text-color" + rgba: + self.theme_cls.disabled_hint_text_color if self.disabled else \ + ( \ + self.text_color_focus if self.focus else self._text_color_normal + ) \ + if not self.error else self.error_color + + canvas.after: + # Hint text texture. + Color: + group: "hint-text-color" + rgba: + self.theme_cls.disabled_hint_text_color \ + if self.disabled else \ self._hint_text_color Rectangle: texture: self._hint_text_label.texture @@ -179,34 +247,6 @@ if self.mode != "line" else \ dp(-6)) if self.mode != "rectangle" else dp(-4)) - self._hint_y - # "rectangle" mode - Color: - rgba: - (self.line_color_focus if not self.error else self.error_color) \ - if self.focus else \ - self.line_color_normal - Line: - width: dp(1) if self.mode == "rectangle" else dp(0.00001) - points: - ( - self.x + self._line_blank_space_right_point, - self.top - self._hint_text_label.texture_size[1] // 2, - self.right, self.top - self._hint_text_label.texture_size[1] // 2, - self.right, self.y, - self.x, self.y, - self.x, self.top - self._hint_text_label.texture_size[1] // 2, - self.x + self._line_blank_space_left_point, - self.top - self._hint_text_label.texture_size[1] // 2 - ) - - # Text color. - Color: - rgba: - self.disabled_foreground_color if self.disabled else \ - ( \ - self.text_color_focus if self.focus else self._text_color_normal - ) \ - if not self.error else self.error_color font_name: "Roboto" if not self.font_name else self.font_name foreground_color: self.theme_cls.text_color diff --git a/sbapp/kivymd/uix/textfield/textfield.py b/sbapp/kivymd/uix/textfield/textfield.py index 08ea932..a351580 100755 --- a/sbapp/kivymd/uix/textfield/textfield.py +++ b/sbapp/kivymd/uix/textfield/textfield.py @@ -528,6 +528,15 @@ class Validator: class MDTextFieldRect(ThemableBehavior, TextInput): + """ + Textfield rect class. + + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.textinput.TextInput` + classes documentation. + """ + line_anim = BooleanProperty(True) """ If True, then text field shows animated line when on focus. @@ -606,6 +615,18 @@ class MDTextField( Validator, AutoFormatTelephoneNumber, ): + """ + Textfield class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.textinput.TextInput` and + :class:`~Validator` and + :class:`~AutoFormatTelephoneNumber` + classes documentation. + """ + helper_text = StringProperty() """ Text for ``helper_text`` mode. @@ -1189,7 +1210,7 @@ class MDTextField( radius = ListProperty([10, 10, 0, 0]) """ - The corner radius for a text field in `fill` mode. + The corner radius for a text field in `fill/rectangle` mode. :attr:`radius` is a :class:`~kivy.properties.ListProperty` and defaults to `[10, 10, 0, 0]`. @@ -1277,6 +1298,7 @@ class MDTextField( _hint_text_font_size=self._hint_text_label.setter("font_size"), _icon_right_color=self._icon_right_label.setter("text_color"), _icon_left_color=self._icon_left_label.setter("text_color"), + font_name_hint_text=self._hint_text_label.setter("font_name"), text=self.set_text, ) self.theme_cls.bind( @@ -1536,7 +1558,7 @@ class MDTextField( if self.mode == "rectangle": self.set_notch_rectangle() - if not self.text and not self.focus: + if (not self.text and not self.focus) or (self.text and not self.focus): self.on_focus(instance_text_field, False) if self.mode == "round" and self.text: @@ -1588,9 +1610,15 @@ class MDTextField( self.helper_text_mode in ("on_focus", "persistent") and self.helper_text ): - self.set_helper_text_color(self.helper_text_color_focus) + Clock.schedule_once( + lambda x: self.set_helper_text_color( + self.helper_text_color_focus + ) + ) if self.mode == "fill": - self.set_fill_color(self.fill_color_focus) + Clock.schedule_once( + lambda x: self.set_fill_color(self.fill_color_focus) + ) self.set_active_underline_width(self.width) self.set_pos_hint_text( @@ -1598,30 +1626,62 @@ class MDTextField( if self.mode != "rectangle" else dp(10) ) - self.set_hint_text_color(focus) + Clock.schedule_once(lambda x: self.set_hint_text_color(focus)) self.set_hint_text_font_size(sp(12)) if self.max_text_length: - self.set_max_length_text_color(self.max_length_text_color) + Clock.schedule_once( + lambda x: self.set_max_length_text_color( + self.max_length_text_color + ) + ) if self.icon_right: - self.set_icon_right_color(self.icon_right_color_focus) + Clock.schedule_once( + lambda x: self.set_icon_right_color( + self.icon_right_color_focus + ) + ) if self.icon_left: - self.set_icon_left_color(self.icon_left_color_focus) + Clock.schedule_once( + lambda x: self.set_icon_left_color( + self.icon_left_color_focus + ) + ) if self.error: if self.hint_text: - self.set_hint_text_color(focus, self.error) + Clock.schedule_once( + lambda x: self.set_hint_text_color(focus, self.error) + ) if self.helper_text: - self.set_helper_text_color(self.error_color) + Clock.schedule_once( + lambda x: self.set_helper_text_color(self.error_color) + ) if self.max_text_length: - self.set_max_length_text_color(self.error_color) + Clock.schedule_once( + lambda x: self.set_max_length_text_color( + self.error_color + ) + ) if self.icon_right: - self.set_icon_right_color(self.error_color) + Clock.schedule_once( + lambda x: self.set_icon_right_color(self.error_color) + ) if self.icon_left: - self.set_icon_left_color(self.error_color) + Clock.schedule_once( + lambda x: self.set_icon_left_color(self.error_color) + ) else: if self.helper_text_mode == "persistent" and self.helper_text: - self.set_helper_text_color(self.helper_text_color_normal) + Clock.schedule_once( + lambda x: self.set_helper_text_color( + self.helper_text_color_normal + ) + ) + if self.helper_text_mode == "on_focus" and self.helper_text: + Clock.schedule_once( + lambda x: self.set_helper_text_color([0.0, 0.0, 0.0, 0.0]) + ) if self.mode == "rectangle" and not self.text: self.set_notch_rectangle(joining=True) if not self.text: @@ -1634,24 +1694,42 @@ class MDTextField( self.set_pos_hint_text(y) self.set_hint_text_font_size(sp(16)) - if self.icon_right: - self.set_icon_right_color(self.icon_right_color_normal) - if self.icon_left: - self.set_icon_left_color(self.icon_left_color_normal) + if self.icon_right and not self.error: + Clock.schedule_once( + lambda x: self.set_icon_right_color( + self.icon_right_color_normal + ) + ) + if self.icon_left and not self.error: + Clock.schedule_once( + lambda x: self.set_icon_left_color( + self.icon_left_color_normal + ) + ) if self.hint_text: - self.set_hint_text_color(focus, self.error) + Clock.schedule_once( + lambda x: self.set_hint_text_color(focus, self.error) + ) self.set_active_underline_width(0) - self.set_max_length_text_color([0, 0, 0, 0]) + Clock.schedule_once( + lambda x: self.set_max_length_text_color([0, 0, 0, 0]) + ) if self.mode == "fill": - self.set_fill_color(self.fill_color_normal) + Clock.schedule_once( + lambda x: self.set_fill_color(self.fill_color_normal) + ) self.error = self._get_has_error() or self.error if self.error: self.set_static_underline_color(self.error_color) else: - self.set_static_underline_color(self.line_color_normal) + Clock.schedule_once( + lambda x: self.set_static_underline_color( + self.line_color_normal + ) + ) def on_icon_left(self, instance_text_field, icon_name: str) -> None: self._icon_left_label.icon = icon_name @@ -1669,33 +1747,61 @@ class MDTextField( """ if error: - self.set_max_length_text_color(self.error_color) + Clock.schedule_once( + lambda x: self.set_max_length_text_color(self.error_color) + ) self.set_active_underline_color(self.error_color) if self.hint_text: self.set_hint_text_color(self.focus, self.error) if self.helper_text: - self.set_helper_text_color(self.error_color) + Clock.schedule_once( + lambda x: self.set_helper_text_color(self.error_color) + ) if self.icon_right: - self.set_icon_right_color(self.error_color) + Clock.schedule_once( + lambda x: self.set_icon_right_color(self.error_color) + ) if self.icon_left: - self.set_icon_left_color(self.error_color) + Clock.schedule_once( + lambda x: self.set_icon_left_color(self.error_color) + ) if self.helper_text_mode == "on_error": - self.set_helper_text_color(self.error_color) + Clock.schedule_once( + lambda x: self.set_helper_text_color(self.error_color) + ) else: - self.set_max_length_text_color(self.max_length_text_color) + Clock.schedule_once( + lambda x: self.set_max_length_text_color( + self.max_length_text_color + ) + ) self.set_active_underline_color(self.line_color_focus) if self.hint_text: self.set_hint_text_color(self.focus) if self.helper_text: - self.set_helper_text_color(self.helper_text_color_focus) + Clock.schedule_once( + lambda x: self.set_helper_text_color( + self.helper_text_color_focus + ) + ) if self.icon_right: - self.set_icon_right_color(self.icon_right_color_focus) + Clock.schedule_once( + lambda x: self.set_icon_right_color( + self.icon_right_color_focus + ) + ) if self.icon_left: - self.set_icon_left_color(self.icon_left_color_focus) - if self.helper_text_mode in ("on_focus", "on_error"): - self.set_helper_text_color([0, 0, 0, 0]) - elif self.helper_text_mode == "persistent": - self.set_helper_text_color(self.helper_text_color_normal) + Clock.schedule_once( + lambda x: self.set_icon_left_color( + self.icon_left_color_focus + ) + ) + if self.helper_text_mode == "persistent": + Clock.schedule_once( + lambda x: self.set_helper_text_color( + self.helper_text_color_normal + ) + ) def on_hint_text(self, instance_text_field, hint_text: str) -> None: if hint_text: @@ -1715,32 +1821,32 @@ class MDTextField( def on_text_color_normal( self, instance_text_field, color: Union[list, str] - ): + ) -> None: self._text_color_normal = color def on_hint_text_color_normal( self, instance_text_field, color: Union[list, str] - ): + ) -> None: self._hint_text_color = color def on_helper_text_color_normal( self, instance_text_field, color: Union[list, str] - ): + ) -> None: self._helper_text_color = color def on_icon_right_color_normal( self, instance_text_field, color: Union[list, str] - ): + ) -> None: self._icon_right_color = color def on_line_color_normal( self, instance_text_field, color: Union[list, str] - ): + ) -> None: self._line_color_normal = color def on_max_length_text_color( self, instance_text_field, color: Union[list, str] - ): + ) -> None: self._max_length_text_color = color def _set_color(self, attr_name: str, color: str, updated: bool) -> None: @@ -1798,79 +1904,75 @@ class MDTextField( if __name__ == "__main__": - from kivy.core.window import Window from kivy.lang import Builder from kivy.uix.textinput import TextInput - Window.size = (800, 750) - from kivymd.app import MDApp KV = """ MDScreen: - MDBoxLayout: - id: box - orientation: "vertical" - spacing: "20dp" - adaptive_height: True - size_hint_x: .8 - pos_hint: {"center_x": .5, "center_y": .5} + MDScrollView: - MDTextField: - hint_text: "Label" - helper_text: "Error message" - mode: "rectangle" - max_text_length: 5 + MDList: + id: box + spacing: "32dp" + padding: "56dp", "12dp", "56dp", "12dp" - MDTextField: - icon_left: "git" - hint_text: "Label" - helper_text: "Error message" - mode: "rectangle" + MDTextField: + hint_text: "Label" + helper_text: "Error message" + mode: "rectangle" + max_text_length: 5 - MDTextField: - icon_left: "git" - hint_text: "Label" - helper_text: "Error message" - mode: "fill" + MDTextField: + icon_left: "git" + hint_text: "Label" + helper_text: "Error message" + mode: "rectangle" - MDTextField: - hint_text: "Label" - helper_text: "Error message" - mode: "fill" + MDTextField: + icon_left: "git" + hint_text: "Label" + helper_text: "Error message" + mode: "fill" - MDTextField: - hint_text: "Label" - helper_text: "Error message" + MDTextField: + hint_text: "Label" + helper_text: "Error message" + mode: "fill" - MDTextField: - icon_left: "git" - hint_text: "Label" - helper_text: "Error message" + MDTextField: + hint_text: "Label" + helper_text: "Error message" - MDTextField: - hint_text: "Round mode" - mode: "round" - max_text_length: 15 - helper_text: "Message" + MDTextField: + icon_left: "git" + hint_text: "Label" + helper_text: "Error message" - MDTextField: - hint_text: "Date dd/mm/yyyy in [01/01/1900, 01/01/2100] interval" - helper_text: "Enter a valid dd/mm/yyyy date" - validator: "date" - date_format: "dd/mm/yyyy" - date_interval: "01/01/1900", "01/01/2100" + MDTextField: + hint_text: "Round mode" + mode: "round" + max_text_length: 15 + helper_text: "Message" - MDTextField: - hint_text: "Email" - helper_text: "user@gmail.com" - validator: "email" + MDTextField: + hint_text: "Date dd/mm/yyyy in [01/01/1900, 01/01/2100] interval" + helper_text: "Enter a valid dd/mm/yyyy date" + validator: "date" + date_format: "dd/mm/yyyy" + date_interval: "01/01/1900", "01/01/2100" - MDFlatButton: - text: "SET TEXT" - pos_hint: {"center_x": .5} - on_release: app.set_text() + MDTextField: + hint_text: "Email" + helper_text: "user@gmail.com" + validator: "email" + + MDFlatButton: + text: "SET TEXT" + pos_hint: {"center_x": .5} + on_release: app.set_text() """ class Test(MDApp): diff --git a/sbapp/kivymd/uix/toolbar/__init__.py b/sbapp/kivymd/uix/toolbar/__init__.py index d6fa24d..e4f163e 100644 --- a/sbapp/kivymd/uix/toolbar/__init__.py +++ b/sbapp/kivymd/uix/toolbar/__init__.py @@ -1,2 +1,8 @@ # NOQA F401 -from .toolbar import MDBottomAppBar, MDTopAppBar +from .toolbar import ( + MDActionBottomAppBarButton, + MDActionOverFlowButton, + MDBottomAppBar, + MDFabBottomAppBarButton, + MDTopAppBar, +) diff --git a/sbapp/kivymd/uix/toolbar/toolbar.py b/sbapp/kivymd/uix/toolbar/toolbar.py index 0a48548..091b040 100755 --- a/sbapp/kivymd/uix/toolbar/toolbar.py +++ b/sbapp/kivymd/uix/toolbar/toolbar.py @@ -8,19 +8,21 @@ Components/Toolbar `Material Design spec, App bars: bottom `_ - `Material Design 3 spec, App bars: bottom `_ + `Material Design 3 spec, App bars: top `_ + + `Material Design 3 spec, App bars: bottom `_ .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-top.png :align: center -`KivyMD` provides the following toolbar positions for use: +`KivyMD` provides the following bar positions for use: -- Top_ -- Bottom_ +- TopAppBar_ +- BottomAppBar_ -.. Top: -Top ---- +.. TopAppBar_: +TopAppBar +--------- .. code-block:: python @@ -31,6 +33,7 @@ Top KV = ''' MDBoxLayout: orientation: "vertical" + md_bg_color: "#1E1E15" MDTopAppBar: title: "MDTopAppBar" @@ -41,12 +44,14 @@ Top ''' - class Test(MDApp): + class Example(MDApp): def build(self): + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) - Test().run() + Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-1.png :align: center @@ -58,6 +63,7 @@ Add left menu MDTopAppBar: title: "MDTopAppBar" + anchor_title: "left" left_action_items: [["menu", lambda x: app.callback()]] .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-2.png @@ -74,6 +80,7 @@ Add right menu MDTopAppBar: title: "MDTopAppBar" + anchor_title: "left" right_action_items: [["dots-vertical", lambda x: app.callback()]] .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-3.png @@ -86,31 +93,38 @@ Add two item to the right menu MDTopAppBar: title: "MDTopAppBar" - right_action_items: [["dots-vertical", lambda x: app.callback_1()], ["clock", lambda x: app.callback_2()]] + anchor_title: "left" + right_action_items: + [ + ["dots-vertical", lambda x: app.callback_1()], + ["clock", lambda x: app.callback_2()] + ] .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-4.png :align: center -Change toolbar color --------------------- +Change bar color +---------------- .. code-block:: kv MDTopAppBar: title: "MDTopAppBar" - md_bg_color: app.theme_cls.accent_color + anchor_title: "left" + md_bg_color: "brown" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-5.png :align: center -Change toolbar text color -------------------------- +Change bar text color +--------------------- .. code-block:: kv MDTopAppBar: title: "MDTopAppBar" - specific_text_color: app.theme_cls.accent_color + anchor_title: "left" + specific_text_color: "white" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-6.png :align: center @@ -122,14 +136,19 @@ Shadow elevation control MDTopAppBar: title: "Elevation 4" + anchor_title: "left" elevation: 4 + shadow_color: "brown" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-7.png :align: center -.. Bottom: -Bottom ------- +.. BottomAppBar: +BottomAppBar +------------ + +M2 style bottom app bar +----------------------- .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-bottom.png :align: center @@ -145,24 +164,28 @@ Usage KV = ''' MDBoxLayout: + md_bg_color: "#1E1E15" # Will always be at the bottom of the screen. MDBottomAppBar: MDTopAppBar: - title: "Title" + title: "MDBottomAppBar" icon: "git" type: "bottom" left_action_items: [["menu", lambda x: x]] ''' - class Test(MDApp): + class Example(MDApp): def build(self): + self.theme_cls.material_style = "M2" + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) - Test().run() + Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-8.png :align: center @@ -177,7 +200,7 @@ Event ``on_action_button``: MDBottomAppBar: MDTopAppBar: - title: "Title" + title: "MDBottomAppBar" icon: "git" type: "bottom" left_action_items: [["menu", lambda x: x]] @@ -198,7 +221,7 @@ Mode: MDBottomAppBar: MDTopAppBar: - title: "Title" + title: "MDBottomAppBar" icon: "git" type: "bottom" left_action_items: [["menu", lambda x: x]] @@ -212,7 +235,7 @@ Mode: MDBottomAppBar: MDTopAppBar: - title: "Title" + title: "MDBottomAppBar" icon: "git" type: "bottom" left_action_items: [["menu", lambda x: x]] @@ -227,22 +250,306 @@ Custom color .. code-block:: kv MDBottomAppBar: - md_bg_color: 0, 1, 0, 1 MDTopAppBar: - title: "Title" + title: "MDBottomAppBar" icon: "git" type: "bottom" left_action_items: [["menu", lambda x: x]] icon_color: 0, 1, 0, 1 + md_bg_bottom_color: "brown" .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-11.png :align: center +M3 style bottom app bar +----------------------- + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/app-bar-bottom-m3.png + :align: center + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + MDFloatLayout: + md_bg_color: "#151511" + + MDBottomAppBar: + md_bg_color: "#232217" + icon_color: "#8A8D79" + + MDFabBottomAppBarButton: + icon: "plus" + md_bg_color: "#373A22" + ''' + + + class Example(MDApp): + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-1.png + :align: center + +Add action items +---------------- + +.. code-block:: kv + + #:import MDActionBottomAppBarButton kivymd.uix.toolbar.MDActionBottomAppBarButton + + + MDFloatLayout: + + MDBottomAppBar: + action_items: + [ + MDActionBottomAppBarButton(icon="gmail"), + MDActionBottomAppBarButton(icon="label-outline"), + MDActionBottomAppBarButton(icon="bookmark"), + ] + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-2.png + :align: center + +Change action items +------------------- + +.. code-block:: python + + from kivy.lang import Builder + + from kivymd.app import MDApp + + KV = ''' + #:import MDActionBottomAppBarButton kivymd.uix.toolbar.MDActionBottomAppBarButton + + + MDFloatLayout: + md_bg_color: "#151511" + + MDBottomAppBar: + id: bottom_appbar + md_bg_color: "#232217" + icon_color: "#8A8D79" + action_items: + [ + MDActionBottomAppBarButton(icon="gmail"), + MDActionBottomAppBarButton(icon="bookmark"), + ] + + MDFabBottomAppBarButton: + icon: "plus" + md_bg_color: "#373A22" + on_release: app.change_actions_items() + ''' + + + class Example(MDApp): + def change_actions_items(self): + self.root.ids.bottom_appbar.action_items = [ + MDActionBottomAppBarButton(icon="magnify"), + MDActionBottomAppBarButton(icon="trash-can-outline"), + MDActionBottomAppBarButton(icon="download-box-outline"), + ] + + def build(self): + self.theme_cls.theme_style = "Dark" + return Builder.load_string(KV) + + + Example().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-3.gif + :align: center + +A practical example +------------------- + +.. code-block:: python + + from kivy.clock import Clock + from kivy.lang import Builder + from kivy.properties import StringProperty, BooleanProperty, ObjectProperty + from kivy.uix.behaviors import FocusBehavior + from kivy.uix.recycleboxlayout import RecycleBoxLayout + from kivy.uix.recycleview.layout import LayoutSelectionBehavior + from kivy.uix.recycleview.views import RecycleDataViewBehavior + + from kivymd.uix.boxlayout import MDBoxLayout + from kivymd.uix.toolbar import MDActionBottomAppBarButton + from kivymd.app import MDApp + from kivymd.utils import asynckivy + + from faker import Faker # pip install Faker + + KV = ''' + #:import MDFabBottomAppBarButton kivymd.uix.toolbar.MDFabBottomAppBarButton + + + + orientation: "vertical" + adaptive_height: True + md_bg_color: "#373A22" if self.selected else "#1F1E15" + radius: 16 + padding: 0, 0, 0, "16dp" + + TwoLineAvatarListItem: + divider: None + _no_ripple_effect: True + text: root.name + secondary_text: root.time + theme_text_color: "Custom" + text_color: "#8A8D79" + secondary_theme_text_color: self.theme_text_color + secondary_text_color: self.text_color + + ImageLeftWidget: + source: root.avatar + radius: self.height / 2 + + MDLabel: + text: root.text + adaptive_height: True + theme_text_color: "Custom" + text_color: "#8A8D79" + padding_x: "16dp" + shorten: True + shorten_from: "right" + + Widget: + + + MDFloatLayout: + md_bg_color: "#151511" + + RecycleView: + id: card_list + viewclass: "UserCard" + + SelectableRecycleGridLayout: + orientation: 'vertical' + spacing: "16dp" + padding: "16dp" + default_size: None, dp(120) + default_size_hint: 1, None + size_hint_y: None + height: self.minimum_height + multiselect: True + touch_multiselect: True + + MDBottomAppBar: + id: bottom_appbar + scroll_cls: card_list + allow_hidden: True + md_bg_color: "#232217" + icon_color: "#8A8D79" + + MDFabBottomAppBarButton: + id: fab_button + icon: "plus" + md_bg_color: "#373A22" + ''' + + + class UserCard(RecycleDataViewBehavior, MDBoxLayout): + name = StringProperty() + time = StringProperty() + text = StringProperty() + avatar = StringProperty() + callback = ObjectProperty(lambda x: x) + + index = None + selected = BooleanProperty(False) + selectable = BooleanProperty(True) + + def refresh_view_attrs(self, rv, index, data): + self.index = index + return super().refresh_view_attrs(rv, index, data) + + def on_touch_down(self, touch): + if super().on_touch_down(touch): + return True + if self.collide_point(*touch.pos) and self.selectable: + Clock.schedule_once(self.callback) + return self.parent.select_with_touch(self.index, touch) + + def apply_selection(self, rv, index, is_selected): + self.selected = is_selected + rv.data[index]["selected"] = is_selected + + + class SelectableRecycleGridLayout( + FocusBehavior, LayoutSelectionBehavior, RecycleBoxLayout + ): + pass + + + class Test(MDApp): + selected_cards = False + + def build(self): + return Builder.load_string(KV) + + def on_tap_card(self, *args): + datas = [data["selected"] for data in self.root.ids.card_list.data] + if True in datas and not self.selected_cards: + self.root.ids.bottom_appbar.action_items = [ + MDActionBottomAppBarButton(icon="gmail"), + MDActionBottomAppBarButton(icon="label-outline"), + MDActionBottomAppBarButton(icon="bookmark"), + ] + self.root.ids.fab_button.icon = "pencil" + self.selected_cards = True + else: + if len(list(set(datas))) == 1 and not list(set(datas))[0]: + self.selected_cards = False + if not self.selected_cards: + self.root.ids.bottom_appbar.action_items = [ + MDActionBottomAppBarButton(icon="magnify"), + MDActionBottomAppBarButton(icon="trash-can-outline"), + MDActionBottomAppBarButton(icon="download-box-outline"), + ] + self.root.ids.fab_button.icon = "plus" + + def on_start(self): + async def generate_card(): + for i in range(10): + await asynckivy.sleep(0) + self.root.ids.card_list.data.append( + { + "name": fake.name(), + "time": fake.date(), + "avatar": fake.image_url(), + "text": fake.text(), + "selected": False, + "callback": self.on_tap_card, + } + ) + + self.on_tap_card() + fake = Faker() + Clock.schedule_once(lambda x: asynckivy.start(generate_card())) + + + Test().run() + +.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/bottom-app-bar-m3-style-4.gif + :align: center + Tooltips -------- -You can add MDTooltips to the Toolbar icons by ading a text string to the toolbar item, as shown below +You can add MDTooltips to the icons by adding a text string to the bar item, +as shown below: .. code-block:: python @@ -259,7 +566,13 @@ You can add MDTooltips to the Toolbar icons by ading a text string to the toolba title: "MDTopAppBar" left_action_items: [["menu", "This is the navigation"]] right_action_items: - [["dots-vertical", lambda x: app.callback(x), "this is the More Actions"]] + [ + [ + "dots-vertical", + lambda x: app.callback(x), + "this is the More Actions" + ] + ] MDLabel: text: "Content" @@ -267,17 +580,21 @@ You can add MDTooltips to the Toolbar icons by ading a text string to the toolba ''' - class Test(MDApp): + class Example(MDApp): def build(self): + self.theme_cls.material_style = "M2" + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) def callback(self, button): Snackbar(text="Hello World").open() - Test().run() -Material design 3 style ------------------------ + Example().run() + +M3 style top app bar +-------------------- .. code-block:: python @@ -298,9 +615,10 @@ Material design 3 style ''' - class TestNavigationDrawer(MDApp): + class Example(MDApp): def build(self): - self.theme_cls.material_style = "M3" + self.theme_cls.theme_style = "Dark" + self.theme_cls.primary_palette = "Orange" return Builder.load_string(KV) def on_start(self): @@ -309,30 +627,40 @@ Material design 3 style MDTopAppBar( type_height=type_height, headline_text=f"Headline {type_height.lower()}", - md_bg_color="#2d2734", + md_bg_color="brown", left_action_items=[["arrow-left", lambda x: x]], right_action_items=[ ["attachment", lambda x: x], ["calendar", lambda x: x], ["dots-vertical", lambda x: x], ], - title="Title" if type_height == "small" else "" + title="Title" if type_height == "small" else "", + anchor_title="left", ) ) - TestNavigationDrawer().run() + Example().run() .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-m3.png :align: center """ -__all__ = ("MDTopAppBar", "MDBottomAppBar", "ActionTopAppBarButton") +from __future__ import annotations + +__all__ = ( + "MDTopAppBar", + "MDBottomAppBar", + "MDActionBottomAppBarButton", + "MDFabBottomAppBarButton", + "MDActionOverFlowButton", +) import os from math import cos, radians, sin from typing import Union +from kivy import Logger from kivy.animation import Animation from kivy.clock import Clock from kivy.core.window import Window @@ -349,13 +677,16 @@ from kivy.properties import ( ) from kivy.uix.boxlayout import BoxLayout from kivy.uix.floatlayout import FloatLayout +from kivy.uix.scrollview import ScrollView from kivymd import uix_path from kivymd.color_definitions import text_colors +from kivymd.material_resources import TOP_APP_BAR_ELEVATION from kivymd.theming import ThemableBehavior from kivymd.uix.behaviors import ( CommonElevationBehavior, DeclarativeBehavior, + RotateBehavior, ScaleBehavior, SpecificBackgroundColorBehavior, ) @@ -364,6 +695,7 @@ from kivymd.uix.controllers import WindowController from kivymd.uix.list import OneLineIconListItem from kivymd.uix.menu import MDDropdownMenu from kivymd.uix.tooltip import MDTooltip +from kivymd.utils import asynckivy from kivymd.utils.set_bars_colors import set_bars_colors with open( @@ -372,22 +704,63 @@ with open( Builder.load_string(kv_file.read()) -class ActionBottomAppBarButton(MDFloatingActionButton, ScaleBehavior): +class MDFabBottomAppBarButton( + MDFloatingActionButton, RotateBehavior, ScaleBehavior, MDTooltip +): """ - Implements a floating action button (FAB) for a toolbar with type 'bottom'. + Implements a floating action button (FAB) for a bar with type 'bottom'. + + For more information, see in the + :class:`~kivymd.uix.button.MDFloatingActionButton` and + :class:`~kivymd.uix.behaviors.RotateBehavior` and + :class:`~kivymd.uix.behaviors.ScaleBehavior` and + :class:`~kivymd.uix.tooltip.MDTooltip` + classes documentation. """ + def set__radius(self, *args) -> None: + super().set__radius() + if self.theme_cls.material_style == "M3": + self.elevation = 0 + class ActionTopAppBarButton(MDIconButton, MDTooltip): - """Implements action buttons on the toolbar.""" + """ + Implements action buttons on the bar. + + For more information, see in the + :class:`~kivymd.uix.button.MDIconButton` and + :class:`~kivymd.uix.tooltip.MDTooltip` + classes documentation. + """ # The text of the menu item of the corresponding action button that will # be displayed in the `OverFlowMenu` menu. overflow_text = StringProperty() -class ActionOverFlowButton(ActionTopAppBarButton): - """Implements a toolbar action button for the `OverFlowMenu` menu.""" +class MDActionBottomAppBarButton(ActionTopAppBarButton): + """ + Implements action buttons for a :class:'MDBottomAppBar' class. + + .. versionadded:: 1.2.0 + + For more information, see in the + :class:`~kivymd.uix.button.MDIconButton` and + :class:`~kivymd.uix.tooltip.MDTooltip` + classes documentation. + """ + + +class MDActionOverFlowButton(ActionTopAppBarButton): + """ + Implements a bar action button for the `OverFlowMenu` menu. + + For more information, see in the + :class:`~kivymd.uix.button.MDIconButton` and + :class:`~kivymd.uix.tooltip.MDTooltip` + classes documentation. + """ icon = "dots-vertical" @@ -411,7 +784,7 @@ class NotchedBox( SpecificBackgroundColorBehavior, BoxLayout, ): - elevation = NumericProperty(4) + elevation = NumericProperty(TOP_APP_BAR_ELEVATION) notch_radius = NumericProperty() notch_center_x = NumericProperty("100dp") @@ -540,7 +913,6 @@ class NotchedBox( raise Exception("Invalid value for start angle") for degree in range(start_angle, end_angle, step): - angle = radians(degree) x = center[0] + (radius * cos(angle)) y = center[1] + (radius * sin(angle)) @@ -556,6 +928,14 @@ class NotchedBox( class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): """ + Top app bar class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~NotchedBox` and + :class:`~kivymd.uix.controllers.WindowController` + classes documentation. + :Events: `on_action_button` Method for the button used for the :class:`~MDBottomAppBar` class. @@ -563,13 +943,14 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): left_action_items = ListProperty() """ - The icons on the left of the toolbar. + The icons on the left of the bar. To add one, append a list like the following: .. code-block:: kv MDTopAppBar: - left_action_items: ["dots-vertical", callback, "tooltip text", "overflow text"] + left_action_items: + ["dots-vertical", callback, "tooltip text", "overflow text"] ``icon_name`` - is a string that corresponds to an icon definition: @@ -578,9 +959,6 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): MDTopAppBar: right_action_items: [["home"]] - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-icon.png - :align: center - ``callback`` - is the function called on a touch release event and: .. code-block:: kv @@ -607,7 +985,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): ["message-reply", lambda x: app.callback(x), "Message reply"], ] - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-tooltip-text.gif + .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-tooltip-text.png :align: center ``overflow text`` - is the text for menu items (:class:`~OverFlowMenuItem`) @@ -616,18 +994,35 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): .. code-block:: kv MDTopAppBar: + use_overflow: True right_action_items: [ - ["home", lambda x: app.callback(x), "", "Home"], - ["message-star", lambda x: app.callback(x), "", "Message star"], - ["message-question", lambda x: app.callback(x), "" , "Message question"], - ["message-reply", lambda x: app.callback(x), "", "Message reply"], + ["home", lambda x: x, "", "Home"], + ["message-star", lambda x: x, "", "Message star"], + ["message-question", lambda x: x, "" , "Message question"], + ["message-reply", lambda x: x, "", "Message reply"], ] .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-overflow-text.png :align: center - Both the ``callback`` and ``tooltip text`` and ``overflow text`` are + ``icon color`` - icon color: + + .. code-block:: kv + + MDTopAppBar: + right_action_items: + [ + [ + "dots-vertical", + callback, + "tooltip text", + "overflow text", + (1, 1, 1, 1), + ] + ] + + Both the ``callback`` and ``tooltip text`` and ``overflow text`` and ``icon color`` are optional but the order must be preserved. :attr:`left_action_items` is an :class:`~kivy.properties.ListProperty` @@ -636,7 +1031,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): right_action_items = ListProperty() """ - The icons on the left of the toolbar. + The icons on the left of the bar. Works the same way as :attr:`left_action_items`. :attr:`right_action_items` is an :class:`~kivy.properties.ListProperty` @@ -645,7 +1040,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): title = StringProperty() """ - Text toolbar. + Text app bar. .. code-block:: kv @@ -755,7 +1150,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): .. code-block:: kv MDTopAppBar: - opposite_colors: True + opposite_colors: False .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-opposite-false.png :align: center @@ -763,7 +1158,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): md_bg_bottom_color = ColorProperty(None) """ - The background color in (r, g, b, a) format for the toolbar with the + The background color in (r, g, b, a) or string format for the bar with the ``bottom`` mode. .. versionadded:: 1.0.0 @@ -773,7 +1168,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): MDBottomAppBar: MDTopAppBar: - md_bg_bottom_color: 0, 1, 0, 1 + md_bg_bottom_color: "brown" icon_color: self.md_bg_bottom_color .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-md-bg-bottom-color.png @@ -786,11 +1181,11 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): set_bars_color = BooleanProperty(False) """ If `True` the background color of the bar status will be set automatically - according to the current color of the toolbar. + according to the current color of the bar. .. versionadded:: 1.0.0 - See `set_bars_colors ` + See `set_bars_colors `_ for more information. :attr:`set_bars_color` is an :class:`~kivy.properties.BooleanProperty` @@ -811,15 +1206,12 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): use_overflow: True right_action_items: [ - ["home", lambda x: app.callback(x), "Home", "Home"], - ["message-star", lambda x: app.callback(x), "Message star", "Message star"], - ["message-question", lambda x: app.callback(x), "Message question", "Message question"], - ["message-reply", lambda x: app.callback(x), "Message reply", "Message reply"], + ["home", lambda x: x, "Home", "Home"], + ["message-star", lambda x: x, "Message star", "Message star"], + ["message-question", lambda x: x, "Message question", "Message question"], + ["message-reply", lambda x: x, "Message reply", "Message reply"], ] - .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/toolbar-use-overflow.gif - :align: center - :attr:`use_overflow` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ @@ -852,10 +1244,10 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): overflow_cls: CustomOverFlowMenu() right_action_items: [ - ["home", lambda x: app.callback(x), "Home", "Home"], - ["message-star", lambda x: app.callback(x), "Message star", "Message star"], - ["message-question", lambda x: app.callback(x), "Message question", "Message question"], - ["message-reply", lambda x: app.callback(x), "Message reply", "Message reply"], + ["home", lambda x: x, "Home", "Home"], + ["message-star", lambda x: x, "Message star", "Message star"], + ["message-question", lambda x: x, "Message question", "Message question"], + ["message-reply", lambda x: x, "Message reply", "Message reply"], ] MDLabel: @@ -869,7 +1261,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): pass - class Test(MDApp): + class Example(MDApp): def build(self): return Builder.load_string(KV) @@ -877,7 +1269,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): print(instance_action_top_appbar_button) - Test().run() + Example().run() :attr:`overflow_cls` is an :class:`~kivy.properties.ObjectProperty` and defaults to `None`. @@ -895,7 +1287,8 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): icon_color = ColorProperty() """ - Color action button. Only for :class:`~MDBottomAppBar` class. + Color in (r, g, b, a) or string format action button. Only for + :class:`~MDBottomAppBar` class. :attr:`icon_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[]`. @@ -905,7 +1298,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): anchor_title = OptionProperty(None, options=["left", "center", "right"]) """ - Position toolbar title. Only used with `material_style = 'M3'` + Position bar title. Only used with `material_style = 'M3'` Available options are: `'left'`, `'center'`, `'right'`. :attr:`anchor_title` is an :class:`~kivy.properties.OptionProperty` @@ -914,7 +1307,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): headline_text = StringProperty() """ - Headline text toolbar. + Headline text bar. .. versionadded:: 1.0.0 @@ -924,7 +1317,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): headline_text_color = ColorProperty(None) """ - Headline text color. + Headline text color in (r, g, b, a) or string format. .. versionadded:: 1.0.0 @@ -934,11 +1327,11 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): type_height = OptionProperty("small", options=["medium", "large", "small"]) """ - Toolbar height type. + Bar height type. .. versionadded:: 1.0.0 - Available options are: 'small', 'large', 'small'. + Available options are: 'medium', 'large', 'small'. :attr:`type_height` is an :class:`~kivy.properties.OptionProperty` and defaults to `'small'`. @@ -951,7 +1344,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): _overflow_menu_items = [] def __init__(self, **kwargs): - self.action_button = ActionBottomAppBarButton() + self.action_button = MDFabBottomAppBarButton() super().__init__(**kwargs) self.register_event_type("on_action_button") @@ -987,7 +1380,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): def on_width(self, instance_toolbar, width: float) -> None: """ - Called when the toolbar is resized (size of the application window). + Called when the bar is resized (size of the application window). """ if self.mode == "center": @@ -1022,7 +1415,7 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): self.remove_overflow_button() def remove_overflow_button(self) -> None: - """Removes an overflow button to the toolbar.""" + """Removes an overflow button to the bar.""" if self.overflow_action_button_is_added(): action_overflow_button = self.ids.right_actions.children[0] @@ -1030,10 +1423,10 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): self._overflow_menu_items = [] def add_overflow_button(self) -> None: - """Adds an overflow button to the toolbar.""" + """Adds an overflow button to the bar.""" self.ids.right_actions.add_widget( - ActionOverFlowButton( + MDActionOverFlowButton( theme_text_color="Custom" if not self.opposite_colors else "Primary", @@ -1046,19 +1439,19 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): def overflow_action_button_is_added(self) -> bool: """ Returns `True` if at least one action button - (:class:`~ActionTopAppBarButton') on the toolbar is added to the + (:class:`~ActionTopAppBarButton') on the bar is added to the overflow. """ if ( not self.ids.right_actions.children[0].__class__ - is ActionOverFlowButton + is MDActionOverFlowButton ): return False return True def add_action_button_to_overflow(self): - """Adds an overflow button to the toolbar.""" + """Adds an overflow button to the bar.""" if len(self.ids.right_actions.children) > 1: button_to_be_added = self.ids.right_actions.children[1] @@ -1318,21 +1711,28 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): if len(item) > 1 and not item[1]: item[1] = lambda x: None if len(item) == 2: - if type(item[1]) is str: + if isinstance(item[1], str) or isinstance(item[1], tuple): item.insert(1, lambda x: None) else: item.append("") + if len(item) == 3: + if isinstance(item[2], tuple): + item.insert(2, "") instance_box_layout.add_widget( ActionTopAppBarButton( icon=item[0], on_release=item[1], tooltip_text=item[2], - overflow_text=item[3] if len(item) == 4 else "", + overflow_text=item[3] + if (len(item) == 4 and isinstance(item[3], str)) + else "", theme_text_color="Custom" if not self.opposite_colors else "Primary", - text_color=self.specific_text_color, + text_color=self.specific_text_color + if not (len(item) == 4 and isinstance(item[3], tuple)) + else item[3], opposite_colors=self.opposite_colors, ) ) @@ -1366,20 +1766,460 @@ class MDTopAppBar(DeclarativeBehavior, NotchedBox, WindowController): ][self.theme_cls.primary_hue] -class MDBottomAppBar(DeclarativeBehavior, FloatLayout): +class MDBottomAppBar( + DeclarativeBehavior, + ThemableBehavior, + SpecificBackgroundColorBehavior, + CommonElevationBehavior, + FloatLayout, +): + """ + Bottom app bar class. + + For more information, see in the + :class:`~kivymd.uix.behaviors.DeclarativeBehavior` and + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivymd.uix.behaviors.SpecificBackgroundColorBehavior` and + :class:`~kivymd.uix.behaviors.CommonElevationBehavior` and + :class:`~kivy.uix.floatlayout.FloatLayout` + classes documentation. + + :Events: + `on_show_bar` + The method is called when the :class:`~MDBottomAppBar` panel + is shown. + `on_hide_bar` + The method is called when the :class:`~MDBottomAppBar` panel + is hidden. + """ + md_bg_color = ColorProperty([0, 0, 0, 0]) """ - Color toolbar. + Color bar in (r, g, b, a) or string format. :attr:`md_bg_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0]`. """ + icon_color = ColorProperty(None) + """ + Color bar in (r, g, b, a) or string format. + + .. versionadded:: 1.2.0 + + :attr:`icon_color` is an :class:`~kivy.properties.ColorProperty` + and defaults to `None`. + """ + + action_items = ListProperty() + """ + The icons on the left bar. + + .. versionadded:: 1.2.0 + + :attr:`action_items` is an :class:`~kivy.properties.ListProperty` + and defaults to `[]`. + """ + + animation = BooleanProperty(True) + """ + # TODO: add description. + # FIXME: changing the value does not affect anything. + + .. versionadded:: 1.2.0 + + :attr:`animation` is an :class:`~kivy.properties.BooleanProperty` + and defaults to `True`. + """ + + show_transition = StringProperty("linear") + """ + Type of button display transition. + + .. versionadded:: 1.2.0 + + :attr:`show_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'linear'`. + """ + + hide_transition = StringProperty("in_back") + """ + Type of button hidden transition. + + .. versionadded:: 1.2.0 + + :attr:`hide_transition` is a :class:`~kivy.properties.StringProperty` + and defaults to `'in_back'`. + """ + + hide_duration = NumericProperty(0.4) + """ + Duration of button hidden transition. + + .. versionadded:: 1.2.0 + + :attr:`hide_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + show_duration = NumericProperty(0.2) + """ + Duration of button display transition. + + .. versionadded:: 1.2.0 + + :attr:`show_duration` is a :class:`~kivy.properties.NumericProperty` + and defaults to `0.2`. + """ + + scroll_cls = ObjectProperty() + """ + Widget inherited from the :class:`~kivy.uix.scrollview.ScrollView` class. + The value must be set if the :attr:`allow_hidden` parameter is `True`. + + .. versionadded:: 1.2.0 + + :attr:`scroll_cls` is a :class:`~kivy.properties.ObjectProperty` + and defaults to `None`. + """ + + allow_hidden = BooleanProperty(False) + """ + Allows or disables hiding the panel when scrolling content. + If the value is `True`, the :attr:`scroll_cls` parameter must be specified. + + .. versionadded:: 1.2.0 + + :attr:`allow_hidden` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + bar_is_hidden = BooleanProperty(False) + """ + Is the panel currently hidden. + + .. versionadded:: 1.2.0 + + :attr:`bar_is_hidden` is a :class:`~kivy.properties.BooleanProperty` + and defaults to `False`. + """ + + _padding = dp(16) + _x = -dp(48) + _scroll_cls_y = 0 + _cache = [] + _current_data = [] + _wait_removed = False + _animated_hidden = True + _animated_show = True + _fab_bottom_app_bar_button = None + _action_overflow_button = None + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.size_hint_y = None + if self.theme_cls.material_style == "M3": + self.register_event_type("on_show_bar") + self.register_event_type("on_hide_bar") + + self.height = dp(80) + Clock.schedule_once(self.set_bg_color) + + def button_centering_animation( + self, + button: MDActionOverFlowButton + | MDActionBottomAppBarButton + | MDFabBottomAppBarButton, + ) -> None: + """ + Animation of centering buttons for + :class:`~MDActionOverFlowButton`, + :class:`~MDActionBottomAppBarButton` and + :class:`~MDFabBottomAppBarButton` classes. + """ + + if self.animation: + Animation( + y=self.height / 2 - dp(48) / 2, + opacity=1, + d=self.show_duration, + t=self.show_transition, + ).start(button) + + def check_scroll_direction(self, scroll_cls, y: float) -> None: + """ + Checks the scrolling direction. + Depending on the scrolling direction, hides or shows the + :class:`~MDBottomAppBar` panel. + """ + + if round(y, 1) < self._scroll_cls_y and not self.bar_is_hidden: + self.hide_bar() + if round(y, 1) > self._scroll_cls_y and self.bar_is_hidden: + self.show_bar() + + self._scroll_cls_y = round(y, 1) + + def show_bar(self) -> None: + """Show :class:`~MDBottomAppBar` panel.""" + + def on_complete(*args): + self.dispatch("on_show_bar") + + def on_progress(animation, instance, progress): + if progress > 0.5 and self._animated_show: + self._animated_show = False + for i, widget in enumerate(self.children): + if isinstance(widget, MDActionBottomAppBarButton): + anim_icon = Animation( + y=self.height / 2 - dp(48) / 2, + d=self.show_duration, + t=self.show_transition, + ) + Clock.schedule_once( + lambda x, y=widget: anim_icon.start(y), + i / 10, + ) + if self._fab_bottom_app_bar_button: + Animation( + y=self._fab_bottom_app_bar_button.y + dp(4), + d=self.show_duration, + t=self.show_transition, + ).start(self._fab_bottom_app_bar_button) + + self.bar_is_hidden = False + self._animated_show = True + anim = Animation( + y=0, + d=self.show_duration, + t=self.show_transition, + ) + anim.bind(on_progress=on_progress, on_complete=on_complete) + anim.start(self) + + def hide_bar(self) -> None: + """Hide :class:`~MDBottomAppBar` panel.""" + + def on_complete(*args): + self.dispatch("on_hide_bar") + + def on_progress(animation, instance, progress): + if ( + progress > 0.5 + and self._animated_hidden + and widget_icon == instance.icon + ): + self._animated_hidden = False + anim_bar = Animation( + y=-self.height, + d=self.hide_duration, + # t=self.hide_transition, + ) + anim_bar.bind(on_complete=on_complete) + anim_bar.start(self) + + if self._fab_bottom_app_bar_button: + Animation( + y=self._fab_bottom_app_bar_button.y - dp(4), + d=self.hide_duration, + t=self.hide_transition, + ).start(self._fab_bottom_app_bar_button) + + self.bar_is_hidden = True + self._animated_hidden = True + len_children = len(self.children) + widget_icon = "" + + for i, widget in enumerate(self.children): + if isinstance(widget, MDActionBottomAppBarButton): + anim = Animation( + y=-widget.height, + d=self.hide_duration, + t=self.hide_transition, + ) + if i + 2 == len_children: + widget_icon = widget.icon + anim.bind(on_progress=on_progress) + Clock.schedule_once( + lambda x, y=widget: anim.start(y), + i / 10, + ) + + def on_show_bar(self, *args) -> None: + """ + The method is called when the :class:`~MDBottomAppBar` panel + is shown. + """ + + def on_hide_bar(self, *args) -> None: + """ + The method is called when the :class:`~MDBottomAppBar` panel + is hidden. + """ + + def on_scroll_cls(self, instance, scroll_cls) -> None: + """ + Called when the value of the :attr:`scroll_cls` attribute changes. + """ + + def on_scroll_cls(*args): + if not self.allow_hidden: + Logger.warning( + "KivyMD: " + "In order for the bottom bar to be automatically hidden " + "in addition to the `scroll_cls` parameter, set the value " + "of the `allow_hidden` parameter to `True`" + ) + + if issubclass(scroll_cls.__class__, ScrollView): + if self.allow_hidden: + scroll_cls.bind(scroll_y=self.check_scroll_direction) + else: + raise TypeError( + f"The `scroll_cls` parameter must be an object inherited from " + f"the {ScrollView} class" + ) + + if self.theme_cls.material_style == "M3": + Clock.schedule_once(on_scroll_cls) + + def on_size(self, *args) -> None: + """Called when the root screen is resized.""" + + if ( + self._fab_bottom_app_bar_button + and self.theme_cls.material_style == "M3" + ): + self._fab_bottom_app_bar_button.x = Window.width - (dp(56) + dp(16)) + + def on_action_items(self, instance, value: list) -> None: + """ + Called when the value of the :attr:`action_items` attribute changes. + """ + + if self.theme_cls.material_style == "M2": + return + + def wait_removed(*args): + if len(self.children) == 1 or not self.children: + Clock.unschedule(wait_removed) + self._wait_removed = False + self._x = -dp(48) + asynckivy.start(add_widget()) + + async def add_widget(): + for button in value: + await asynckivy.sleep(0) + self.add_widget(button) + + if self._cache: + self._cache.append(value) + + for data in self._cache: + if value[0] in data: + for i, widget in enumerate(self.children): + if not self._wait_removed: + Clock.schedule_interval(wait_removed, 0) + self._wait_removed = True + if isinstance(widget, MDActionBottomAppBarButton): + anim = Animation( + y=-widget.height, + d=self.hide_duration, + t=self.hide_transition, + ) + anim.bind( + on_complete=lambda x, y=widget: self.remove_widget( + y + ) + ) + Clock.schedule_once( + lambda x, y=widget: anim.start(y), + i / 10, + ) + else: + self._cache.append(value) + self._current_data = value + asynckivy.start(add_widget()) + + def set_fab_opacity(self, *ars) -> None: + """ + Sets the transparency value of the:class:`~MDFabBottomAppBarButton` + button. + """ + + self._fab_bottom_app_bar_button.ids.lbl_ic.opacity = 1 + + def set_fab_icon(self, instance, value) -> None: + """ + Animates the size of the :class:`~MDFabBottomAppBarButton` button. + """ + + self._fab_bottom_app_bar_button.ids.lbl_ic.opacity = 0 + anim = Animation( + scale_value_x=0, + scale_value_y=0, + opacity=0, + d=self.hide_duration, + t=self.hide_transition, + ) + Animation( + scale_value_x=1, + scale_value_y=1, + opacity=1, + d=self.show_duration, + t=self.show_transition, + ) + anim.bind(on_complete=self.set_fab_opacity) + anim.start(instance) + + def set_bg_color(self, *args) -> None: + """ + Sets the background color for the :class:`~MDBottomAppBar` class. + """ + + if self.md_bg_color == [0, 0, 0, 0]: + self.md_bg_color = self.theme_cls.primary_color + + def set_icon_color( + self, widget: MDActionOverFlowButton | MDActionBottomAppBarButton + ) -> None: + """ + Sets the icon color for the :class:`~MDActionOverFlowButton` and + :class:`~MDActionBottomAppBarButton` classes. + """ + + if self.icon_color: + widget.theme_icon_color = "Custom" + widget.icon_color = self.icon_color def add_widget(self, widget, index=0, canvas=None): - if isinstance(widget, MDTopAppBar): + # For M2 style. + if ( + isinstance(widget, MDTopAppBar) + and self.theme_cls.material_style == "M2" + ): super().add_widget(widget) + widget.elevation = 0 return super().add_widget(widget.action_button) + # For M3 style. + if self.theme_cls.material_style == "M3": + if isinstance(widget, MDActionBottomAppBarButton): + self._x += widget.width + widget.pos = ( + self._x + self._padding, + -dp(48) if self.animation else self.height / 2 - dp(48) / 2, + ) + widget.opacity = int(not self.animation) + self.set_icon_color(widget) + super().add_widget(widget) + self.button_centering_animation(widget) + elif isinstance(widget, MDFabBottomAppBarButton): + widget.bind(icon=self.set_fab_icon) + self._fab_bottom_app_bar_button = widget + Clock.schedule_once(self.set_fab_opacity) + widget.scale_value_x = int(not self.animation) + widget.scale_value_y = int(not self.animation) + widget.pos = ( + Window.width - (dp(56) + self._padding), + self.height / 2 - dp(56) / 2, + ) + super().add_widget(widget) diff --git a/sbapp/kivymd/uix/tooltip/tooltip.py b/sbapp/kivymd/uix/tooltip/tooltip.py index 855119a..ba6b5da 100644 --- a/sbapp/kivymd/uix/tooltip/tooltip.py +++ b/sbapp/kivymd/uix/tooltip/tooltip.py @@ -97,9 +97,19 @@ with open( class MDTooltip(ThemableBehavior, HoverBehavior, TouchBehavior): + """ + Tooltip class. + + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior and + :class:`~kivymd.uix.behaviors.HoverBehavior` and + :class:`~kivymd.uix.behaviors.TouchBehavior` + classes documentation. + """ + tooltip_bg_color = ColorProperty(None) """ - Tooltip background color in ``rgba`` format. + Tooltip background color in (r, g, b, a) or string format :attr:`tooltip_bg_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -107,7 +117,7 @@ class MDTooltip(ThemableBehavior, HoverBehavior, TouchBehavior): tooltip_text_color = ColorProperty(None) """ - Tooltip text color in ``rgba`` format. + Tooltip text color in (r, g, b, a) or string format :attr:`tooltip_text_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. @@ -330,6 +340,15 @@ class MDTooltip(ThemableBehavior, HoverBehavior, TouchBehavior): class MDTooltipViewClass(ThemableBehavior, BoxLayout): + """ + Tooltip view class. + + For more information, see in the + :class:`~kivymd.theming.ThemableBehavior` and + :class:`~kivy.uix.boxlayout.BoxLayout` + classes documentation. + """ + tooltip_bg_color = ColorProperty(None) """ See :attr:`~MDTooltip.tooltip_bg_color`. diff --git a/sbapp/kivymd/uix/transition/transition.py b/sbapp/kivymd/uix/transition/transition.py index 169584d..e905ea6 100644 --- a/sbapp/kivymd/uix/transition/transition.py +++ b/sbapp/kivymd/uix/transition/transition.py @@ -142,7 +142,6 @@ class MDTransitionBase(TransitionBase): and self.manager.current_heroes and self.screen_out.heroes_to ): - for heroes_tag in self.manager.current_heroes: for hero_to_widget in self.screen_out.heroes_to: if hero_to_widget.tag == heroes_tag: diff --git a/sbapp/kivymd/uix/widget.py b/sbapp/kivymd/uix/widget.py index 044722b..27c1371 100644 --- a/sbapp/kivymd/uix/widget.py +++ b/sbapp/kivymd/uix/widget.py @@ -38,11 +38,12 @@ __all__ = ("MDWidget",) from kivy.uix.widget import Widget +from kivymd.theming import ThemableBehavior from kivymd.uix import MDAdaptiveWidget from kivymd.uix.behaviors import DeclarativeBehavior -class MDWidget(DeclarativeBehavior, MDAdaptiveWidget, Widget): +class MDWidget(DeclarativeBehavior, ThemableBehavior, MDAdaptiveWidget, Widget): """ See :class:`~kivy.uix.Widget` class documentation for more information. diff --git a/sbapp/kivymd/utils/fpsmonitor.py b/sbapp/kivymd/utils/fpsmonitor.py index 2284ba8..4fbc2fc 100644 --- a/sbapp/kivymd/utils/fpsmonitor.py +++ b/sbapp/kivymd/utils/fpsmonitor.py @@ -11,7 +11,7 @@ application : from kivy.clock import Clock from kivy.lang import Builder -from kivy.properties import NumericProperty, StringProperty +from kivy.properties import NumericProperty, StringProperty, OptionProperty from kivy.uix.label import Label Builder.load_string( @@ -20,7 +20,7 @@ Builder.load_string( size_hint_y: None height: self.texture_size[1] text: root._fsp_value - pos_hint: {"top": 1} + pos_hint: {root.anchor: 1} canvas.before: Color: @@ -36,6 +36,9 @@ class FpsMonitor(Label): updated_interval = NumericProperty(0.5) """FPS refresh rate.""" + anchor = OptionProperty("top", options=["top", "bottom"]) + """Monitor position.""" + _fsp_value = StringProperty() def start(self): diff --git a/sbapp/main.py b/sbapp/main.py index abd84a0..7dc1891 100644 --- a/sbapp/main.py +++ b/sbapp/main.py @@ -1,6 +1,6 @@ __debug_build__ = False -__disable_shaders__ = True -__version__ = "0.5.2" +__disable_shaders__ = False +__version__ = "0.5.3" __variant__ = "beta" import sys diff --git a/sbapp/patches/AndroidManifest.tmpl.xml b/sbapp/patches/AndroidManifest.tmpl.xml index c29b290..5a4f128 100644 --- a/sbapp/patches/AndroidManifest.tmpl.xml +++ b/sbapp/patches/AndroidManifest.tmpl.xml @@ -31,11 +31,7 @@ {% for perm in args.permissions %} - {% if '.' in perm %} - - {% else %} - - {% endif %} + {% endfor %} {% if args.wakelock %} diff --git a/sbapp/patches/p4a_build.py b/sbapp/patches/p4a_build.py new file mode 100644 index 0000000..66f1d9c --- /dev/null +++ b/sbapp/patches/p4a_build.py @@ -0,0 +1,1064 @@ +#!/usr/bin/env python3 + +from gzip import GzipFile +import hashlib +import json +from os.path import ( + dirname, join, isfile, realpath, + relpath, split, exists, basename +) +from os import environ, listdir, makedirs, remove +import os +import shlex +import shutil +import subprocess +import sys +import tarfile +import tempfile +import time + +from distutils.version import LooseVersion +from fnmatch import fnmatch +import jinja2 + + +def get_dist_info_for(key, error_if_missing=True): + try: + with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh: + info = json.load(fileh) + value = info[key] + except (OSError, KeyError) as e: + if not error_if_missing: + return None + print("BUILD FAILURE: Couldn't extract the key `" + key + "` " + + "from dist_info.json: " + str(e)) + sys.exit(1) + return value + + +def get_hostpython(): + return get_dist_info_for('hostpython') + + +def get_bootstrap_name(): + return get_dist_info_for('bootstrap') + + +if os.name == 'nt': + ANDROID = 'android.bat' + ANT = 'ant.bat' +else: + ANDROID = 'android' + ANT = 'ant' + +curdir = dirname(__file__) + +BLACKLIST_PATTERNS = [ + # code versionning + '^*.hg/*', + '^*.git/*', + '^*.bzr/*', + '^*.svn/*', + + # temp files + '~', + '*.bak', + '*.swp', + + # Android artifacts + '*.apk', + '*.aab', +] + +WHITELIST_PATTERNS = [] + +if os.environ.get("P4A_BUILD_IS_RUNNING_UNITTESTS", "0") != "1": + PYTHON = get_hostpython() + _bootstrap_name = get_bootstrap_name() +else: + PYTHON = "python3" + _bootstrap_name = "sdl2" + +if PYTHON is not None and not exists(PYTHON): + PYTHON = None + +if _bootstrap_name in ('sdl2', 'webview', 'service_only'): + WHITELIST_PATTERNS.append('pyconfig.h') + +environment = jinja2.Environment(loader=jinja2.FileSystemLoader( + join(curdir, 'templates'))) + + +DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS = 'org.kivy.android.PythonActivity' +DEFAULT_PYTHON_SERVICE_JAVA_CLASS = 'org.kivy.android.PythonService' + + +def ensure_dir(path): + if not exists(path): + makedirs(path) + + +def render(template, dest, **kwargs): + '''Using jinja2, render `template` to the filename `dest`, supplying the + + keyword arguments as template parameters. + ''' + + dest_dir = dirname(dest) + if dest_dir and not exists(dest_dir): + makedirs(dest_dir) + + template = environment.get_template(template) + text = template.render(**kwargs) + + f = open(dest, 'wb') + f.write(text.encode('utf-8')) + f.close() + + +def is_whitelist(name): + return match_filename(WHITELIST_PATTERNS, name) + + +def is_blacklist(name): + if is_whitelist(name): + return False + return match_filename(BLACKLIST_PATTERNS, name) + + +def match_filename(pattern_list, name): + for pattern in pattern_list: + if pattern.startswith('^'): + pattern = pattern[1:] + else: + pattern = '*/' + pattern + if fnmatch(name, pattern): + return True + + +def listfiles(d): + basedir = d + subdirlist = [] + for item in os.listdir(d): + fn = join(d, item) + if isfile(fn): + yield fn + else: + subdirlist.append(join(basedir, item)) + for subdir in subdirlist: + for fn in listfiles(subdir): + yield fn + + +def make_tar(tfn, source_dirs, byte_compile_python=False, optimize_python=True): + ''' + Make a zip file `fn` from the contents of source_dis. + ''' + + def clean(tinfo): + """cleaning function (for reproducible builds)""" + tinfo.uid = tinfo.gid = 0 + tinfo.uname = tinfo.gname = '' + tinfo.mtime = 0 + return tinfo + + # get the files and relpath file of all the directory we asked for + files = [] + for sd in source_dirs: + sd = realpath(sd) + for fn in listfiles(sd): + if is_blacklist(fn): + continue + if fn.endswith('.py') and byte_compile_python: + fn = compile_py_file(fn, optimize_python=optimize_python) + files.append((fn, relpath(realpath(fn), sd))) + files.sort() # deterministic + + # create tar.gz of thoses files + gf = GzipFile(tfn, 'wb', mtime=0) # deterministic + tf = tarfile.open(None, 'w', gf, format=tarfile.USTAR_FORMAT) + dirs = [] + for fn, afn in files: + dn = dirname(afn) + if dn not in dirs: + # create every dirs first if not exist yet + d = '' + for component in split(dn): + d = join(d, component) + if d.startswith('/'): + d = d[1:] + if d == '' or d in dirs: + continue + dirs.append(d) + tinfo = tarfile.TarInfo(d) + tinfo.type = tarfile.DIRTYPE + clean(tinfo) + tf.addfile(tinfo) + + # put the file + tf.add(fn, afn, filter=clean) + tf.close() + gf.close() + + +def compile_py_file(python_file, optimize_python=True): + ''' + Compile python_file to *.pyc and return the filename of the *.pyc file. + ''' + + if PYTHON is None: + return + + args = [PYTHON, '-m', 'compileall', '-b', '-f', python_file] + if optimize_python: + # -OO = strip docstrings + args.insert(1, '-OO') + return_code = subprocess.call(args) + + if return_code != 0: + print('Error while running "{}"'.format(' '.join(args))) + print('This probably means one of your Python files has a syntax ' + 'error, see logs above') + exit(1) + + return ".".join([os.path.splitext(python_file)[0], "pyc"]) + + +def make_package(args): + # If no launcher is specified, require a main.py/main.pyc: + if (get_bootstrap_name() != "sdl" or args.launcher is None) and \ + get_bootstrap_name() not in ["webview", "service_library"]: + # (webview doesn't need an entrypoint, apparently) + if args.private is None or ( + not exists(join(realpath(args.private), 'main.py')) and + not exists(join(realpath(args.private), 'main.pyc'))): + print('''BUILD FAILURE: No main.py(c) found in your app directory. This +file must exist to act as the entry point for you app. If your app is +started by a file with a different name, rename it to main.py or add a +main.py that loads it.''') + sys.exit(1) + + assets_dir = "src/main/assets" + + # Delete the old assets. + shutil.rmtree(assets_dir, ignore_errors=True) + ensure_dir(assets_dir) + + # Add extra environment variable file into tar-able directory: + env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-") + with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f: + if hasattr(args, "window"): + f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n") + if hasattr(args, "sdl_orientation_hint"): + f.write("KIVY_ORIENTATION=" + str(args.sdl_orientation_hint) + "\n") + f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n") + f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n") + + # Package up the private data (public not supported). + use_setup_py = get_dist_info_for("use_setup_py", + error_if_missing=False) is True + private_tar_dirs = [env_vars_tarpath] + _temp_dirs_to_clean = [] + try: + if args.private: + if not use_setup_py or ( + not exists(join(args.private, "setup.py")) and + not exists(join(args.private, "pyproject.toml")) + ): + print('No setup.py/pyproject.toml used, copying ' + 'full private data into .apk.') + private_tar_dirs.append(args.private) + else: + print("Copying main.py's ONLY, since other app data is " + "expected in site-packages.") + main_py_only_dir = tempfile.mkdtemp() + _temp_dirs_to_clean.append(main_py_only_dir) + + # Check all main.py files we need to copy: + copy_paths = ["main.py", join("service", "main.py")] + for copy_path in copy_paths: + variants = [ + copy_path, + copy_path.partition(".")[0] + ".pyc", + ] + # Check in all variants with all possible endings: + for variant in variants: + if exists(join(args.private, variant)): + # Make sure surrounding directly exists: + dir_path = os.path.dirname(variant) + if (len(dir_path) > 0 and + not exists( + join(main_py_only_dir, dir_path) + )): + os.mkdir(join(main_py_only_dir, dir_path)) + # Copy actual file: + shutil.copyfile( + join(args.private, variant), + join(main_py_only_dir, variant), + ) + + # Append directory with all main.py's to result apk paths: + private_tar_dirs.append(main_py_only_dir) + if get_bootstrap_name() == "webview": + for asset in listdir('webview_includes'): + shutil.copy(join('webview_includes', asset), join(assets_dir, asset)) + + for asset in args.assets: + asset_src, asset_dest = asset.split(":") + if isfile(realpath(asset_src)): + ensure_dir(dirname(join(assets_dir, asset_dest))) + shutil.copy(realpath(asset_src), join(assets_dir, asset_dest)) + else: + shutil.copytree(realpath(asset_src), join(assets_dir, asset_dest)) + + if args.private or args.launcher: + for arch in get_dist_info_for("archs"): + libs_dir = f"libs/{arch}" + make_tar( + join(libs_dir, "libpybundle.so"), + [f"_python_bundle__{arch}"], + byte_compile_python=args.byte_compile_python, + optimize_python=args.optimize_python, + ) + make_tar( + join(assets_dir, "private.tar"), + private_tar_dirs, + byte_compile_python=args.byte_compile_python, + optimize_python=args.optimize_python, + ) + finally: + for directory in _temp_dirs_to_clean: + shutil.rmtree(directory) + + # Remove extra env vars tar-able directory: + shutil.rmtree(env_vars_tarpath) + + # Prepare some variables for templating process + res_dir = "src/main/res" + res_dir_initial = "src/res_initial" + # make res_dir stateless + if exists(res_dir_initial): + pass + #shutil.rmtree(res_dir, ignore_errors=True) + shutil.copytree(res_dir_initial, res_dir, dirs_exist_ok=True) + else: + shutil.copytree(res_dir, res_dir_initial) + + # Add user resouces + for resource in args.resources: + resource_src, resource_dest = resource.split(":") + if isfile(realpath(resource_src)): + ensure_dir(dirname(join(res_dir, resource_dest))) + shutil.copy(realpath(resource_src), join(res_dir, resource_dest)) + else: + shutil.copytree(realpath(resource_src), + join(res_dir, resource_dest), dirs_exist_ok=True) + + default_icon = 'templates/kivy-icon.png' + default_presplash = 'templates/kivy-presplash.jpg' + shutil.copy( + args.icon or default_icon, + join(res_dir, 'mipmap/icon.png') + ) + if args.icon_fg and args.icon_bg: + shutil.copy(args.icon_fg, join(res_dir, 'mipmap/icon_foreground.png')) + shutil.copy(args.icon_bg, join(res_dir, 'mipmap/icon_background.png')) + with open(join(res_dir, 'mipmap-anydpi-v26/icon.xml'), "w") as fd: + fd.write(""" + + + + +""") + elif args.icon_fg or args.icon_bg: + print("WARNING: Received an --icon_fg or an --icon_bg argument, but not both. " + "Ignoring.") + + if get_bootstrap_name() != "service_only": + lottie_splashscreen = join(res_dir, 'raw/splashscreen.json') + if args.presplash_lottie: + shutil.copy( + 'templates/lottie.xml', + join(res_dir, 'layout/lottie.xml') + ) + ensure_dir(join(res_dir, 'raw')) + shutil.copy( + args.presplash_lottie, + join(res_dir, 'raw/splashscreen.json') + ) + else: + if exists(lottie_splashscreen): + remove(lottie_splashscreen) + remove(join(res_dir, 'layout/lottie.xml')) + + shutil.copy( + args.presplash or default_presplash, + join(res_dir, 'drawable/presplash.jpg') + ) + + # If extra Java jars were requested, copy them into the libs directory + jars = [] + if args.add_jar: + for jarname in args.add_jar: + if not exists(jarname): + print('Requested jar does not exist: {}'.format(jarname)) + sys.exit(-1) + shutil.copy(jarname, 'src/main/libs') + jars.append(basename(jarname)) + + # If extra aar were requested, copy them into the libs directory + aars = [] + if args.add_aar: + ensure_dir("libs") + for aarname in args.add_aar: + if not exists(aarname): + print('Requested aar does not exists: {}'.format(aarname)) + sys.exit(-1) + shutil.copy(aarname, 'libs') + aars.append(basename(aarname).rsplit('.', 1)[0]) + + versioned_name = (args.name.replace(' ', '').replace('\'', '') + + '-' + args.version) + + version_code = 0 + if not args.numeric_version: + """ + Set version code in format (10 + minsdk + app_version) + Historically versioning was (arch + minsdk + app_version), + with arch expressed with a single digit from 6 to 9. + Since the multi-arch support, has been changed to 10. + """ + min_sdk = args.min_sdk_version + for i in args.version.split('.'): + version_code *= 100 + version_code += int(i) + args.numeric_version = "{}{}{}".format("10", min_sdk, version_code) + + if args.intent_filters: + with open(args.intent_filters) as fd: + args.intent_filters = fd.read() + + if not args.add_activity: + args.add_activity = [] + + if not args.activity_launch_mode: + args.activity_launch_mode = '' + + if args.extra_source_dirs: + esd = [] + for spec in args.extra_source_dirs: + if ':' in spec: + specdir, specincludes = spec.split(':') + print('WARNING: Currently gradle builds only support including source ' + 'directories, so when building using gradle all files in ' + '{} will be included.'.format(specdir)) + else: + specdir = spec + specincludes = '**' + esd.append((realpath(specdir), specincludes)) + args.extra_source_dirs = esd + else: + args.extra_source_dirs = [] + + service = False + if args.private: + service_main = join(realpath(args.private), 'service', 'main.py') + if exists(service_main) or exists(service_main + 'o'): + service = True + + service_names = [] + base_service_class = args.service_class_name.split('.')[-1] + for sid, spec in enumerate(args.services): + spec = spec.split(':') + name = spec[0] + entrypoint = spec[1] + options = spec[2:] + + foreground = 'foreground' in options + sticky = 'sticky' in options + + service_names.append(name) + service_target_path =\ + 'src/main/java/{}/Service{}.java'.format( + args.package.replace(".", "/"), + name.capitalize() + ) + render( + 'Service.tmpl.java', + service_target_path, + name=name, + entrypoint=entrypoint, + args=args, + foreground=foreground, + sticky=sticky, + service_id=sid + 1, + base_service_class=base_service_class, + ) + + # Find the SDK directory and target API + with open('project.properties', 'r') as fileh: + target = fileh.read().strip() + android_api = target.split('-')[1] + + if android_api.isdigit(): + android_api = int(android_api) + else: + raise ValueError( + "failed to extract the Android API level from " + + "build.properties. expected int, got: '" + + str(android_api) + "'" + ) + + with open('local.properties', 'r') as fileh: + sdk_dir = fileh.read().strip() + sdk_dir = sdk_dir[8:] + + # Try to build with the newest available build tools + ignored = {".DS_Store", ".ds_store"} + build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored] + build_tools_versions = sorted(build_tools_versions, + key=LooseVersion) + build_tools_version = build_tools_versions[-1] + + # Folder name for launcher (used by SDL2 bootstrap) + url_scheme = 'kivy' + + # Copy backup rules file if specified and update the argument + res_xml_dir = join(res_dir, 'xml') + if args.backup_rules: + ensure_dir(res_xml_dir) + shutil.copy(join(args.private, args.backup_rules), res_xml_dir) + args.backup_rules = split(args.backup_rules)[1][:-4] + + # Copy res_xml files to src/main/res/xml + if args.res_xmls: + ensure_dir(res_xml_dir) + for xmlpath in args.res_xmls: + if not os.path.exists(xmlpath): + xmlpath = join(args.private, xmlpath) + shutil.copy(xmlpath, res_xml_dir) + + # Render out android manifest: + manifest_path = "src/main/AndroidManifest.xml" + render_args = { + "args": args, + "service": service, + "service_names": service_names, + "android_api": android_api, + "debug": "debug" in args.build_mode, + "native_services": args.native_services + } + if get_bootstrap_name() == "sdl2": + render_args["url_scheme"] = url_scheme + render( + 'AndroidManifest.tmpl.xml', + manifest_path, + **render_args) + + # Copy the AndroidManifest.xml to the dist root dir so that ant + # can also use it + if exists('AndroidManifest.xml'): + remove('AndroidManifest.xml') + shutil.copy(manifest_path, 'AndroidManifest.xml') + + # gradle build templates + render( + 'build.tmpl.gradle', + 'build.gradle', + args=args, + aars=aars, + jars=jars, + android_api=android_api, + build_tools_version=build_tools_version, + debug_build="debug" in args.build_mode, + is_library=(get_bootstrap_name() == 'service_library'), + ) + + # gradle properties + render( + 'gradle.tmpl.properties', + 'gradle.properties', + args=args) + + # ant build templates + render( + 'build.tmpl.xml', + 'build.xml', + args=args, + versioned_name=versioned_name) + + # String resources: + timestamp = time.time() + if 'SOURCE_DATE_EPOCH' in environ: + # for reproducible builds + timestamp = int(environ['SOURCE_DATE_EPOCH']) + private_version = "{} {} {}".format( + args.version, + args.numeric_version, + timestamp + ) + render_args = { + "args": args, + "private_version": hashlib.sha1(private_version.encode()).hexdigest() + } + if get_bootstrap_name() == "sdl2": + render_args["url_scheme"] = url_scheme + render( + 'strings.tmpl.xml', + join(res_dir, 'values/strings.xml'), + **render_args) + + if exists(join("templates", "custom_rules.tmpl.xml")): + render( + 'custom_rules.tmpl.xml', + 'custom_rules.xml', + args=args) + + if get_bootstrap_name() == "webview": + render('WebViewLoader.tmpl.java', + 'src/main/java/org/kivy/android/WebViewLoader.java', + args=args) + + if args.sign: + render('build.properties', 'build.properties') + else: + if exists('build.properties'): + os.remove('build.properties') + + # Apply java source patches if any are present: + if exists(join('src', 'patches')): + print("Applying Java source code patches...") + for patch_name in os.listdir(join('src', 'patches')): + patch_path = join('src', 'patches', patch_name) + print("Applying patch: " + str(patch_path)) + + # -N: insist this is FORWARD patch, don't reverse apply + # -p1: strip first path component + # -t: batch mode, don't ask questions + patch_command = ["patch", "-N", "-p1", "-t", "-i", patch_path] + + try: + # Use a dry run to establish whether the patch is already applied. + # If we don't check this, the patch may be partially applied (which is bad!) + subprocess.check_output(patch_command + ["--dry-run"]) + except subprocess.CalledProcessError as e: + if e.returncode == 1: + # Return code 1 means not all hunks could be applied, this usually + # means the patch is already applied. + print("Warning: failed to apply patch (exit code 1), " + "assuming it is already applied: ", + str(patch_path)) + else: + raise e + else: + # The dry run worked, so do the real thing + subprocess.check_output(patch_command) + + +def parse_permissions(args_permissions): + if args_permissions and isinstance(args_permissions[0], list): + args_permissions = [p for perm in args_permissions for p in perm] + + def _is_advanced_permission(permission): + return permission.startswith("(") and permission.endswith(")") + + def _decode_advanced_permission(permission): + SUPPORTED_PERMISSION_PROPERTIES = ["name", "maxSdkVersion", "usesPermissionFlags"] + _permission_args = permission[1:-1].split(";") + _permission_args = (arg.split("=") for arg in _permission_args) + advanced_permission = dict(_permission_args) + + if "name" not in advanced_permission: + raise ValueError("Advanced permission must have a name property") + + for key in advanced_permission.keys(): + if key not in SUPPORTED_PERMISSION_PROPERTIES: + raise ValueError( + f"Property '{key}' is not supported. " + "Advanced permission only supports: " + f"{', '.join(SUPPORTED_PERMISSION_PROPERTIES)} properties" + ) + + return advanced_permission + + _permissions = [] + for permission in args_permissions: + if _is_advanced_permission(permission): + _permissions.append(_decode_advanced_permission(permission)) + else: + if "." in permission: + _permissions.append(dict(name=permission)) + else: + _permissions.append(dict(name=f"android.permission.{permission}")) + return _permissions + + +def get_sdl_orientation_hint(orientations): + SDL_ORIENTATION_MAP = { + "landscape": "LandscapeLeft", + "portrait": "Portrait", + "portrait-reverse": "PortraitUpsideDown", + "landscape-reverse": "LandscapeRight", + } + return " ".join( + [SDL_ORIENTATION_MAP[x] for x in orientations if x in SDL_ORIENTATION_MAP] + ) + + +def get_manifest_orientation(orientations, manifest_orientation=None): + # If the user has specifically set an orientation to use in the manifest, + # use that. + if manifest_orientation is not None: + return manifest_orientation + + # If multiple or no orientations are specified, use unspecified in the manifest, + # as we can only specify one orientation in the manifest. + if len(orientations) != 1: + return "unspecified" + + # Convert the orientation to a value that can be used in the manifest. + # If the specified orientation is not supported, use unspecified. + MANIFEST_ORIENTATION_MAP = { + "landscape": "landscape", + "portrait": "portrait", + "portrait-reverse": "reversePortrait", + "landscape-reverse": "reverseLandscape", + } + return MANIFEST_ORIENTATION_MAP.get(orientations[0], "unspecified") + + +def get_dist_ndk_min_api_level(): + # Get the default minsdk, equal to the NDK API that this dist is built against + try: + with open('dist_info.json', 'r') as fileh: + info = json.load(fileh) + ndk_api = int(info['ndk_api']) + except (OSError, KeyError, ValueError, TypeError): + print('WARNING: Failed to read ndk_api from dist info, defaulting to 12') + ndk_api = 12 # The old default before ndk_api was introduced + return ndk_api + + +def create_argument_parser(): + ndk_api = get_dist_ndk_min_api_level() + import argparse + ap = argparse.ArgumentParser(description='''\ +Package a Python application for Android (using +bootstrap ''' + get_bootstrap_name() + '''). + +For this to work, Java and Ant need to be in your path, as does the +tools directory of the Android SDK. +''') + + # --private is required unless for sdl2, where there's also --launcher + ap.add_argument('--private', dest='private', + help='the directory with the app source code files' + + ' (containing your main.py entrypoint)', + required=(get_bootstrap_name() != "sdl2")) + ap.add_argument('--package', dest='package', + help=('The name of the java package the project will be' + ' packaged under.'), + required=True) + ap.add_argument('--name', dest='name', + help=('The human-readable name of the project.'), + required=True) + ap.add_argument('--numeric-version', dest='numeric_version', + help=('The numeric version number of the project. If not ' + 'given, this is automatically computed from the ' + 'version.')) + ap.add_argument('--version', dest='version', + help=('The version number of the project. This should ' + 'consist of numbers and dots, and should have the ' + 'same number of groups of numbers as previous ' + 'versions.'), + required=True) + if get_bootstrap_name() == "sdl2": + ap.add_argument('--launcher', dest='launcher', action='store_true', + help=('Provide this argument to build a multi-app ' + 'launcher, rather than a single app.')) + ap.add_argument('--home-app', dest='home_app', action='store_true', default=False, + help=('Turn your application into a home app (launcher)')) + ap.add_argument('--permission', dest='permissions', action='append', default=[], + help='The permissions to give this app.', nargs='+') + ap.add_argument('--meta-data', dest='meta_data', action='append', default=[], + help='Custom key=value to add in application metadata') + ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[], + help='Used shared libraries included using tag in AndroidManifest.xml') + ap.add_argument('--asset', dest='assets', + action="append", default=[], + metavar="/path/to/source:dest", + help='Put this in the assets folder at assets/dest') + ap.add_argument('--resource', dest='resources', + action="append", default=[], + metavar="/path/to/source:kind/asset", + help='Put this in the res folder at res/kind') + ap.add_argument('--icon', dest='icon', + help=('A png file to use as the icon for ' + 'the application.')) + ap.add_argument('--icon-fg', dest='icon_fg', + help=('A png file to use as the foreground of the adaptive icon ' + 'for the application.')) + ap.add_argument('--icon-bg', dest='icon_bg', + help=('A png file to use as the background of the adaptive icon ' + 'for the application.')) + ap.add_argument('--service', dest='services', action='append', default=[], + help='Declare a new service entrypoint: ' + 'NAME:PATH_TO_PY[:foreground]') + ap.add_argument('--native-service', dest='native_services', action='append', default=[], + help='Declare a new native service: ' + 'package.name.service') + if get_bootstrap_name() != "service_only": + ap.add_argument('--presplash', dest='presplash', + help=('A jpeg file to use as a screen while the ' + 'application is loading.')) + ap.add_argument('--presplash-lottie', dest='presplash_lottie', + help=('A lottie (json) file to use as an animation while the ' + 'application is loading.')) + ap.add_argument('--presplash-color', + dest='presplash_color', + default='#000000', + help=('A string to set the loading screen ' + 'background color. ' + 'Supported formats are: ' + '#RRGGBB #AARRGGBB or color names ' + 'like red, green, blue, etc.')) + ap.add_argument('--window', dest='window', action='store_true', + default=False, + help='Indicate if the application will be windowed') + ap.add_argument('--manifest-orientation', dest='manifest_orientation', + help=('The orientation that will be set in the ' + 'android:screenOrientation attribute of the activity ' + 'in the AndroidManifest.xml file. If not set, ' + 'the value will be synthesized from the --orientation option.')) + ap.add_argument('--orientation', dest='orientation', + action="append", default=[], + choices=['portrait', 'landscape', 'landscape-reverse', 'portrait-reverse'], + help=('The orientations that the app will display in. ' + 'Since Android ignores android:screenOrientation ' + 'when in multi-window mode (Which is the default on Android 12+), ' + 'this option will also set the window orientation hints ' + 'for apps using the (default) SDL bootstrap.' + 'If multiple orientations are given, android:screenOrientation ' + 'will be set to "unspecified"')) + + ap.add_argument('--enable-androidx', dest='enable_androidx', + action='store_true', + help=('Enable the AndroidX support library, ' + 'requires api = 28 or greater')) + ap.add_argument('--android-entrypoint', dest='android_entrypoint', + default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS, + help='Defines which java class will be used for startup, usually a subclass of PythonActivity') + ap.add_argument('--android-apptheme', dest='android_apptheme', + default='@android:style/Theme.NoTitleBar', + help='Defines which app theme should be selected for the main activity') + ap.add_argument('--add-compile-option', dest='compile_options', default=[], + action='append', help='add compile options to gradle.build') + ap.add_argument('--add-gradle-repository', dest='gradle_repositories', + default=[], + action='append', + help='Ddd a repository for gradle') + ap.add_argument('--add-packaging-option', dest='packaging_options', + default=[], + action='append', + help='Dndroid packaging options') + + ap.add_argument('--wakelock', dest='wakelock', action='store_true', + help=('Indicate if the application needs the device ' + 'to stay on')) + ap.add_argument('--blacklist', dest='blacklist', + default=join(curdir, 'blacklist.txt'), + help=('Use a blacklist file to match unwanted file in ' + 'the final APK')) + ap.add_argument('--whitelist', dest='whitelist', + default=join(curdir, 'whitelist.txt'), + help=('Use a whitelist file to prevent blacklisting of ' + 'file in the final APK')) + ap.add_argument('--release', dest='build_mode', action='store_const', + const='release', default='debug', + help='Build your app as a non-debug release build. ' + '(Disables gdb debugging among other things)') + ap.add_argument('--with-debug-symbols', dest='with_debug_symbols', + action='store_const', const=True, default=False, + help='Will keep debug symbols from `.so` files.') + ap.add_argument('--add-jar', dest='add_jar', action='append', + help=('Add a Java .jar to the libs, so you can access its ' + 'classes with pyjnius. You can specify this ' + 'argument more than once to include multiple jars')) + ap.add_argument('--add-aar', dest='add_aar', action='append', + help=('Add an aar dependency manually')) + ap.add_argument('--depend', dest='depends', action='append', + help=('Add a external dependency ' + '(eg: com.android.support:appcompat-v7:19.0.1)')) + # The --sdk option has been removed, it is ignored in favour of + # --android-api handled by toolchain.py + ap.add_argument('--sdk', dest='sdk_version', default=-1, + type=int, help=('Deprecated argument, does nothing')) + ap.add_argument('--minsdk', dest='min_sdk_version', + default=ndk_api, type=int, + help=('Minimum Android SDK version that the app supports. ' + 'Defaults to {}.'.format(ndk_api))) + ap.add_argument('--allow-minsdk-ndkapi-mismatch', default=False, + action='store_true', + help=('Allow the --minsdk argument to be different from ' + 'the discovered ndk_api in the dist')) + ap.add_argument('--intent-filters', dest='intent_filters', + help=('Add intent-filters xml rules to the ' + 'AndroidManifest.xml file. The argument is a ' + 'filename containing xml. The filename should be ' + 'located relative to the python-for-android ' + 'directory')) + ap.add_argument('--res_xml', dest='res_xmls', action='append', default=[], + help='Add files to res/xml directory (for example device-filters)', nargs='+') + ap.add_argument('--with-billing', dest='billing_pubkey', + help='If set, the billing service will be added (not implemented)') + ap.add_argument('--add-source', dest='extra_source_dirs', action='append', + help='Include additional source dirs in Java build') + if get_bootstrap_name() == "webview": + ap.add_argument('--port', + help='The port on localhost that the WebView will access', + default='5000') + ap.add_argument('--try-system-python-compile', dest='try_system_python_compile', + action='store_true', + help='Use the system python during compileall if possible.') + ap.add_argument('--sign', action='store_true', + help=('Try to sign the APK with your credentials. You must set ' + 'the appropriate environment variables.')) + ap.add_argument('--add-activity', dest='add_activity', action='append', + help='Add this Java class as an Activity to the manifest.') + ap.add_argument('--activity-launch-mode', + dest='activity_launch_mode', + default='singleTask', + help='Set the launch mode of the main activity in the manifest.') + ap.add_argument('--allow-backup', dest='allow_backup', default='true', + help="if set to 'false', then android won't backup the application.") + ap.add_argument('--backup-rules', dest='backup_rules', default='', + help=('Backup rules for Android Auto Backup. Argument is a ' + 'filename containing xml. The filename should be ' + 'located relative to the private directory containing your source code ' + 'files (containing your main.py entrypoint). ' + 'See https://developer.android.com/guide/topics/data/' + 'autobackup#IncludingFiles for more information')) + ap.add_argument('--no-byte-compile-python', dest='byte_compile_python', + action='store_false', default=True, + help='Skip byte compile for .py files.') + ap.add_argument('--no-optimize-python', dest='optimize_python', + action='store_false', default=True, + help=('Whether to compile to optimised .pyc files, using -OO ' + '(strips docstrings and asserts)')) + ap.add_argument('--extra-manifest-xml', default='', + help=('Extra xml to write directly inside the element of' + 'AndroidManifest.xml')) + ap.add_argument('--extra-manifest-application-arguments', default='', + help='Extra arguments to be added to the tag of' + 'AndroidManifest.xml') + ap.add_argument('--manifest-placeholders', dest='manifest_placeholders', + default='[:]', help=('Inject build variables into the manifest ' + 'via the manifestPlaceholders property')) + ap.add_argument('--service-class-name', dest='service_class_name', default=DEFAULT_PYTHON_SERVICE_JAVA_CLASS, + help='Use that parameter if you need to implement your own PythonServive Java class') + ap.add_argument('--activity-class-name', dest='activity_class_name', default=DEFAULT_PYTHON_ACTIVITY_JAVA_CLASS, + help='The full java class name of the main activity') + + return ap + + +def parse_args_and_make_package(args=None): + global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON + + ndk_api = get_dist_ndk_min_api_level() + ap = create_argument_parser() + + # Put together arguments, and add those from .p4a config file: + if args is None: + args = sys.argv[1:] + + def _read_configuration(): + if not exists(".p4a"): + return + print("Reading .p4a configuration") + with open(".p4a") as fd: + lines = fd.readlines() + lines = [shlex.split(line) + for line in lines if not line.startswith("#")] + for line in lines: + for arg in line: + args.append(arg) + _read_configuration() + + args = ap.parse_args(args) + + if args.name and args.name[0] == '"' and args.name[-1] == '"': + args.name = args.name[1:-1] + + if ndk_api != args.min_sdk_version: + print(('WARNING: --minsdk argument does not match the api that is ' + 'compiled against. Only proceed if you know what you are ' + 'doing, otherwise use --minsdk={} or recompile against api ' + '{}').format(ndk_api, args.min_sdk_version)) + if not args.allow_minsdk_ndkapi_mismatch: + print('You must pass --allow-minsdk-ndkapi-mismatch to build ' + 'with --minsdk different to the target NDK api from the ' + 'build step') + sys.exit(1) + else: + print('Proceeding with --minsdk not matching build target api') + + if args.billing_pubkey: + print('Billing not yet supported!') + sys.exit(1) + + if args.sdk_version == -1: + print('WARNING: Received a --sdk argument, but this argument is ' + 'deprecated and does nothing.') + args.sdk_version = -1 # ensure it is not used + + args.permissions = parse_permissions(args.permissions) + + args.manifest_orientation = get_manifest_orientation( + args.orientation, args.manifest_orientation + ) + + if get_bootstrap_name() == "sdl2": + args.sdl_orientation_hint = get_sdl_orientation_hint(args.orientation) + + if args.res_xmls and isinstance(args.res_xmls[0], list): + args.res_xmls = [x for res in args.res_xmls for x in res] + + if args.try_system_python_compile: + # Hardcoding python2.7 is okay for now, as python3 skips the + # compilation anyway + python_executable = 'python2.7' + try: + subprocess.call([python_executable, '--version']) + except (OSError, subprocess.CalledProcessError): + pass + else: + PYTHON = python_executable + + if args.blacklist: + with open(args.blacklist) as fd: + patterns = [x.strip() for x in fd.read().splitlines() + if x.strip() and not x.strip().startswith('#')] + BLACKLIST_PATTERNS += patterns + + if args.whitelist: + with open(args.whitelist) as fd: + patterns = [x.strip() for x in fd.read().splitlines() + if x.strip() and not x.strip().startswith('#')] + WHITELIST_PATTERNS += patterns + + if args.private is None and \ + get_bootstrap_name() == 'sdl2' and args.launcher is None: + print('Need --private directory or ' + + '--launcher (SDL2 bootstrap only)' + + 'to have something to launch inside the .apk!') + sys.exit(1) + make_package(args) + + return args + + +if __name__ == "__main__": + if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'): + WHITELIST_PATTERNS.append('pyconfig.h') + parse_args_and_make_package() diff --git a/sbapp/services/sidebandservice.py b/sbapp/services/sidebandservice.py index 6bacded..171bf83 100644 --- a/sbapp/services/sidebandservice.py +++ b/sbapp/services/sidebandservice.py @@ -1,4 +1,4 @@ -__debug_build__ = False +__debug_build__ = True import time import RNS diff --git a/sbapp/ui/announces.py b/sbapp/ui/announces.py index f60abac..dae7104 100644 --- a/sbapp/ui/announces.py +++ b/sbapp/ui/announces.py @@ -222,12 +222,12 @@ class Announces(): item.dmenu = MDDropdownMenu( caller=item.iconr, items=dm_items, - position="center", - width_mult=4, - elevation=1, - radius=dp(3), - opening_transition="linear", - opening_time=0.0, + #position="center", + #width_mult=4, + #elevation=1, + #radius=dp(3), + #opening_transition="linear", + #opening_time=0.0, ) def callback_factory(ref): diff --git a/sbapp/ui/conversations.py b/sbapp/ui/conversations.py index 6d158b2..c076888 100644 --- a/sbapp/ui/conversations.py +++ b/sbapp/ui/conversations.py @@ -220,6 +220,8 @@ class Conversations(): item.dmenu.dismiss() return x + item.iconr = IconRightWidget(icon="dots-vertical"); + if self.conversation_dropdown == None: dmi_h = 40 dm_items = [ @@ -250,18 +252,21 @@ class Conversations(): ] self.conversation_dropdown = MDDropdownMenu( - caller=None, + caller=item.iconr, items=dm_items, position="auto", - width_mult=4, - elevation=1, - radius=dp(3), - opening_transition="linear", - opening_time=0.0, + #border_margin=dp(24), + #width=dp(256), + #elevation=0, + #radius=dp(3), + show_transition="linear", + hide_transition="linear", + #show_duration=0.1, + #hide_duration=0.1, + ) self.conversation_dropdown.effect_cls = ScrollEffect - item.iconr = IconRightWidget(icon="dots-vertical"); item.dmenu = self.conversation_dropdown def callback_factory(ref, dest): diff --git a/sbapp/ui/helpers.py b/sbapp/ui/helpers.py index 68f9c72..2ef4942 100644 --- a/sbapp/ui/helpers.py +++ b/sbapp/ui/helpers.py @@ -25,7 +25,7 @@ intensity_msgs_light = "500" class ContentNavigationDrawer(Screen): pass -class DrawerList(ThemableBehavior, MDList): +class DrawerList(MDList): pass class IconListItem(OneLineIconListItem): diff --git a/sbapp/ui/messages.py b/sbapp/ui/messages.py index 9f93b72..e8b8060 100644 --- a/sbapp/ui/messages.py +++ b/sbapp/ui/messages.py @@ -429,12 +429,12 @@ class Messages(): item.dmenu = MDDropdownMenu( caller=item.ids.msg_submenu, items=dm_items, - position="center", - width_mult=4, - elevation=1, - radius=dp(3), - opening_transition="linear", - opening_time=0.0, + #position="center", + #width_mult=4, + #elevation=1, + #radius=dp(3), + #opening_transition="linear", + #opening_time=0.0, ) def callback_factory(ref): diff --git a/setup.py b/setup.py index 98f5023..90f5e87 100644 --- a/setup.py +++ b/setup.py @@ -81,9 +81,9 @@ setuptools.setup( 'sideband=sbapp:main.run', ] }, - install_requires=["rns>=0.4.6", "lxmf>=0.2.8", "kivy==2.1.0", "plyer", "pillow", "qrcode"], + install_requires=["rns>=0.5.6", "lxmf>=0.3.1", "kivy>=2.2.1", "plyer", "pillow", "qrcode", "materialyoucolor"], extras_require={ "macos": ["pyobjus"], }, - python_requires='>=3.6', + python_requires='>=3.8', )