Skip to content

Commit

Permalink
Merge pull request #24 from ubopod/callable-properties
Browse files Browse the repository at this point in the history
feat: add a mechanism to sync properties with subscribable values (defined in python-redux). applied for these properties:
  • Loading branch information
sassanh authored Jan 26, 2024
2 parents c121310 + af332e9 commit 1d6fc89
Show file tree
Hide file tree
Showing 8 changed files with 559 additions and 316 deletions.
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,31 @@
# Changelog

## Version 0.8.0

- feat: add a mechanism to sync properties with subscribable values (defined in python-redux).
applied for these properties:
- `MenuWidget`:
1. `application`
1. `sub_menu`
1. `heading` of headed menu
1. `sub_heading` of headed menu
1. `items` of menu
1. `title` of menu
- `ItemWidget`:
1. `label`
1. `is_short`
1. `color`
1. `background_color`
1. `icon`

## Version 0.7.9

- fix: don't call `application` if it's a subclass of `PageWidget`

## Version 0.7.8

- feat: make all fields of `Menu` and `Item` and their sub classes accept callables

## Version 0.7.7

- fix: dispatch `on_close` when a `PageWidget` is left (`on_leave` is dispatched)
Expand Down
464 changes: 237 additions & 227 deletions poetry.lock

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "ubo-gui"
version = "0.7.7"
version = "0.8.0"
description = "GUI sdk for Ubo Pod"
authors = ["Sassan Haradji <[email protected]>"]
license = "Apache-2.0"
Expand All @@ -17,7 +17,9 @@ priority = "primary"
[tool.poetry.dependencies]
python = "^3.11"
headless-kivy-pi = [
{ version = "^0.5.3" },
{ version = "^0.5.3", markers = "extra=='default'", extras = [
'default',
] },
{ version = "^0.5.3", markers = "extra=='dev'", extras = [
'dev',
] },
Expand All @@ -26,6 +28,7 @@ python-immutable = "^1.0.2"

[tool.poetry.extras]
dev = ['headless-kivy-pi']
default = ['headless-kivy-pi']

[build-system]
requires = ["poetry-core"]
Expand All @@ -52,3 +55,6 @@ multiline-quotes = "double"

[tool.ruff.format]
quote-style = 'single'

[tool.isort]
profile = "black"
145 changes: 105 additions & 40 deletions ubo_gui/menu/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@
HeadedMenu,
Item,
SubMenuItem,
menu_items,
menu_title,
process_subscribable_value,
)

if TYPE_CHECKING:
Expand All @@ -51,12 +50,14 @@
class MenuWidget(BoxLayout):
"""Paginated menu."""

_subscriptions: list[Callable[[], None]]

_title: str | None = None
_current_menu: Menu | None = None
_current_menu_items: Sequence[Item]
_current_application: PageWidget | None = None
screen_manager: ScreenManager
slider: AnimatedSlider
cancel_items_subscription: Callable[[], None] | None = None

def _handle_transition_progress(
self: MenuWidget,
Expand Down Expand Up @@ -98,10 +99,15 @@ def _swap_transition(self: MenuWidget) -> SwapTransition:
self._setup_transition(transition)
return transition

def __init__(self: MenuWidget, **kwargs: Any) -> None: # noqa: ANN401
def __init__(self: MenuWidget, **kwargs: dict[str, Any]) -> None:
"""Initialize a `MenuWidget`."""
self._current_menu_items = []
super().__init__(**kwargs)
self._subscriptions = []
self._current_menu_items = []

def __del__(self: MenuWidget) -> None:
"""Unsubscribe from the item."""
self.clear_subscriptions()

def set_root_menu(self: MenuWidget, root_menu: Menu) -> None:
"""Set the root menu."""
Expand All @@ -124,12 +130,17 @@ def get_pages(self: MenuWidget) -> int:
return math.ceil((len(self.current_menu_items) + 2) / 3)
return math.ceil(len(self.current_menu_items) / 3)

def set_title(self: MenuWidget, title: str) -> bool:
"""Set the title of the currently active menu."""
self._title = title
return True

def get_title(self: MenuWidget) -> str | None:
"""Return the title of the currently active menu."""
if self.current_application:
return getattr(self.current_application, 'title', None)
if self.current_menu:
return menu_title(self.current_menu)
return self._title
return None

def go_down(self: MenuWidget) -> None:
Expand Down Expand Up @@ -182,15 +193,12 @@ def select(self: MenuWidget, index: int) -> None:
return
current_page = cast(PageWidget, self.screen_manager.current_screen)
item = current_page.get_item(index)
if not item:
warnings.warn('Selected `item` is `None`', RuntimeWarning, stacklevel=1)
return

if isinstance(item, ActionItem):
item = item.action()
if not item:
return
if isinstance(item, type):
if isinstance(item, type) and issubclass(item, PageWidget):
application_instance = item(name=uuid.uuid4().hex)
self.open_application(application_instance)
else:
Expand All @@ -201,18 +209,37 @@ def select(self: MenuWidget, index: int) -> None:
transition=self._slide_transition,
direction='left',
)
elif isinstance(item, ApplicationItem):
application_instance = item.application(name=uuid.uuid4().hex)
self.open_application(application_instance)
elif isinstance(item, SubMenuItem):
if isinstance(item, ApplicationItem):

def handle_application_change(application: type[PageWidget]) -> None:
application_instance = application(name=uuid.uuid4().hex)
self.open_application(application_instance)

self._subscriptions.append(
process_subscribable_value(item.application, handle_application_change),
)

if isinstance(item, SubMenuItem):
self.push_menu()
self.current_menu = item.sub_menu
if self.current_screen:
self.screen_manager.switch_to(
self.current_screen,
transition=self._slide_transition,
direction='left',
)

is_first_transition = True

def handle_sub_menu_change(sub_menu: Menu) -> None:
nonlocal is_first_transition
self.current_menu = sub_menu
if self.current_screen:
self.screen_manager.switch_to(
self.current_screen,
transition=self._slide_transition
if is_first_transition
else self._no_transition,
direction='left',
)
is_first_transition = False

self._subscriptions.append(
process_subscribable_value(item.sub_menu, handle_sub_menu_change),
)

def go_back(self: MenuWidget) -> None:
"""Go back to the previous menu."""
Expand All @@ -230,12 +257,27 @@ def get_current_screen(self: MenuWidget) -> Screen | None:
return None

if self.page_index == 0 and isinstance(self.current_menu, HeadedMenu):
return HeaderMenuPageWidget(
header_menu_page_widget = HeaderMenuPageWidget(
self.current_menu_items[:1],
self.current_menu.heading,
self.current_menu.sub_heading,
name=f'Page {self.get_depth()} 0',
)
self._subscriptions.append(
process_subscribable_value(
self.current_menu.heading,
lambda value: setattr(header_menu_page_widget, 'heading', value),
),
)
self._subscriptions.append(
process_subscribable_value(
self.current_menu.sub_heading,
lambda value: setattr(
header_menu_page_widget,
'sub_heading',
value,
),
),
)
return header_menu_page_widget

offset = -(PAGE_SIZE - 1) if isinstance(self.current_menu, HeadedMenu) else 0
return NormalMenuPageWidget(
Expand Down Expand Up @@ -320,6 +362,7 @@ def set_current_application(
self._current_application = application
if application:
self.current_menu = None
self.clear_subscriptions()

return True

Expand All @@ -338,30 +381,45 @@ def get_current_menu(self: MenuWidget) -> Menu | None:

def set_current_menu(self: MenuWidget, menu: Menu | None) -> bool:
"""Set the current menu."""
self._current_menu_items = menu_items(menu)
if self.cancel_items_subscription:
self.cancel_items_subscription()
self.cancel_items_subscription = None
if menu and hasattr(menu.items, 'subscribe'):

def refresh_items(items: Sequence[Item]) -> None:
self.current_menu_items = items
self._current_menu = menu

if not menu:
self._current_menu_items = []
self.page_index = 0
return True

self.current_application = None
self.clear_subscriptions()

is_first_transition = True

def refresh_items(items: Sequence[Item]) -> None:
nonlocal is_first_transition
self.current_menu_items = items
if not is_first_transition:
self.screen_manager.switch_to(
self.current_screen,
transition=self._no_transition,
)
is_first_transition = False

self.cancel_items_subscription = cast(Any, menu.items).subscribe(
self._subscriptions.append(
process_subscribable_value(
menu.items,
refresh_items,
)

self._current_menu = menu
),
)

if not menu:
self.page_index = 0
return True
def handle_title_change(title: str) -> None:
if self._title != title:
self.title = title

self.current_application = None
self._subscriptions.append(
process_subscribable_value(
menu.title,
handle_title_change,
),
)

pages = self.get_pages()
if self.page_index >= pages:
Expand All @@ -375,10 +433,17 @@ def on_kv_post(self: MenuWidget, base_widget: Any) -> None: # noqa: ANN401
self.screen_manager = cast(ScreenManager, self.ids.screen_manager)
self.slider = self.ids.slider

def clear_subscriptions(self: MenuWidget) -> None:
"""Clear the subscriptions."""
for subscription in self._subscriptions:
subscription()
self._subscriptions.clear()

page_index = NumericProperty(0)
stack: list[tuple[Menu, int] | PageWidget] = ListProperty()
title = AliasProperty(
getter=get_title,
setter=set_title,
bind=['current_menu', 'current_application'],
cache=True,
)
Expand Down
6 changes: 6 additions & 0 deletions ubo_gui/menu/header_menu_page_widget.kv
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#:kivy 2.2.1

<HeaderMenuPageWidget>:
canvas:
Color:
rgba: 1, 0, 0, .2
Rectangle:
pos: self.pos
size: self.size
BoxLayout:
id: layout
pos: self.pos
Expand Down
10 changes: 6 additions & 4 deletions ubo_gui/menu/header_menu_page_widget.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Module for the `HeaderMenuPageWidget` class."""
from __future__ import annotations

import pathlib
Expand All @@ -15,23 +16,23 @@


class HeaderMenuPageWidget(PageWidget):
"""renders a header page of a `Menu`."""
"""Renders a header page of a `Menu`."""

heading = StringProperty()
sub_heading = StringProperty()

def __init__(
self: HeaderMenuPageWidget,
items: Sequence[Item],
heading: str,
sub_heading: str,
heading: str = '',
sub_heading: str = '',
**kwargs: Any, # noqa: ANN401
) -> None:
"""Initialize a `HeaderMenuPageWidget`.
Parameters
----------
item: `Item`
items: `Sequence`[[`Item`]]
The item to be shown in this page
heading: `str`
Expand All @@ -52,6 +53,7 @@ def __init__(
self.sub_heading = sub_heading

def get_item(self: HeaderMenuPageWidget, index: int) -> Item | None:
"""Get the item at the given index."""
if index != PAGE_SIZE - 1:
warnings.warn(
f'index must be {PAGE_SIZE - 1}',
Expand Down
Loading

0 comments on commit 1d6fc89

Please sign in to comment.