diff --git a/adafruit_displayio_layout/layouts/page_layout.py b/adafruit_displayio_layout/layouts/page_layout.py index bee92ba..1dc1798 100644 --- a/adafruit_displayio_layout/layouts/page_layout.py +++ b/adafruit_displayio_layout/layouts/page_layout.py @@ -53,11 +53,11 @@ def __init__( self.x = x self.y = y - self._page_content_list = [] + self.page_content_list = [] self._cur_showing_index = 0 def add_content(self, page_content, page_name=None): - """Add a child to the grid. + """Add a child to the page layout. :param page_content: the content for the page typically a Group :param page_name: the name of this page @@ -72,10 +72,10 @@ def add_content(self, page_content, page_name=None): "page_name": page_name, } - if len(self._page_content_list) > 0: + if len(self.page_content_list) > 0: _page_group.hidden = True - self._page_content_list.append(sub_view_obj) + self.page_content_list.append(sub_view_obj) self.append(_page_group) def _check_args(self, page_name, page_index): @@ -95,16 +95,16 @@ def _check_args(self, page_name, page_index): ) if page_index is not None: - if page_index >= len(self._page_content_list): + if page_index >= len(self.page_content_list): raise KeyError( "KeyError at index {} in list length {}".format( - page_index, len(self._page_content_list) + page_index, len(self.page_content_list) ), ) if page_name is not None: _found = False - for page in self._page_content_list: + for page in self.page_content_list: if not _found: if page_name == page["page_name"]: _found = True @@ -125,10 +125,10 @@ def get_page(self, page_name=None, page_index=None): self._check_args(page_name, page_index) if page_index is not None: - return self._page_content_list[page_index] + return self.page_content_list[page_index] if page_name is not None: - for cell in self._page_content_list: + for cell in self.page_content_list: if cell["page_name"] == page_name: return cell @@ -149,7 +149,7 @@ def show_page(self, page_name=None, page_index=None): self._check_args(page_name, page_index) - for cur_index, page in enumerate(self._page_content_list): + for cur_index, page in enumerate(self.page_content_list): if page_name is not None: if page["page_name"] == page_name: self._cur_showing_index = cur_index @@ -182,7 +182,7 @@ def showing_page_name(self): Name of the currently showing page :return string: showing_page_name """ - return self._page_content_list[self._cur_showing_index]["page_name"] + return self.page_content_list[self._cur_showing_index]["page_name"] @showing_page_name.setter def showing_page_name(self, new_name): @@ -194,7 +194,7 @@ def showing_page_content(self): The content object for the currently showing page :return Displayable: showing_page_content """ - return self._page_content_list[self._cur_showing_index]["content"][0] + return self.page_content_list[self._cur_showing_index]["content"][0] def next_page(self, loop=True): """ @@ -203,7 +203,7 @@ def next_page(self, loop=True): :return: None """ - if self._cur_showing_index + 1 < len(self._page_content_list): + if self._cur_showing_index + 1 < len(self.page_content_list): self.show_page(page_index=self._cur_showing_index + 1) else: if not loop: @@ -223,4 +223,4 @@ def previous_page(self, loop=True): if not loop: print("No more pages") else: - self.show_page(page_index=len(self._page_content_list) - 1) + self.show_page(page_index=len(self.page_content_list) - 1) diff --git a/adafruit_displayio_layout/layouts/tab_layout.py b/adafruit_displayio_layout/layouts/tab_layout.py new file mode 100644 index 0000000..e3ee2ca --- /dev/null +++ b/adafruit_displayio_layout/layouts/tab_layout.py @@ -0,0 +1,288 @@ +# SPDX-FileCopyrightText: 2022 Tim Cocks +# +# SPDX-License-Identifier: MIT + +""" +`tab_layout` +================================================================================ + +A layout that organizes pages into tabs. + + +* Author(s): Tim Cocks + +Implementation Notes +-------------------- + +**Hardware:** + +**Software and Dependencies:** + +* Adafruit CircuitPython firmware for the supported boards: + https://github.com/adafruit/circuitpython/releases + +""" +try: + from typing import Optional, Union, Tuple + from fontio import BuiltinFont + from adafruit_bitmap_font.bdf import BDF + from adafruit_bitmap_font.pcf import PCF +except ImportError: + pass + +import terminalio +import displayio +import adafruit_imageload +from adafruit_display_text.bitmap_label import Label +from adafruit_imageload.tilegrid_inflator import inflate_tilegrid +from adafruit_displayio_layout.layouts.page_layout import PageLayout + + +class TabLayout(displayio.Group): + """ + A layout that organizes children into a grid table structure. + + .. warning:: + Requires CircuitPython version 7.3.0-beta.2 or newer + + :param int x: x location the layout should be placed. Pixel coordinates. + :param int y: y location the layout should be placed. Pixel coordinates. + :param displayio.Display display: The Display object to show the tab layout on. + :param int tab_text_scale: Size of the text shown in the tabs. + Whole numbers 1 and greater are valid + :param Optional[Union[BuiltinFont, BDF, PCF]] custom_font: A pre-loaded font object to use + for the tab labels + :param str inactive_tab_spritesheet: Filepath of the spritesheet to show for inactive tabs. + :param str showing_tab_spritesheet: Filepath of the spritesheet to show for the active tab. + :param Optional[int, tuple[int, int, int]] showing_tab_text_color: Hex or tuple color to use + for the active tab label + :param Optional[int, tuple[int, int, int]] inactive_tab_text_color: Hex or tuple color to + use for inactive tab labels + :param Optional[Union[int, tuple[int, int]]] inactive_tab_transparent_indexes: single index + or tuple of multiple indexes to be made transparent in the inactive tab sprite palette. + :param Optional[Union[int, tuple[int, int]]] showing_tab_transparent_indexes: single index + or tuple of multiple indexes to be made transparent in the active tab sprite palette. + :param int tab_count: How many tabs to draw in the layout. Positive whole numbers are valid. + """ + + # pylint: disable=too-many-instance-attributes, too-many-arguments, invalid-name, too-many-branches + + def __init__( + self, + x: int = 0, + y: int = 0, + display: Optional[displayio.Display] = None, + tab_text_scale: int = 1, + custom_font: Optional[Union[BuiltinFont, BDF, PCF]] = terminalio.FONT, + inactive_tab_spritesheet: Optional[str] = None, + showing_tab_spritesheet: Optional[str] = None, + showing_tab_text_color: Optional[Union[int, Tuple[int, int, int]]] = 0x999999, + inactive_tab_text_color: Optional[Union[int, Tuple[int, int, int]]] = 0xFFFFF, + inactive_tab_transparent_indexes: Optional[Union[int, Tuple[int, int]]] = None, + showing_tab_transparent_indexes: Optional[Union[int, Tuple[int, int]]] = None, + tab_count: int = None, + ): + + if display is None: + # pylint: disable=import-outside-toplevel + import board + + if hasattr(board, "DISPLAY"): + display = board.DISPLAY + if inactive_tab_spritesheet is None: + raise AttributeError("Must pass active_tab_spritesheet") + if showing_tab_spritesheet is None: + raise AttributeError("Must pass inactive_tab_spritesheet") + if tab_count is None: + raise AttributeError("Must pass tab_count") + + super().__init__(x=x, y=y) + self.tab_count = tab_count + self._active_bmp, self._active_palette = adafruit_imageload.load( + showing_tab_spritesheet + ) + self._inactive_bmp, self._inactive_palette = adafruit_imageload.load( + inactive_tab_spritesheet + ) + + if isinstance(showing_tab_transparent_indexes, int): + self._active_palette.make_transparent(showing_tab_transparent_indexes) + elif isinstance(showing_tab_transparent_indexes, tuple): + for index in showing_tab_transparent_indexes: + self._active_palette.make_transparent(index) + else: + raise AttributeError("active_tab_transparent_indexes must be int or tuple") + + if isinstance(inactive_tab_transparent_indexes, int): + self._inactive_palette.make_transparent(inactive_tab_transparent_indexes) + elif isinstance(inactive_tab_transparent_indexes, tuple): + for index in inactive_tab_transparent_indexes: + self._inactive_palette.make_transparent(index) + else: + raise AttributeError( + "inactive_tab_transparent_indexes must be int or tuple" + ) + + self.tab_height = self._active_bmp.height + self.display = display + self.active_tab_text_color = showing_tab_text_color + self.inactive_tab_text_color = inactive_tab_text_color + self.custom_font = custom_font + self.tab_text_scale = tab_text_scale + self.tab_group = displayio.Group() + self.tab_dict = {} + self.page_layout = PageLayout(x=x, y=y + self.tab_height) + + self.append(self.tab_group) + self.append(self.page_layout) + + def _draw_tabs(self): + for i, page_dict in enumerate(self.page_layout.page_content_list): + if i not in self.tab_dict: + print(f"creating tab {i}") + _new_tab_group = displayio.Group() + _tab_tilegrid = inflate_tilegrid( + bmp_obj=self._inactive_bmp, + bmp_palette=self._inactive_palette, + target_size=( + (self.display.width // self.tab_count) + // (self._active_bmp.width // 3), + 3, + ), + ) + + _tab_tilegrid.x = (self.display.width // self.tab_count) * i + _new_tab_group.append(_tab_tilegrid) + + _tab_label = Label( + self.custom_font, + text=page_dict["page_name"], + color=self.inactive_tab_text_color, + scale=self.tab_text_scale, + ) + + _tab_label.anchor_point = (0.5, 0.5) + _tab_label.anchored_position = ( + _tab_tilegrid.x + + ((_tab_tilegrid.width * _tab_tilegrid.tile_width) // 2), + (_tab_tilegrid.height * _tab_tilegrid.tile_height) // 2, + ) + _new_tab_group.append(_tab_label) + + if i == self.page_layout.showing_page_index: + try: + _tab_tilegrid.bitmap = self._active_bmp + except AttributeError as e: + print(e) + raise ( + AttributeError( + "TabLayout requires CircuitPython version 7.3.0-beta.2 or newer." + ) + ) from e + _tab_tilegrid.pixel_shader = self._active_palette + _tab_label.color = self.active_tab_text_color + self.tab_dict[i] = _new_tab_group + self.tab_group.append(_new_tab_group) + + def _update_active_tab(self): + for i in range(len(self.page_layout)): + if i == self.page_layout.showing_page_index: + self.tab_group[i][0].bitmap = self._active_bmp + self.tab_group[i][0].pixel_shader = self._active_palette + self.tab_group[i][1].color = self.active_tab_text_color + else: + self.tab_group[i][0].bitmap = self._inactive_bmp + self.tab_group[i][0].pixel_shader = self._inactive_palette + self.tab_group[i][1].color = self.inactive_tab_text_color + + def add_content(self, tab_content, tab_name): + """Add a child to the tab layout. + + :param tab_content: the content for the tab typically a Group + :param tab_name: the name of this tab, will be shown inside the tab + + :return: None""" + self.page_layout.add_content(tab_content, tab_name) + self._draw_tabs() + + def show_page(self, page_name=None, page_index=None): + """ + Show the specified page, and hide all other pages. + + :param string page_name: The name of a page to show + :param int page_index: The index of a page to show + :return: None + """ + + self.page_layout.show_page(page_name=page_name, page_index=page_index) + self._update_active_tab() + + @property + def showing_page_index(self): + """ + Index of the currently showing page + :return int: showing_page_index + """ + return self.page_layout.showing_page_index + + @showing_page_index.setter + def showing_page_index(self, new_index): + if self.showing_page_index != new_index: + self.show_page(page_index=new_index) + + @property + def showing_page_name(self): + """ + Name of the currently showing page + :return string: showing_page_name + """ + return self.page_layout.showing_page_name + + @showing_page_name.setter + def showing_page_name(self, new_name): + self.show_page(page_name=new_name) + + @property + def showing_page_content(self): + """ + The content object for the currently showing page + :return Displayable: showing_page_content + """ + return self.page_layout.showing_page_content + + def next_page(self, loop=True): + """ + Hide the current page and show the next one in the list by index + :param bool loop: whether to loop from the last page back to the first + :return: None + """ + + self.page_layout.next_page(loop=loop) + self._update_active_tab() + + def previous_page(self, loop=True): + """ + Hide the current page and show the previous one in the list by index + :param bool loop: whether to loop from the first page to the last one + :return: None + """ + self.page_layout.previous_page(loop=loop) + self._update_active_tab() + + def handle_touch_events(self, touch_event): + """ + Check if the touch event is on the tabs and if so change to the touched tab. + + :param tuple touch_event: tuple containing x and y coordinates of the + touch event in indexes 0 and 1. + :return: None + """ + + if touch_event: + if 0 <= touch_event[1] <= self.tab_height: + + touched_tab_index = touch_event[0] // ( + self.display.width // self.tab_count + ) + print(f"{touch_event[0]} - {touched_tab_index}") + self.showing_page_index = touched_tab_index diff --git a/docs/api.rst b/docs/api.rst index b517167..a707e30 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,6 +6,18 @@ .. automodule:: adafruit_displayio_layout.layouts.grid_layout :members: + :private-members: + :member-order: bysource + +.. automodule:: adafruit_displayio_layout.layouts.page_layout + :members: + :private-members: + :member-order: bysource + +.. automodule:: adafruit_displayio_layout.layouts.tab_layout + :members: + :private-members: + :member-order: bysource .. automodule:: adafruit_displayio_layout.widgets.widget :members: diff --git a/examples/bmps/active_tab_sprite.bmp b/examples/bmps/active_tab_sprite.bmp new file mode 100644 index 0000000..d97d1db Binary files /dev/null and b/examples/bmps/active_tab_sprite.bmp differ diff --git a/examples/bmps/active_tab_sprite.bmp.license b/examples/bmps/active_tab_sprite.bmp.license new file mode 100644 index 0000000..8f7990c --- /dev/null +++ b/examples/bmps/active_tab_sprite.bmp.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2022 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT diff --git a/examples/bmps/inactive_tab_sprite.bmp b/examples/bmps/inactive_tab_sprite.bmp new file mode 100644 index 0000000..ab41509 Binary files /dev/null and b/examples/bmps/inactive_tab_sprite.bmp differ diff --git a/examples/bmps/inactive_tab_sprite.bmp.license b/examples/bmps/inactive_tab_sprite.bmp.license new file mode 100644 index 0000000..8f7990c --- /dev/null +++ b/examples/bmps/inactive_tab_sprite.bmp.license @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: 2022 Tim Cocks for Adafruit Industries +# SPDX-License-Identifier: MIT diff --git a/examples/displayio_layout_tab_layout_simpletest.py b/examples/displayio_layout_tab_layout_simpletest.py new file mode 100644 index 0000000..20ad427 --- /dev/null +++ b/examples/displayio_layout_tab_layout_simpletest.py @@ -0,0 +1,148 @@ +# SPDX-FileCopyrightText: 2022 Tim C +# +# SPDX-License-Identifier: MIT +""" +Make a TabLayout and illustrate the most basic features and usage. +""" +import time +import displayio +import board +import terminalio +from adafruit_display_text.bitmap_label import Label +from adafruit_display_shapes.rect import Rect +from adafruit_display_shapes.circle import Circle +from adafruit_display_shapes.triangle import Triangle +from adafruit_displayio_layout.layouts.tab_layout import TabLayout + +CHANGE_DELAY = 1.0 # Seconds to wait before auto-advancing to the next tab + +# built-in display +display = board.DISPLAY + +# create and show main_group +main_group = displayio.Group() +display.show(main_group) + +font = terminalio.FONT + +# create the page layout +test_page_layout = TabLayout( + x=0, + y=0, + display=board.DISPLAY, + tab_text_scale=2, + custom_font=font, + inactive_tab_spritesheet="bmps/inactive_tab_sprite.bmp", + showing_tab_spritesheet="bmps/active_tab_sprite.bmp", + showing_tab_text_color=0x00AA59, + inactive_tab_text_color=0xEEEEEE, + inactive_tab_transparent_indexes=(0, 1), + showing_tab_transparent_indexes=(0, 1), + tab_count=4, +) + +# make page content Groups +page_1_group = displayio.Group() +page_2_group = displayio.Group() +page_3_group = displayio.Group() +page_4_group = displayio.Group() + +# labels +page_1_lbl = Label( + font=terminalio.FONT, + scale=2, + text="This is the first page!", + anchor_point=(0, 0), + anchored_position=(10, 10), +) +page_2_lbl = Label( + font=terminalio.FONT, + scale=2, + text="This page is the\nsecond page!", + anchor_point=(0, 0), + anchored_position=(10, 10), +) +page_3_lbl = Label( + font=terminalio.FONT, + scale=2, + text="The third page is fun!", + anchor_point=(0, 0), + anchored_position=(10, 10), +) + +page_4_lbl = Label( + font=terminalio.FONT, + scale=2, + text="The fourth page\nis where it's at", + anchor_point=(0, 0), + anchored_position=(10, 10), +) + +# shapes +square = Rect(x=20, y=70, width=40, height=40, fill=0x00DD00) +circle = Circle(50, 120, r=30, fill=0xDD00DD) +triangle = Triangle(50, 0, 100, 50, 0, 50, fill=0xDDDD00) +rectangle = Rect(x=80, y=80, width=100, height=50, fill=0x0000DD) + +triangle.x = 80 +triangle.y = 70 + +# add everything to their page groups +page_1_group.append(square) +page_1_group.append(page_1_lbl) +page_2_group.append(page_2_lbl) +page_2_group.append(circle) +page_3_group.append(page_3_lbl) +page_3_group.append(triangle) +page_4_group.append(page_4_lbl) +page_4_group.append(rectangle) + +# add the pages to the layout, supply your own page names +test_page_layout.add_content(page_1_group, "One") +test_page_layout.add_content(page_2_group, "Two") +test_page_layout.add_content(page_3_group, "Thr") +test_page_layout.add_content(page_4_group, "For") + +# add it to the group that is showing on the display +main_group.append(test_page_layout) + +# change page with function by name +test_page_layout.show_page(page_name="Thr") +print("showing page index:{}".format(test_page_layout.showing_page_index)) +time.sleep(1) + +# change page with function by index +test_page_layout.show_page(page_index=0) +print("showing page name: {}".format(test_page_layout.showing_page_name)) +time.sleep(1) + +# change page by updating the page name property +test_page_layout.showing_page_name = "Thr" +print("showing page index: {}".format(test_page_layout.showing_page_index)) +time.sleep(1) + +# change page by updating the page index property +test_page_layout.showing_page_index = 1 +print("showing page name: {}".format(test_page_layout.showing_page_name)) +time.sleep(5) + +another_text = Label( + terminalio.FONT, + text="And another thing!", + scale=2, + color=0x00FF00, + anchor_point=(0, 0), + anchored_position=(100, 100), +) +test_page_layout.showing_page_content.append(another_text) + +print("starting loop") + +prev_change_time = time.monotonic() + +while True: + now = time.monotonic() + if prev_change_time + CHANGE_DELAY <= now: + prev_change_time = now + # change page by next page function. It will loop by default + test_page_layout.next_page() diff --git a/examples/displayio_layout_tab_layout_touchtest.py b/examples/displayio_layout_tab_layout_touchtest.py new file mode 100644 index 0000000..47c5cbd --- /dev/null +++ b/examples/displayio_layout_tab_layout_touchtest.py @@ -0,0 +1,137 @@ +# SPDX-FileCopyrightText: 2022 Tim C +# +# SPDX-License-Identifier: MIT +""" +Make a TabLayout change tabs with the touchscreen +""" +import displayio +import board +import terminalio +import adafruit_touchscreen +from adafruit_display_text.bitmap_label import Label +from adafruit_display_shapes.rect import Rect +from adafruit_display_shapes.circle import Circle +from adafruit_display_shapes.triangle import Triangle +from adafruit_displayio_layout.layouts.tab_layout import TabLayout + +# built-in display +display = board.DISPLAY + +# ------------ Touchscreen setup --------------- # +# See: https://learn.adafruit.com/making-a-pyportal-user-interface-displayio/display +display = board.DISPLAY # create the display object + +screen_width = display.width +screen_height = display.height +ts = adafruit_touchscreen.Touchscreen( + board.TOUCH_XL, + board.TOUCH_XR, + board.TOUCH_YD, + board.TOUCH_YU, + calibration=((5200, 59000), (5800, 57000)), + size=(screen_width, screen_height), +) + +# create and show main_group +main_group = displayio.Group() +display.show(main_group) + +font = terminalio.FONT + +# create the page layout +test_page_layout = TabLayout( + x=0, + y=0, + display=board.DISPLAY, + tab_text_scale=2, + custom_font=font, + inactive_tab_spritesheet="bmps/inactive_tab_sprite.bmp", + showing_tab_spritesheet="bmps/active_tab_sprite.bmp", + showing_tab_text_color=0x00AA59, + inactive_tab_text_color=0xEEEEEE, + inactive_tab_transparent_indexes=(0, 1), + showing_tab_transparent_indexes=(0, 1), + tab_count=4, +) + +# make page content Groups +page_1_group = displayio.Group() +page_2_group = displayio.Group() +page_3_group = displayio.Group() +page_4_group = displayio.Group() + +# labels +page_1_lbl = Label( + font=terminalio.FONT, + scale=2, + text="This is the first page!", + anchor_point=(0, 0), + anchored_position=(10, 10), +) +page_2_lbl = Label( + font=terminalio.FONT, + scale=2, + text="This page is the\nsecond page!", + anchor_point=(0, 0), + anchored_position=(10, 10), +) +page_3_lbl = Label( + font=terminalio.FONT, + scale=2, + text="The third page is fun!", + anchor_point=(0, 0), + anchored_position=(10, 10), +) + +page_4_lbl = Label( + font=terminalio.FONT, + scale=2, + text="The fourth page\nis where it's at", + anchor_point=(0, 0), + anchored_position=(10, 10), +) + +# shapes +square = Rect(x=20, y=70, width=40, height=40, fill=0x00DD00) +circle = Circle(50, 120, r=30, fill=0xDD00DD) +triangle = Triangle(50, 0, 100, 50, 0, 50, fill=0xDDDD00) +rectangle = Rect(x=80, y=80, width=100, height=50, fill=0x0000DD) + +triangle.x = 80 +triangle.y = 70 + +# add everything to their page groups +page_1_group.append(square) +page_1_group.append(page_1_lbl) +page_2_group.append(page_2_lbl) +page_2_group.append(circle) +page_3_group.append(page_3_lbl) +page_3_group.append(triangle) +page_4_group.append(page_4_lbl) +page_4_group.append(rectangle) + +# add the pages to the layout, supply your own page names +test_page_layout.add_content(page_1_group, "One") +test_page_layout.add_content(page_2_group, "Two") +test_page_layout.add_content(page_3_group, "Thr") +test_page_layout.add_content(page_4_group, "For") + +# add it to the group that is showing on the display +main_group.append(test_page_layout) + + +# add something new after the TabLayout was already created +another_text = Label( + terminalio.FONT, + text="And another thing!", + scale=2, + color=0x00FF00, + anchor_point=(0, 0), + anchored_position=(100, 100), +) +test_page_layout.showing_page_content.append(another_text) + +while True: + touch = ts.touch_point + if touch: + test_page_layout.handle_touch_events(touch)