From 070968c7728cb6c23f3226df073c18f0b07c704f Mon Sep 17 00:00:00 2001 From: Spencer Brown Date: Tue, 9 Jul 2024 16:28:50 +1000 Subject: [PATCH] Move selectorwin attribute logic out of ui_tk --- src/app/selector_win.py | 102 ++++++++++++++++++++++++++------------ src/packages/__init__.py | 14 +++--- src/ui_tk/selector_win.py | 63 +++++++++++++---------- 3 files changed, 113 insertions(+), 66 deletions(-) diff --git a/src/app/selector_win.py b/src/app/selector_win.py index 94371d34..b50a66d4 100644 --- a/src/app/selector_win.py +++ b/src/app/selector_win.py @@ -12,7 +12,7 @@ import tkinter as tk from contextlib import aclosing -from collections.abc import Callable, Container, Iterable +from collections.abc import Callable, Container, Iterable, Iterator from collections import defaultdict from enum import Enum, auto as enum_auto import functools @@ -28,8 +28,6 @@ from app.mdown import MarkdownData from app import sound, img, DEV_MODE -from ui_tk.tooltip import set_tooltip -from ui_tk.img import TK_IMG from ui_tk.wid_transtoken import set_menu_text, set_text from ui_tk import TK_ROOT, tk_tools from packages import SelitemData, AttrTypes, AttrDef as AttrDef, AttrMap @@ -275,7 +273,6 @@ class SelectorWinBase[ButtonT, SuggLblT]: modal: bool win: tk.Toplevel # TODO move attrs: list[AttrDef] - attr_labels: dict[str, ttk.Label] # Current list of item IDs we display. item_list: list[utils.SpecialID] @@ -342,6 +339,7 @@ def __init__(self, opt: Options) -> None: self.description = opt.desc self.readonly_description = opt.readonly_desc self.readonly_override = opt.readonly_override + self.attrs = list(opt.attributes) prev_state = config.APP.get_cur_conf( LastSelected, @@ -521,6 +519,30 @@ def sort_func(item_id: utils.SpecialID) -> str: assert menu_pos is not None, "Didn't add to the menu?" group.menu_pos = menu_pos + def _attr_widget_positions(self) -> Iterator[tuple[ + AttrDef, int, + Literal['left', 'right', 'wide'], + ]]: + """Positions all the required attribute widgets. + + Yields (attr, row, col_type) tuples. + """ + self.attrs.sort(key=lambda at: 0 if at.type.is_wide else 1) + index = 0 + for attr in self.attrs: + # Wide ones have their own row, narrow ones are two to a row. + if attr.type.is_wide: + if index % 2: # Row has a single narrow, skip the empty space. + index += 1 + yield attr, index // 2, 'wide' + index += 2 + else: + if index % 2: + yield attr, index // 2, 'right' + else: + yield attr, index // 2, 'left' + index += 1 + async def _rollover_suggest_task(self) -> None: """Handle previewing suggested items when hovering over the 'set suggested' button.""" while True: @@ -796,35 +818,34 @@ def sel_item(self, item_id: utils.SpecialID, _: object = None) -> None: item_attrs = EmptyMapping for attr in self.attrs: val = item_attrs.get(attr.id, attr.default) - attr_label = self.attr_labels[attr.id] - - if attr.type is AttrTypes.BOOL: - TK_IMG.apply(attr_label, ICON_CHECK if val else ICON_CROSS) - elif attr.type is AttrTypes.COLOR: - assert isinstance(val, Vec) - TK_IMG.apply(attr_label, img.Handle.color(val, 16, 16)) - # Display the full color when hovering... - set_tooltip(attr_label, TRANS_ATTR_COLOR.format( - r=int(val.x), g=int(val.y), b=int(val.z), - )) - elif attr.type.is_list: - # Join the values (in alphabetical order) - assert isinstance(val, Iterable) and not isinstance(val, Vec), repr(val) - children = [ - txt if isinstance(txt, TransToken) else TransToken.untranslated(txt) - for txt in val - ] - if attr.type is AttrTypes.LIST_AND: - set_text(attr_label, TransToken.list_and(children, sort=True)) - else: - set_text(attr_label, TransToken.list_or(children, sort=True)) - elif attr.type is AttrTypes.STRING: - # Just a string. - if not isinstance(val, TransToken): - val = TransToken.untranslated(str(val)) - set_text(attr_label, val) - else: - raise ValueError(f'Invalid attribute type: "{attr.type}"') + match attr.type: + case AttrTypes.BOOL: + self._ui_attr_set_image(attr, ICON_CHECK if val else ICON_CROSS) + case AttrTypes.COLOUR: + assert isinstance(val, Vec) + self._ui_attr_set_image(attr, img.Handle.color(val, 16, 16)) + # Display the full color when hovering... + self._ui_attr_set_tooltip(attr, TRANS_ATTR_COLOR.format( + r=int(val.x), g=int(val.y), b=int(val.z), + )) + case AttrTypes.LIST_OR | AttrTypes.LIST_AND: + # Join the values (in alphabetical order) + assert isinstance(val, Iterable) and not isinstance(val, Vec), repr(val) + children = [ + txt if isinstance(txt, TransToken) else TransToken.untranslated(txt) + for txt in val + ] + if attr.type is AttrTypes.LIST_AND: + self._ui_attr_set_text(attr, TransToken.list_and(children, sort=True)) + else: + self._ui_attr_set_text(attr, TransToken.list_or(children, sort=True)) + case AttrTypes.STRING: + # Just a string. + if not isinstance(val, TransToken): + val = TransToken.untranslated(str(val)) + self._ui_attr_set_text(attr, val) + case _: + assert_never(attr.type) def key_navigate(self, key: NavKeys) -> None: """Navigate using arrow keys.""" @@ -1176,6 +1197,21 @@ def _ui_props_set_samp_button_icon(self, glyph: str, /) -> None: """Set the icon in the play-sample button.""" raise NotImplementedError + @abstractmethod + def _ui_attr_set_text(self, attr: AttrDef, text: TransToken, /) -> None: + """Set the value of a text-style attribute widget.""" + raise NotImplementedError + + @abstractmethod + def _ui_attr_set_image(self, attr: AttrDef, image: img.Handle, /) -> None: + """Set the image for an image-style attribute widget.""" + raise NotImplementedError + + @abstractmethod + def _ui_attr_set_tooltip(self, attr: AttrDef, tooltip: TransToken, /) -> None: + """Set the hover tooltip. This only applies to image-style widgets.""" + raise NotImplementedError + @abstractmethod def _ui_menu_set_font(self, item_id: utils.SpecialID, /, suggested: bool) -> None: """Set the font of an item, and its parent group.""" diff --git a/src/packages/__init__.py b/src/packages/__init__.py index 7e8e1f85..2ce33432 100644 --- a/src/packages/__init__.py +++ b/src/packages/__init__.py @@ -119,11 +119,11 @@ @utils.freeze_enum_props class AttrTypes(Enum): """The type of labels used for selectoritem attributes.""" - STR = STRING = 'string' # Normal text + STRING = 'string' # Normal text LIST_AND = 'list_and' # A sequence, joined by commas LIST_OR = 'list_or' # A sequence, joined by commas BOOL = 'bool' # A yes/no checkmark - COLOR = COLOUR = 'color' # A Vec 0-255 RGB colour + COLOUR = 'color' # A Vec 0-255 RGB colour @property def is_wide(self) -> bool: @@ -131,9 +131,9 @@ def is_wide(self) -> bool: return self.value in ('string', 'list_and', 'list_or') @property - def is_list(self) -> bool: - """Determine if this is a list.""" - return self.value.startswith('list_') + def is_image(self) -> bool: + """Check if this uses an image, or is just text.""" + return self.value in ('bool', 'color') # TransToken is str()-ified. @@ -141,7 +141,7 @@ def is_list(self) -> bool: type AttrMap = Mapping[str, AttrValues] -@attrs.define +@attrs.define(eq=False) class AttrDef: """Configuration for attributes shown on selector labels.""" id: str @@ -198,7 +198,7 @@ def color( """Alternative constructor for color-type attrs.""" if default is None: default = Vec(255, 255, 255) - return AttrDef(attr_id, desc, default, AttrTypes.COLOR) + return AttrDef(attr_id, desc, default, AttrTypes.COLOUR) @attrs.frozen(kw_only=True) diff --git a/src/ui_tk/selector_win.py b/src/ui_tk/selector_win.py index d4748d5d..0ced0464 100644 --- a/src/ui_tk/selector_win.py +++ b/src/ui_tk/selector_win.py @@ -68,6 +68,7 @@ class SelectorWin(SelectorWinBase[ prop_desc: RichText prop_scroll: tk_tools.HidingScroll prop_reset: ttk.Button + attr_labels: dict[AttrDef, ttk.Label] # Variable associated with self.display. disp_label: tk.StringVar @@ -302,7 +303,6 @@ def __init__(self, parent: tk.Tk | tk.Toplevel, opt: Options) -> None: ) # Wide before short. - self.attrs = sorted(opt.attributes, key=lambda at: 0 if at.type.is_wide else 1) self.attr_labels = {} if self.attrs: attrs_frame = ttk.Frame(self.prop_frm) @@ -314,17 +314,15 @@ def __init__(self, parent: tk.Tk | tk.Toplevel, opt: Options) -> None: padx=5, ) attrs_frame.columnconfigure(0, weight=1) - attrs_frame.columnconfigure(1, weight=1) + attrs_frame.columnconfigure(2, weight=1) - # Add in all the attribute labels - index = 0 - for attr in self.attrs: + for attr, row, col_type in self._attr_widget_positions(): attr_frame = ttk.Frame(attrs_frame) desc_label = ttk.Label(attr_frame) set_text(desc_label, TRANS_ATTR_DESC.format(desc=attr.desc)) - self.attr_labels[attr.id] = attr_label = ttk.Label(attr_frame) + self.attr_labels[attr] = attr_label = ttk.Label(attr_frame) - if attr.type is AttrTypes.COLOR: + if attr.type is AttrTypes.COLOUR: # A small colour swatch. attr_label.configure(relief='raised') # Show the color value when hovered. @@ -333,30 +331,28 @@ def __init__(self, parent: tk.Tk | tk.Toplevel, opt: Options) -> None: desc_label.grid(row=0, column=0, sticky='e') attr_label.grid(row=0, column=1, sticky='w') # Wide ones have their own row, narrow ones are two to a row - if attr.type.is_wide: - if index % 2: # Row has a single narrow, skip the empty space. - index += 1 - attr_frame.grid( - row=index // 2, - column=0, columnspan=3, - sticky='w', - ) - index += 2 - else: - if index % 2: # Right. - ttk.Separator(orient='vertical').grid(row=index // 2, column=1, sticky='NS') + match col_type: + case 'wide': attr_frame.grid( - row=index // 2, - column=2, - sticky='E', + row=row, + column=0, columnspan=3, + sticky='w', ) - else: + case 'left': attr_frame.grid( - row=index // 2, + row=row, column=0, - sticky='W', + sticky='w', ) - index += 1 + case 'right': + ttk.Separator(attrs_frame, orient='vertical').grid(row=row, column=1, sticky='NS') + attr_frame.grid( + row=row, + column=2, + sticky='e', + ) + case _: + assert_never(col_type) self.set_disp() self.wid_canvas.bind("", self.flow_items) @@ -532,6 +528,21 @@ def _ui_sugg_place(self, label: SuggLabel, button: ttk.Button, x: int, y: int) - label.place(x=x, y=y) label['width'] = button.winfo_width() + @override + def _ui_attr_set_text(self, attr: AttrDef, text: TransToken, /) -> None: + """Set the value of a text-style attribute widget.""" + set_text(self.attr_labels[attr], text) + + @override + def _ui_attr_set_image(self, attr: AttrDef, image: img.Handle, /) -> None: + """Set the image for an image-style attribute widget.""" + TK_IMG.apply(self.attr_labels[attr], image) + + @override + def _ui_attr_set_tooltip(self, attr: AttrDef, tooltip: TransToken, /) -> None: + """Set the hover tooltip. This only applies to image-style widgets.""" + set_tooltip(self.attr_labels[attr], tooltip) + @override def _ui_props_set_author(self, author: TransToken) -> None: """Set the author text for the selected item."""