diff --git a/.github/workflows/integration_delivery.yml b/.github/workflows/integration_delivery.yml index 7d7e62c..af30bf1 100644 --- a/.github/workflows/integration_delivery.yml +++ b/.github/workflows/integration_delivery.yml @@ -206,13 +206,28 @@ jobs: name: binary path: artifacts + - uses: actions/checkout@v4 + name: Checkout + + - name: Extract Changelog + id: changelog + run: | + perl -0777 -ne 'while (/## Version ${{ needs.build.outputs.version }}\n(\s*\n)*(.*?)(\s*\n)*## Version \d+\.\d+\.\d+\n/sg) {print "$2\n"}' CHANGELOG.md > CURRENT_CHANGELOG.md + { + echo "CONTENT<> "$GITHUB_OUTPUT" + - name: Release uses: softprops/action-gh-release@v2 with: files: artifacts/* tag_name: v${{ needs.build.outputs.version }} body: | - Release of version ${{ needs.build.outputs.version }} PyPI package: https://pypi.org/project/${{ needs.build.outputs.name }}/${{ needs.build.outputs.version }} + + # Changes: + ${{ steps.changelog.outputs.CONTENT }} prerelease: false draft: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 8823487..8edb375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## Version 0.11.0 + +- feat(Menu): render faded next and previous menu items to induce there are more + items in the menu and it can be scrolled +- refactor(core): add colors to list of global constants to avoid hardcoding +- refactor(AnimatedSlider): not using the default look, replaced with the one designed + in figma +- refactor(ItemWidget): add `opacity` field +- ci(github): add changelog to the release notes + ## Version 0.10.8 - fix(regression): handle return value of the action in `ActionItem` when it's callable diff --git a/poetry.lock b/poetry.lock index 4fda221..47371df 100644 --- a/poetry.lock +++ b/poetry.lock @@ -35,13 +35,13 @@ adafruit-circuitpython-register = "*" [[package]] name = "adafruit-circuitpython-busdevice" -version = "5.2.6" +version = "5.2.9" description = "CircuitPython bus device classes to manage bus sharing." optional = true python-versions = "*" files = [ - {file = "adafruit-circuitpython-busdevice-5.2.6.tar.gz", hash = "sha256:ed06f5552e5567b0c89589c5bc6ef3adcac67d59eb505ce9127af99f33c2bc90"}, - {file = "adafruit_circuitpython_busdevice-5.2.6-py3-none-any.whl", hash = "sha256:9f25577843f0a338a0936a1b57436f4451f7783b38e3cf46160b6be78faeaa44"}, + {file = "adafruit_circuitpython_busdevice-5.2.9-py3-none-any.whl", hash = "sha256:78245c551eb3795a4f3739f1adac0b08a65c9fb0f722e866de620482cbbac51f"}, + {file = "adafruit_circuitpython_busdevice-5.2.9.tar.gz", hash = "sha256:9f9c3df385091410dac5961918e475ec88faed7ff543ad6d74208f08c2566513"}, ] [package.dependencies] @@ -50,13 +50,13 @@ adafruit-circuitpython-typing = "*" [[package]] name = "adafruit-circuitpython-connectionmanager" -version = "1.0.1" +version = "1.2.1" description = "A urllib3.poolmanager/urllib3.connectionpool-like library for managing sockets and connections" optional = true python-versions = "*" files = [ - {file = "adafruit-circuitpython-connectionmanager-1.0.1.tar.gz", hash = "sha256:913b325488860524edcb6a69f13575addc27c0127764cd74074476e149ba7a73"}, - {file = "adafruit_circuitpython_connectionmanager-1.0.1-py3-none-any.whl", hash = "sha256:9b7800960b61896f499086a2dbf192c270a3eb1f69bf88268a1bd5422f33ad68"}, + {file = "adafruit_circuitpython_connectionmanager-1.2.1-py3-none-any.whl", hash = "sha256:fc3588b931583e8759600217d779dafc617bc83430e425f4967b2804221a7dbc"}, + {file = "adafruit_circuitpython_connectionmanager-1.2.1.tar.gz", hash = "sha256:586742d29f335df5c99e940a16c34346b441be7e43ba3ec994f1da88495e8b7b"}, ] [package.dependencies] @@ -81,13 +81,13 @@ typing-extensions = ">=4.0,<5.0" [[package]] name = "adafruit-circuitpython-requests" -version = "3.2.5" +version = "3.2.8" description = "A requests-like library for web interfacing" optional = true python-versions = "*" files = [ - {file = "adafruit_circuitpython_requests-3.2.5-py3-none-any.whl", hash = "sha256:25f789bac9f0568c289585418e52abeee7cd37c06c2fb58780aeb62022faff3e"}, - {file = "adafruit_circuitpython_requests-3.2.5.tar.gz", hash = "sha256:971d7fe96b032c8f7a92ab6aaa1997e3c920171f3b935ae76b2c6b8af9c8929d"}, + {file = "adafruit_circuitpython_requests-3.2.8-py3-none-any.whl", hash = "sha256:aca829e7a84bda37b8a70a867e28ae8c8aa27cbf596ea16f52805b0d2051be13"}, + {file = "adafruit_circuitpython_requests-3.2.8.tar.gz", hash = "sha256:50e46e4a1ee9a4f2e832308468191b855424ebc468a0bc8c911c68df33918863"}, ] [package.dependencies] @@ -338,13 +338,13 @@ files = [ [[package]] name = "docutils" -version = "0.21.1" +version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = true python-versions = ">=3.9" files = [ - {file = "docutils-0.21.1-py3-none-any.whl", hash = "sha256:14c8d34a55b46c88f9f714adb29cefbdd69fb82f3fef825e59c5faab935390d8"}, - {file = "docutils-0.21.1.tar.gz", hash = "sha256:65249d8a5345bc95e0f40f280ba63c98eb24de35c6c8f5b662e3e8948adea83f"}, + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] @@ -698,13 +698,13 @@ files = [ [[package]] name = "pyright" -version = "1.1.359" +version = "1.1.360" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.359-py3-none-any.whl", hash = "sha256:5582777be7eab73512277ac7da7b41e15bc0737f488629cb9babd96e0769be61"}, - {file = "pyright-1.1.359.tar.gz", hash = "sha256:f0eab50f3dafce8a7302caeafd6a733f39901a2bf5170bb23d77fd607c8a8dbc"}, + {file = "pyright-1.1.360-py3-none-any.whl", hash = "sha256:7637f75451ac968b7cf1f8c51cfefb6d60ac7d086eb845364bc8ac03a026efd7"}, + {file = "pyright-1.1.360.tar.gz", hash = "sha256:784ddcda9745e9f5610483d7b963e9aa8d4f50d7755a9dffb28ccbeb27adce32"}, ] [package.dependencies] @@ -822,28 +822,28 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" -version = "0.4.1" +version = "0.4.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:2d9ef6231e3fbdc0b8c72404a1a0c46fd0dcea84efca83beb4681c318ea6a953"}, - {file = "ruff-0.4.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9485f54a7189e6f7433e0058cf8581bee45c31a25cd69009d2a040d1bd4bfaef"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2921ac03ce1383e360e8a95442ffb0d757a6a7ddd9a5be68561a671e0e5807e"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eec8d185fe193ad053eda3a6be23069e0c8ba8c5d20bc5ace6e3b9e37d246d3f"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:baa27d9d72a94574d250f42b7640b3bd2edc4c58ac8ac2778a8c82374bb27984"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f1ee41580bff1a651339eb3337c20c12f4037f6110a36ae4a2d864c52e5ef954"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0926cefb57fc5fced629603fbd1a23d458b25418681d96823992ba975f050c2b"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c6e37f2e3cd74496a74af9a4fa67b547ab3ca137688c484749189bf3a686ceb"}, - {file = "ruff-0.4.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd703a5975ac1998c2cc5e9494e13b28f31e66c616b0a76e206de2562e0843c"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b92f03b4aa9fa23e1799b40f15f8b95cdc418782a567d6c43def65e1bbb7f1cf"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1c859f294f8633889e7d77de228b203eb0e9a03071b72b5989d89a0cf98ee262"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b34510141e393519a47f2d7b8216fec747ea1f2c81e85f076e9f2910588d4b64"}, - {file = "ruff-0.4.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6e68d248ed688b9d69fd4d18737edcbb79c98b251bba5a2b031ce2470224bdf9"}, - {file = "ruff-0.4.1-py3-none-win32.whl", hash = "sha256:b90506f3d6d1f41f43f9b7b5ff845aeefabed6d2494307bc7b178360a8805252"}, - {file = "ruff-0.4.1-py3-none-win_amd64.whl", hash = "sha256:c7d391e5936af5c9e252743d767c564670dc3889aff460d35c518ee76e4b26d7"}, - {file = "ruff-0.4.1-py3-none-win_arm64.whl", hash = "sha256:a1eaf03d87e6a7cd5e661d36d8c6e874693cb9bc3049d110bc9a97b350680c43"}, - {file = "ruff-0.4.1.tar.gz", hash = "sha256:d592116cdbb65f8b1b7e2a2b48297eb865f6bdc20641879aa9d7b9c11d86db79"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, + {file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, + {file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, + {file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, + {file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, + {file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, + {file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, + {file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, ] [[package]] @@ -923,4 +923,4 @@ dev = ["headless-kivy-pi", "headless-kivy-pi"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "630a56c3154e09a791590827438b4c51378cbc48a45c75ee6916ce81668353d0" +content-hash = "b6aa36d9a732793786ced7c236f4a2c459c08b9c5b4cdf15fc07236f4ab90153" diff --git a/pyproject.toml b/pyproject.toml index ff04c95..f28dd07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ubo-gui" -version = "0.10.8" +version = "0.11.0" description = "GUI sdk for Ubo Pod" authors = ["Sassan Haradji "] license = "Apache-2.0" @@ -26,8 +26,8 @@ optional = true [tool.poetry.group.dev.dependencies] poethepoet = "^0.24.4" -pyright = "^1.1.359" -ruff = "^0.4.1" +pyright = "^1.1.360" +ruff = "^0.4.2" [tool.poetry.extras] dev = ['headless-kivy-pi'] diff --git a/ubo_gui/animated_slider/__init__.py b/ubo_gui/animated_slider/__init__.py index c6639b1..5d1851d 100644 --- a/ubo_gui/animated_slider/__init__.py +++ b/ubo_gui/animated_slider/__init__.py @@ -2,64 +2,53 @@ from __future__ import annotations +import pathlib + from kivy.animation import Animation -from kivy.properties import BoundedNumericProperty +from kivy.lang.builder import Builder +from kivy.properties import AliasProperty from kivy.uix.slider import Slider class AnimatedSlider(Slider): """A slider that moves up and down when its value is changed.""" - animated_value = BoundedNumericProperty(0) + def set_animated_value(self: AnimatedSlider, value: float) -> bool: + """Set the value of the `animated_value` property. + + Animates the slider moving to the new value. - def animated_value_error_handler(self: AnimatedSlider, value: float) -> float: - """Handle error for the `animated_value` property's `errorhandler` argument. + Arguments: + --------- + value: The new value that the `animated_value` property is being set to. - This method is called when the `animated_value` property is set to a - value outside of its `min` and `max` bounds. - This method returns the value that the `animated_value` property should be - set to, instead of the value that was passed to it. """ - if value < self.min: - return self.min - if value > self.max: - return self.max - return 0 + self._animated_value = max(self.min, min(value, self.max)) + Animation(value=self._animated_value, duration=0.2).start(self) + return True + + def get_animated_value(self: AnimatedSlider) -> float: + """Return the value of the `animated_value` property. + + Returns + ------- + The value of the `animated_value` property. + + """ + return self._animated_value + + _animated_value = 0 + animated_value = AliasProperty( + setter=set_animated_value, + getter=get_animated_value, + ) def __init__(self: AnimatedSlider, **kwargs: object) -> None: """Initialize the `AnimatedSlider` class.""" super().__init__(**kwargs) self.value = self.animated_value - self.padding = 0 - self.property('animated_value').__init__( - 0, - min=self.min, - max=self.max, - errorhandler=self.animated_value_error_handler, - ) - - def on_min(self: AnimatedSlider, *largs: float) -> None: - """Handle the `min` property being set to a new value.""" - value = largs[1] - self.property('animated_value').set_min(self, value) - - def on_max(self: AnimatedSlider, *largs: float) -> None: - """Handle the `max` property being set to a new value.""" - value = largs[1] - self.property('animated_value').set_max(self, value) - - def on_animated_value( - self: AnimatedSlider, - _: AnimatedSlider, - new_value: float, - ) -> None: - """Handle the `animated_value` property being set to a new value. - Animates the slider moving to the new value. - Arguments: - --------- - new_value: The new value that the `animated_value` property is being set to. - - """ - Animation(value=new_value, duration=0.2).start(self) +Builder.load_file( + pathlib.Path(__file__).parent.joinpath('animated_slider.kv').resolve().as_posix(), +) diff --git a/ubo_gui/animated_slider/animated_slider.kv b/ubo_gui/animated_slider/animated_slider.kv new file mode 100644 index 0000000..a25ca94 --- /dev/null +++ b/ubo_gui/animated_slider/animated_slider.kv @@ -0,0 +1,19 @@ +#:kivy 2.3.0 +#:import utils kivy.utils + +<-AnimatedSlider@Slider>: + background_width: dp(2) + cursor_size: ((dp(13), dp(28)) if self.orientation == 'horizontal' else (dp(28), dp(13))) + padding: min(*self.cursor_size) / 2 + canvas: + Color: + rgb: utils.get_color_from_hex(UBO_GUI_SECONDARY_COLOR_LIGHT) + Rectangle: + pos: (self.x, self.center_y - self.background_width/2) if self.orientation == 'horizontal' else (self.center_x - self.background_width/2, self.y) + size: (self.width, self.background_width) if self.orientation == 'horizontal' else (self.background_width, self.height) + Color: + rgb: utils.get_color_from_hex(UBO_GUI_PRIMARY_COLOR) + RoundedRectangle: + pos: (root.value_pos[0] - root.cursor_width/2, root.center_y - root.cursor_height/2) if root.orientation == 'horizontal' else (root.center_x - root.cursor_width/2, root.value_pos[1] - root.cursor_height/2) + size: root.cursor_size + radius: (min(*root.cursor_size) / 2,) * 2 diff --git a/ubo_gui/app/app.kv b/ubo_gui/app/app.kv index f4d89d3..bf93a62 100644 --- a/ubo_gui/app/app.kv +++ b/ubo_gui/app/app.kv @@ -1,7 +1,7 @@ -#:kivy 2.2.1 +#:kivy 2.3.0 : - BoxLayout: + FloatLayout: id: main_layout size: root.size pos: root.pos @@ -10,15 +10,18 @@ BoxLayout: id: header_layout size_hint: 1, None + pos: 0, root.height - dp(30) height: dp(30) BoxLayout: id: central_layout + pos: 0, 0 size_hint: 1, 1 BoxLayout: id: footer_layout size_hint: 1, None + pos: 0, 0 height: dp(40) RootWidget: diff --git a/ubo_gui/constants.py b/ubo_gui/constants.py index 9c2f817..ce68740 100644 --- a/ubo_gui/constants.py +++ b/ubo_gui/constants.py @@ -4,6 +4,7 @@ PRIMARY_COLOR = '#68B7FF' SECONDARY_COLOR = '#363F4B' +SECONDARY_COLOR_LIGHT = '#ABA7A7' TEXT_COLOR = '#FFFFFF' INFO_COLOR = '#2196F3' @@ -15,8 +16,12 @@ f""" #:set UBO_GUI_PRIMARY_COLOR '{PRIMARY_COLOR}' #:set UBO_GUI_SECONDARY_COLOR '{SECONDARY_COLOR}' +#:set UBO_GUI_SECONDARY_COLOR_LIGHT '{SECONDARY_COLOR_LIGHT}' #:set UBO_GUI_TEXT_COLOR '{TEXT_COLOR}' + +#:set UBO_GUI_INFO_COLOR '{INFO_COLOR}' #:set UBO_GUI_DANGER_COLOR '{DANGER_COLOR}' +#:set UBO_GUI_WARNING_COLOR '{WARNING_COLOR}' #:set UBO_GUI_SUCCESS_COLOR '{SUCCESS_COLOR}' """, ) diff --git a/ubo_gui/gauge/gauge_widget.kv b/ubo_gui/gauge/gauge_widget.kv index ff28a5a..50ec4f6 100644 --- a/ubo_gui/gauge/gauge_widget.kv +++ b/ubo_gui/gauge/gauge_widget.kv @@ -1,4 +1,4 @@ -#:kivy 2.2.1 +#:kivy 2.3.0 : _size: (self.width, self.width) if self.width < (self.height - dp(24)) * 2 else ((self.height - dp(24)) * 2, (self.height - dp(24)) * 2) @@ -24,7 +24,7 @@ angle_end: 270 + (self.value - self.min_value) / (self.max_value - self.min_value) * 180 Color: - rgba: 0, 0, 0, 1 + rgb: 0, 0, 0 Ellipse: pos: root._pos[0] + root._size[0] / 4, root._pos[1] + root._size[1] / 4 diff --git a/ubo_gui/menu/__init__.py b/ubo_gui/menu/__init__.py index ba43889..6e0fe57 100644 --- a/ubo_gui/menu/__init__.py +++ b/ubo_gui/menu/__init__.py @@ -14,11 +14,16 @@ import uuid import warnings from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Callable, Self, Sequence, cast +from typing import TYPE_CHECKING, Callable, Self, Sequence, cast from headless_kivy_pi import HeadlessWidget from kivy.lang.builder import Builder -from kivy.properties import AliasProperty, ListProperty +from kivy.properties import ( + AliasProperty, + BooleanProperty, + ListProperty, + NumericProperty, +) from kivy.uix.boxlayout import BoxLayout from kivy.uix.screenmanager import Screen, ScreenManager, TransitionBase @@ -41,6 +46,8 @@ from .widgets.normal_menu_page_widget import NormalMenuPageWidget if TYPE_CHECKING: + from kivy.uix.widget import Widget + from ubo_gui.animated_slider import AnimatedSlider @@ -113,7 +120,7 @@ class MenuWidget(BoxLayout, TransitionsMixin): screen_manager: ScreenManager slider: AnimatedSlider - def __init__(self: MenuWidget, **kwargs: dict[str, Any]) -> None: + def __init__(self: MenuWidget, **kwargs: object) -> None: """Initialize a `MenuWidget`.""" self._current_menu_items = [] self.widget_subscriptions = set() @@ -159,7 +166,7 @@ def go_down(self: MenuWidget) -> None: self.current_application.go_down() return - if self.pages <= 1: + if self.pages == 1: return self.page_index = (self.page_index + 1) % self.pages self.render_items() @@ -178,7 +185,7 @@ def go_up(self: MenuWidget) -> None: self.current_application.go_up() return - if self.pages <= 1: + if self.pages == 1: return self.page_index = (self.page_index - 1) % self.pages self.render_items() @@ -289,67 +296,137 @@ def go_back(self: MenuWidget) -> None: headless_widget.activate_high_fps_mode() self.pop() - def render_items(self: MenuWidget, *_: object) -> None: - """Render the items of the current menu.""" - self.clear_widget_subscriptions() - if not self.current_menu: - return - if self.page_index == 0 and isinstance(self.current_menu, HeadedMenu): - list_widget = HeaderMenuPageWidget( - self.current_menu_items[:1], - name=f'Page {self.get_depth()} 0', + def render_header_menu(self: MenuWidget, menu: HeadedMenu) -> HeaderMenuPageWidget: + """Render a header menu.""" + next_item = ( + None + if self.page_index == self.pages - 1 + else (padding_item := self.current_menu_items[PAGE_SIZE - 2]) + and Item( + label=padding_item.label, + icon=padding_item.icon, + background_color=padding_item.background_color, + is_short=padding_item.is_short, + opacity=0.6, ) + ) + list_widget = HeaderMenuPageWidget( + [*self.current_menu_items[: PAGE_SIZE - 2], next_item], + name=f'Page {self.get_depth()} 0', + count=PAGE_SIZE + 1 if self.render_surroundings else PAGE_SIZE, + offset=1 if self.render_surroundings else 0, + render_surroundings=self.render_surroundings, + padding_bottom=self.padding_bottom, + padding_top=self.padding_top, + ) - def handle_heading_change(heading: str) -> None: - logger.debug( - 'Handle `heading` change...', - extra={ - 'new_heading': heading, - 'old_heading': list_widget.heading, - 'subscription_level': 'widget', - }, - ) - if heading != list_widget.heading: - list_widget.heading = heading - - self.widget_subscriptions.add( - process_subscribable_value( - self.current_menu.heading, - handle_heading_change, - ), + def handle_heading_change(heading: str) -> None: + logger.debug( + 'Handle `heading` change...', + extra={ + 'new_heading': heading, + 'old_heading': list_widget.heading, + 'subscription_level': 'widget', + }, ) + if heading != list_widget.heading: + list_widget.heading = heading - def handle_sub_heading_change(sub_heading: str) -> None: - logger.debug( - 'Handle `sub_heading` change...', - extra={ - 'new_sub_heading': sub_heading, - 'old_sub_heading': list_widget.sub_heading, - 'subscription_level': 'widget', - }, - ) - if sub_heading != list_widget.sub_heading: - list_widget.sub_heading = sub_heading - - self.widget_subscriptions.add( - process_subscribable_value( - self.current_menu.sub_heading, - handle_sub_heading_change, - ), + self.widget_subscriptions.add( + process_subscribable_value( + menu.heading, + handle_heading_change, + ), + ) + + def handle_sub_heading_change(sub_heading: str) -> None: + logger.debug( + 'Handle `sub_heading` change...', + extra={ + 'new_sub_heading': sub_heading, + 'old_sub_heading': list_widget.sub_heading, + 'subscription_level': 'widget', + }, ) + if sub_heading != list_widget.sub_heading: + list_widget.sub_heading = sub_heading - self.current_screen = list_widget - else: - offset = ( - -(PAGE_SIZE - 1) if isinstance(self.current_menu, HeadedMenu) else 0 + self.widget_subscriptions.add( + process_subscribable_value( + menu.sub_heading, + handle_sub_heading_change, + ), + ) + + return list_widget + + def render_normal_menu(self: MenuWidget, menu: Menu) -> NormalMenuPageWidget: + """Render a normal menu.""" + offset = -(PAGE_SIZE - 1) if isinstance(menu, HeadedMenu) else 0 + items: list[Item | None] = list( + self.current_menu_items[ + self.page_index * PAGE_SIZE + offset : self.page_index * PAGE_SIZE + + PAGE_SIZE + + offset + ], + ) + if self.render_surroundings: + previous_item = ( + None + if self.page_index == 0 + else ( + padding_item := self.current_menu_items[ + self.page_index * PAGE_SIZE + offset - 1 + ] + ) + and Item( + label=padding_item.label, + icon=padding_item.icon, + background_color=padding_item.background_color, + is_short=padding_item.is_short, + opacity=0.6, + ) ) - list_widget = NormalMenuPageWidget( - self.current_menu_items[ - self.page_index * 3 + offset : self.page_index * 3 + 3 + offset - ], - name=f'Page {self.get_depth()} 0', + next_item = ( + None + if self.page_index == self.pages - 1 + else ( + padding_item := self.current_menu_items[ + self.page_index * PAGE_SIZE + PAGE_SIZE + offset + ] + ) + and Item( + label=padding_item.label, + icon=padding_item.icon, + background_color=padding_item.background_color, + is_short=padding_item.is_short, + opacity=0.6, + ) ) - self.current_screen = list_widget + items = [previous_item, *items, next_item] + return NormalMenuPageWidget( + items, + name=f'Page {self.get_depth()} 0', + count=PAGE_SIZE + 2 if self.render_surroundings else PAGE_SIZE, + offset=1 if self.render_surroundings else 0, + render_surroundings=self.render_surroundings, + padding_bottom=self.padding_bottom, + padding_top=self.padding_top, + ) + + def render_items(self: MenuWidget, *_: object) -> None: + """Render the items of the current menu.""" + self.clear_widget_subscriptions() + if self.page_index >= self.pages: + self.page_index = self.pages - 1 + if not self.current_menu: + return + if self.page_index == 0 and isinstance(self.current_menu, HeadedMenu): + list_widget = self.render_header_menu(self.current_menu) + else: + list_widget = self.render_normal_menu(self.current_menu) + + self.current_screen = list_widget def handle_placeholder_change(placeholder: str | None) -> None: logger.debug( @@ -442,14 +519,18 @@ def open_application(self: MenuWidget, application: PageWidget) -> None: if headless_widget: headless_widget.activate_high_fps_mode() application.name = uuid.uuid4().hex + application.padding_bottom = self.padding_bottom + application.padding_top = self.padding_top self.push( application, transition=self._swap_transition, duration=0.2, direction='left', ) - application.bind(on_close=self.close_application) - application.bind(on_leave=self.leave_application) + application.bind( + on_close=self.close_application, + on_leave=self.leave_application, + ) def clean_application(self: MenuWidget, application: PageWidget) -> None: """Clean up the application bounds.""" @@ -566,8 +647,8 @@ def get_is_scrollbar_visible(self: MenuWidget) -> bool: """Return whether scroll-bar is needed or not.""" return not self.current_application and self.pages > 1 - def on_kv_post(self: MenuWidget, base_widget: Any) -> None: # noqa: ANN401 - """Activate low fps mode when transition is done.""" + def on_kv_post(self: MenuWidget, base_widget: Widget) -> None: + """Run after the widget is fully constructed.""" _ = base_widget self.screen_manager = cast(ScreenManager, self.ids.screen_manager) self.slider = self.ids.slider @@ -619,9 +700,9 @@ def set_page_index(self: MenuWidget, page_index: int) -> bool: def get_pages(self: MenuWidget) -> int: """Return the number of pages of the currently active menu.""" if isinstance(self.current_menu, HeadedMenu): - return math.ceil((len(self.current_menu_items) + 2) / 3) + return max(math.ceil((len(self.current_menu_items) + 2) / 3), 1) if isinstance(self.current_menu, HeadlessMenu): - return math.ceil(len(self.current_menu_items) / 3) + return max(math.ceil(len(self.current_menu_items) / 3), 1) return 0 def get_current_menu_items(self: MenuWidget) -> Sequence[Item] | None: @@ -631,7 +712,7 @@ def get_current_menu_items(self: MenuWidget) -> Sequence[Item] | None: def set_current_menu_items(self: MenuWidget, items: Sequence[Item]) -> bool: """Set current menu items.""" self._current_menu_items = items - self.slider.value = self.get_pages() - 1 + self.slider.value = self.get_pages() - 1 - self.page_index return True stack: list[StackItem] = ListProperty() @@ -643,7 +724,7 @@ def set_current_menu_items(self: MenuWidget, items: Sequence[Item]) -> bool: depth: int = AliasProperty(getter=get_depth, bind=['stack'], cache=True) pages: int = AliasProperty( getter=get_pages, - bind=['current_menu_items'], + bind=['current_menu_items', 'current_menu'], cache=True, ) page_index = AliasProperty( @@ -674,6 +755,12 @@ def set_current_menu_items(self: MenuWidget, items: Sequence[Item]) -> bool: bind=['pages'], cache=True, ) + render_surroundings = BooleanProperty( + default=False, + cache=True, + ) + padding_bottom = NumericProperty(default=0) + padding_top = NumericProperty(default=0) Builder.load_file( diff --git a/ubo_gui/menu/constants.py b/ubo_gui/menu/constants.py index 1750b7b..0f2a08a 100644 --- a/ubo_gui/menu/constants.py +++ b/ubo_gui/menu/constants.py @@ -6,10 +6,14 @@ PAGE_SIZE = PAGE_MAX_ITEMS SHORT_WIDTH = 46 +MENU_ITEM_HEIGHT = 52 +MENU_ITEM_GAP = 7 Builder.load_string( f""" -#:set UBO_GUI_PAGE_SIZE '{PAGE_SIZE}' -#:set UBO_GUI_SHORT_WIDTH '{SHORT_WIDTH}' +#:set UBO_GUI_PAGE_SIZE {PAGE_SIZE} +#:set UBO_GUI_SHORT_WIDTH {SHORT_WIDTH} +#:set UBO_GUI_MENU_ITEM_HEIGHT {MENU_ITEM_HEIGHT} +#:set UBO_GUI_MENU_ITEM_GAP {MENU_ITEM_GAP} """, ) diff --git a/ubo_gui/menu/menu.kv b/ubo_gui/menu/menu.kv index d36c145..6b217ff 100644 --- a/ubo_gui/menu/menu.kv +++ b/ubo_gui/menu/menu.kv @@ -1,20 +1,25 @@ -#:kivy 2.2.1 +#:kivy 2.3.0 : orientation: 'horizontal' - ScreenManager: id: screen_manager size_hint: 1, 1 - AnimatedSlider: - id: slider - min: 0 - max: root.pages - 1 - animated_value: root.pages - 1 - root.page_index + BoxLayout: orientation: 'vertical' - size_hint: (None, 1) if root.is_scrollbar_visible else (None, None) + padding: 0, root.padding_top, 0, root.padding_bottom + size_hint: (None, 1) if root.is_scrollbar_visible else (0, None) width: dp(30) if root.is_scrollbar_visible else 0 - height: 0 - opacity: 1 if root.is_scrollbar_visible else 0 - disabled: not root.is_scrollbar_visible + + AnimatedSlider: + id: slider + min: 0 + max: root.pages - 1 + animated_value: root.pages - 1 - root.page_index + orientation: 'vertical' + size_hint: (None, 1) if root.is_scrollbar_visible else (None, None) + width: dp(30) if root.is_scrollbar_visible else 0 + height: 0 + opacity: 1 if root.is_scrollbar_visible else 0 + disabled: not root.is_scrollbar_visible diff --git a/ubo_gui/menu/transitions.py b/ubo_gui/menu/transitions.py index 9a5944b..7a02dfb 100644 --- a/ubo_gui/menu/transitions.py +++ b/ubo_gui/menu/transitions.py @@ -59,7 +59,7 @@ def _handle_transition_complete( len(self.transition_queue) > 1 and transition is not self._no_transition ): - duration = 0.1 + duration = 0.08 mainthread( lambda *_: self.screen_manager.switch_to( screen, @@ -106,6 +106,8 @@ def _perform_switch( duration: float | None = None, direction: str | None = None, ) -> None: + if duration is None: + duration = 0.2 self.screen_manager.switch_to( screen, transition=transition, @@ -119,7 +121,7 @@ def _switch_to( /, *, transition: TransitionBase, - duration: float | None = None, + duration: float | None = 0.3, direction: str | None = None, ) -> None: """Switch to a new screen.""" diff --git a/ubo_gui/menu/types.py b/ubo_gui/menu/types.py index 53765ff..a020c67 100644 --- a/ubo_gui/menu/types.py +++ b/ubo_gui/menu/types.py @@ -116,6 +116,9 @@ class Item(Immutable): icon of the item is rendered and its label is hidden. Optionally it can be a callable returning the is_short value. + opacity: `float` + Between 0 and 1, sets the transparency channel of the item. + """ label: str | Callable[[], str] = '' @@ -123,6 +126,7 @@ class Item(Immutable): background_color: Color | Callable[[], Color] = PRIMARY_COLOR icon: str | None | Callable[[], str | None] = None is_short: bool | Callable[[], bool] = False + opacity: float | None = None class ActionItem(Item): diff --git a/ubo_gui/menu/widgets/header_menu_page_widget.kv b/ubo_gui/menu/widgets/header_menu_page_widget.kv index 98392af..d1f7764 100644 --- a/ubo_gui/menu/widgets/header_menu_page_widget.kv +++ b/ubo_gui/menu/widgets/header_menu_page_widget.kv @@ -1,10 +1,10 @@ -#:kivy 2.2.1 +#:kivy 2.3.0 : BoxLayout: id: layout - pos: self.pos - size: self.size + pos: 0, root.padding_bottom - dp(UBO_GUI_MENU_ITEM_HEIGHT + UBO_GUI_MENU_ITEM_GAP if root.render_surroundings else 0) + size: root.size orientation: 'vertical' Widget: @@ -56,10 +56,8 @@ markup: True BoxLayout: + id: layout height: self.minimum_height if root.items else 0 size_hint_y: None if root.items else 0 orientation: 'vertical' - - ItemWidget: - item: root.items[0] if len(root.items) > 0 else None - size_hint: 1, None + spacing: dp(UBO_GUI_MENU_ITEM_GAP) diff --git a/ubo_gui/menu/widgets/header_menu_page_widget.py b/ubo_gui/menu/widgets/header_menu_page_widget.py index 7ee5202..2cf5657 100644 --- a/ubo_gui/menu/widgets/header_menu_page_widget.py +++ b/ubo_gui/menu/widgets/header_menu_page_widget.py @@ -10,6 +10,7 @@ from kivy.properties import StringProperty from ubo_gui.menu.constants import PAGE_SIZE +from ubo_gui.menu.widgets.item_widget import ItemWidget from ubo_gui.page import PageWidget if TYPE_CHECKING: @@ -21,11 +22,10 @@ class HeaderMenuPageWidget(PageWidget): heading = StringProperty() sub_heading = StringProperty() - placeholder = StringProperty(allownone=True) def __init__( self: HeaderMenuPageWidget, - items: Sequence[Item], + items: Sequence[Item | None], **kwargs: Any, # noqa: ANN401 ) -> None: """Initialize a `HeaderMenuPageWidget`. @@ -40,10 +40,26 @@ def __init__( `Screen`. """ - if len(items) > 1: - msg = '`HeaderMenuPageWidget` is initialized with more than one item' - raise ValueError(msg) + self.bind(on_kv_post=self.render) super().__init__(items, **kwargs) + if len(items) > self.count - 2: + msg = ( + '`HeaderMenuPageWidget` is initialized with more than ' + f'`{self.count - 2}` items' + ) + raise ValueError(msg) + self.bind(items=self.render) + + def render(self: HeaderMenuPageWidget, *_: object) -> None: + """Render the widget.""" + self.ids.layout.clear_widgets() + for i in range(self.count - 2): + self.ids.layout.add_widget( + ItemWidget( + item=self.items[i] if i < len(self.items) else None, + size_hint=(1, None), + ), + ) def get_item(self: HeaderMenuPageWidget, index: int) -> Item | None: """Get the item at the given index.""" diff --git a/ubo_gui/menu/widgets/item_widget.kv b/ubo_gui/menu/widgets/item_widget.kv index 039b4bd..01c094d 100644 --- a/ubo_gui/menu/widgets/item_widget.kv +++ b/ubo_gui/menu/widgets/item_widget.kv @@ -1,7 +1,8 @@ -#:kivy 2.2.1 +#:kivy 2.3.0 : - height: dp(52) + height: dp(UBO_GUI_MENU_ITEM_HEIGHT) + opacity: root.opacity canvas.before: Color: diff --git a/ubo_gui/menu/widgets/item_widget.py b/ubo_gui/menu/widgets/item_widget.py index 382cfe9..9e3cbc3 100644 --- a/ubo_gui/menu/widgets/item_widget.py +++ b/ubo_gui/menu/widgets/item_widget.py @@ -9,6 +9,7 @@ from kivy.properties import ( BooleanProperty, ColorProperty, + NumericProperty, ObjectProperty, StringProperty, ) @@ -50,12 +51,13 @@ class ItemWidget(BoxLayout): background_color: Color = ColorProperty(PRIMARY_COLOR) icon: str = StringProperty(defaultvalue='') is_short: bool = BooleanProperty(defaultvalue=False) - item: Item = ObjectProperty() + item: Item = ObjectProperty(allownone=True) + opacity: float = NumericProperty(default=1, min=0, max=1) - def __init__(self: ItemWidget, **kwargs: dict[str, Any]) -> None: + def __init__(self: ItemWidget, item: Item | None = None, **kwargs: Any) -> None: # noqa: ANN401 """Initialize an `ItemWidget`.""" - super().__init__(**kwargs) self._subscriptions = [] + super().__init__(item=item, **kwargs) def __del__(self: ItemWidget) -> None: """Unsubscribe from the item.""" @@ -117,6 +119,8 @@ def on_item(self: ItemWidget, instance: ItemWidget, value: Item | None) -> None: lambda value: setattr(instance, 'icon', value or ''), ), ) + + instance.opacity = value.opacity or 1 else: instance.is_set = False diff --git a/ubo_gui/menu/widgets/normal_menu_page_widget.kv b/ubo_gui/menu/widgets/normal_menu_page_widget.kv index 7efcf58..bdb5fa2 100644 --- a/ubo_gui/menu/widgets/normal_menu_page_widget.kv +++ b/ubo_gui/menu/widgets/normal_menu_page_widget.kv @@ -1,10 +1,10 @@ -#:kivy 2.2.1 +#:kivy 2.3.0 : BoxLayout: id: layout - pos: self.pos - size: self.size + pos: 0, root.padding_bottom - dp(UBO_GUI_MENU_ITEM_HEIGHT + UBO_GUI_MENU_ITEM_GAP if root.render_surroundings else 0) + size: root.size orientation: 'vertical' Label: @@ -19,19 +19,8 @@ markup: True BoxLayout: + id: layout height: self.minimum_height if root.items else 0 size_hint_y: None if root.items else 0 orientation: 'vertical' - spacing: dp(7) - - ItemWidget: - item: root.items[0] if len(root.items) > 0 else None - size_hint: 1, None - - ItemWidget: - item: root.items[1] if len(root.items) > 1 else None - size_hint: 1, None - - ItemWidget: - item: root.items[2] if len(root.items) > 2 else None - size_hint: 1, None + spacing: dp(UBO_GUI_MENU_ITEM_GAP) diff --git a/ubo_gui/menu/widgets/normal_menu_page_widget.py b/ubo_gui/menu/widgets/normal_menu_page_widget.py index 4b72a45..a76da21 100644 --- a/ubo_gui/menu/widgets/normal_menu_page_widget.py +++ b/ubo_gui/menu/widgets/normal_menu_page_widget.py @@ -1,17 +1,44 @@ """Module for the `NormalMenuPageWidget` class.""" +from __future__ import annotations + import pathlib +from typing import TYPE_CHECKING, Sequence from kivy.lang.builder import Builder -from kivy.properties import StringProperty from ubo_gui.page import PageWidget +from .item_widget import ItemWidget + +if TYPE_CHECKING: + from ubo_gui.menu.types import Item + class NormalMenuPageWidget(PageWidget): """renders a normal page of a `Menu`.""" - placeholder = StringProperty(allownone=True) + def __init__( + self: PageWidget, + items: Sequence[Item | None] | None = None, + *args: object, + **kwargs: object, + ) -> None: + """Initialize `NormalMenuPageWidget`.""" + self.bind(on_kv_post=self.render) + super().__init__(items, *args, **kwargs) + self.bind(items=self.render) + + def render(self: NormalMenuPageWidget, *_: object) -> None: + """Render the widget.""" + self.ids.layout.clear_widgets() + for i in range(self.count): + self.ids.layout.add_widget( + ItemWidget( + item=self.items[i] if i < len(self.items) else None, + size_hint=(1, None), + ), + ) Builder.load_file( diff --git a/ubo_gui/notification/notification_widget.kv b/ubo_gui/notification/notification_widget.kv index ebcdf12..bcda2bb 100644 --- a/ubo_gui/notification/notification_widget.kv +++ b/ubo_gui/notification/notification_widget.kv @@ -1,15 +1,16 @@ -#:kivy 2.2.1 +#:kivy 2.3.0 : BoxLayout: orientation: 'horizontal' + padding: 0, root.padding_top, 0, root.padding_bottom spacing: dp(4) BoxLayout: size_hint: None, 1 width: dp(UBO_GUI_SHORT_WIDTH) orientation: 'vertical' - spacing: dp(7) + spacing: dp(UBO_GUI_MENU_ITEM_GAP) ItemWidget: item: root.info_action @@ -19,7 +20,7 @@ item: root.dismiss_action size_hint: 1, None - Widget: + StencilView: id: container size_hint: 1, 1 @@ -37,26 +38,22 @@ halign: 'center' valign: 'middle' color: root.color - size_hint: 1, None + size_hint: 1, None if root.icon else 0 height: dp(46) markup: True Label: font_size: dp(22) text: root.notification_title - size_hint: None, None + size_hint: None, None if root.title else 0 width: container.width text_size: self.width, None - height: self.texture_size[1] + dp(16) + height: self.texture_size[1] + dp(8) halign: 'center' valign: 'middle' strip: True markup: True - Widget: - size_hint: 1, None - height: dp(4) - Label: font_size: dp(16) text: root.content @@ -64,26 +61,28 @@ height: self.texture_size[1] size_hint: 1, None halign: 'center' + valign: 'top' shorten: False strip: True markup: True Widget: size_hint: 1, 1 + height: 0 - BoxLayout: - size_hint: None, 1 - orientation: 'vertical' - width: dp(UBO_GUI_SHORT_WIDTH) - padding: 0, dp(16), 0, dp(16) + AnchorLayout: + size_hint: (None, 1) if scrollable_widget.height - container.height else (0, None) + width: dp(UBO_GUI_SHORT_WIDTH - 2) AnimatedSlider: id: slider + anchor_x: 'center' min: 0 max: max(scrollable_widget.height - container.height, 0) animated_value: self.max orientation: 'vertical' - size_hint: (1, 1) if scrollable_widget.height - container.height > 0 else (None, None) + size_hint: (None, 1) if scrollable_widget.height - container.height > 0 else (None, None) + width: dp(30) if scrollable_widget.height - container.height > 0 else 0 height: root.height opacity: 1 if scrollable_widget.height - container.height > 0 else 0 disabled: scrollable_widget.height - container.height <= 0 diff --git a/ubo_gui/page/__init__.py b/ubo_gui/page/__init__.py index c856880..8885b78 100644 --- a/ubo_gui/page/__init__.py +++ b/ubo_gui/page/__init__.py @@ -5,7 +5,12 @@ import warnings from typing import TYPE_CHECKING, Self, Sequence -from kivy.properties import ListProperty, StringProperty +from kivy.properties import ( + BooleanProperty, + ListProperty, + NumericProperty, + StringProperty, +) from kivy.uix.screenmanager import Screen if TYPE_CHECKING: @@ -20,8 +25,17 @@ class PageWidget(Screen): __events__ = ('on_close',) - items: Sequence[Item] = ListProperty([]) + items: Sequence[Item | None] = ListProperty([]) title: str | None = StringProperty(allownone=True, defaultvalue=None) + count = NumericProperty(defaultvalue=PAGE_MAX_ITEMS) + offset = NumericProperty(defaultvalue=0) + placeholder = StringProperty(allownone=True) + render_surroundings = BooleanProperty( + default=False, + cache=True, + ) + padding_top = NumericProperty(default=0) + padding_bottom = NumericProperty(default=0) def go_up(self: Self) -> None: """Implement this method to provide custom logic for up key.""" @@ -41,7 +55,7 @@ def go_back(self: Self) -> bool | None: def __init__( self: PageWidget, - items: Sequence[Item] | None = None, + items: Sequence[Item | None] | None = None, *args: object, **kwargs: object, ) -> None: @@ -60,15 +74,16 @@ def __init__( `Screen`. """ - if items and len(items) > PAGE_MAX_ITEMS: - msg = f"""`PageWidget` is initialized with more than `MAX_ITEMS`={ - PAGE_MAX_ITEMS} items""" - raise ValueError(msg) self.items = items or [] super().__init__(*args, **kwargs) + if items and len(items) > self.count: + msg = f"""`PageWidget` is initialized with more than `MAX_ITEMS`={ + self.count} items""" + raise ValueError(msg) def get_item(self: PageWidget, index: int) -> Item | None: """Get the page item at the given index.""" + index += self.offset if not 0 <= index < len(self.items): msg = f"""index must be greater than or equal to 0 and less than { len(self.items)}""" diff --git a/ubo_gui/prompt/prompt_widget.kv b/ubo_gui/prompt/prompt_widget.kv index 2afde42..4b0d1a9 100644 --- a/ubo_gui/prompt/prompt_widget.kv +++ b/ubo_gui/prompt/prompt_widget.kv @@ -1,14 +1,14 @@ -#:kivy 2.2.1 +#:kivy 2.3.0 : BoxLayout: id: layout + padding: 0, root.padding_top, 0, root.padding_bottom orientation: 'vertical' - spacing: dp(7) + spacing: dp(UBO_GUI_MENU_ITEM_GAP) Label: - padding: dp(50), 0, dp(50), 0 - text: root.heading + padding: dp(10), 0, dp(10), 0 text_size: self.size halign: 'center' valign: 'bottom' @@ -20,16 +20,17 @@ markup: True Label: + padding: dp(10), 0, dp(10), 0 text: root.prompt - text_size: self.size + text_size: self.size[0], None font_size: dp(18) halign: 'center' valign: 'middle' color: 1, 1, 1 size_hint: 1, None height: dp(20) - shorten: True - shorten_from: 'right' + shorten: False + strip: True markup: True ItemWidget: diff --git a/ubo_gui/volume/volume_widget.kv b/ubo_gui/volume/volume_widget.kv index 48a7aea..7ab3b9b 100644 --- a/ubo_gui/volume/volume_widget.kv +++ b/ubo_gui/volume/volume_widget.kv @@ -1,4 +1,4 @@ -#:kivy 2.2.1 +#:kivy 2.3.0 : canvas.before: