class StylePane(BaseFeature): name = "Style pane" icon = "edit" _defaults = { **BaseFeature._defaults, "side": "right", } def __init__(self, master, studio, **cnf): super().__init__(master, studio, **cnf) self.body = ScrolledFrame(self, **self.style.dark) self.body.pack(side="top", fill="both", expand=True) self._toggle_btn = Button(self._header, image=get_icon_image("chevron_down", 15, 15), **self.style.dark_button, width=25, height=25) self._toggle_btn.pack(side="right") self._toggle_btn.on_click(self._toggle) self._search_btn = Button(self._header, image=get_icon_image("search", 15, 15), width=25, height=25, **self.style.dark_button) self._search_btn.pack(side="right") self._search_btn.on_click(self.start_search) self.groups = [] self._identity_group = self.add_group(IdentityGroup) self._layout_group = self.add_group(LayoutGroup) self._attribute_group = self.add_group(AttributeGroup) self._empty_frame = Frame(self.body) self.show_empty() self._current = None self._expanded = False def create_menu(self): return (("command", "Search", get_icon_image("search", 14, 14), self.start_search, {}), ("command", "Expand all", get_icon_image("chevron_down", 14, 14), self.expand_all, {}), ("command", "Collapse all", get_icon_image("chevron_up", 14, 14), self.collapse_all, {})) def add_group(self, group_class) -> StyleGroup: if not issubclass(group_class, StyleGroup): raise ValueError('type required.') group = group_class(self.body.body) self.groups.append(group) group.pack(side='top', fill='x', pady=4) return group def show_empty(self): self.remove_empty() self._empty_frame.place(x=0, y=0, relheight=1, relwidth=1) Label(self._empty_frame, text="You have not selected any item", **self.style.dark_text_passive).place(x=0, y=0, relheight=1, relwidth=1) def remove_empty(self): self._empty_frame.clear_children() self._empty_frame.place_forget() def show_loading(self): self.remove_empty() self._empty_frame.place(x=0, y=0, relheight=1, relwidth=1) Label(self._empty_frame, text="Loading...", **self.style.dark_text_passive).place(x=0, y=0, relheight=1, relwidth=1) def styles_for(self, widget): self._current = widget if widget is None: self.show_empty() return self.show_loading() for group in self.groups: group.on_widget_change(widget) self.remove_empty() self.body.update_idletasks() def layout_for(self, widget): self._layout_group.on_widget_change(widget) def on_select(self, widget): self.styles_for(widget) def on_widget_change(self, old_widget, new_widget=None): self.styles_for(new_widget) def on_widget_layout_change(self, widget): self.layout_for(widget) def expand_all(self): for group in self.groups: group.expand() self._expanded = True self._toggle_btn.config(image=get_icon_image("chevron_up", 15, 15)) def clear_all(self): for group in self.groups: group.clear_children() def collapse_all(self): for group in self.groups: group.collapse() self._expanded = False self._toggle_btn.config(image=get_icon_image("chevron_down", 15, 15)) def _toggle(self, *_): if not self._expanded: self.expand_all() else: self.collapse_all() def __update_frames(self): for group in self.groups: group.update_state() def start_search(self, *_): if self._current: super().start_search() self.body.scroll_to_start() def on_search_query(self, query): for group in self.groups: group.on_search_query(query) self.__update_frames() def on_search_clear(self): for group in self.groups: group.on_search_clear() # The search bar is being closed and we need to bring everything back super().on_search_clear()
class MenuEditor(BaseToolWindow): # TODO Add context menu for nodes # TODO Add style search # TODO Handle widget change from the studio main control _MESSAGE_EDITOR_EMPTY = "No item selected" def __init__(self, master, widget, menu=None): super().__init__(master, widget) self.title(f'Edit menu for {widget.id}') if not isinstance(menu, tk.Menu): menu = tk.Menu(widget, tearoff=False) widget.configure(menu=menu) self._base_menu = menu self._tool_bar = Frame(self, **self.style.dark, **self.style.dark_highlight_dim, height=30) self._tool_bar.pack(side="top", fill="x") self._tool_bar.pack_propagate(False) self._pane = PanedWindow(self, **self.style.dark_pane_horizontal) self._tree = MenuTree(self._pane, widget, menu) self._tree.allow_multi_select(True) self._tree.on_select(self._refresh_styles) self._tree.on_structure_change(self._refresh_styles) self._editor_pane = ScrolledFrame(self._pane) self._editor_pane_cover = Label(self._editor_pane, **self.style.dark_text_passive) self._editor_pane.pack(side="top", fill="both", expand=True) self._menu_item_styles = CollapseFrame(self._editor_pane.body) self._menu_item_styles.pack(side="top", fill="x", pady=4) self._menu_item_styles.label = "Menu Item attributes" self._menu_styles = CollapseFrame(self._editor_pane.body) self._menu_styles.pack(side="top", fill="x", pady=4) self._menu_styles.label = "Menu attributes" self._style_item_ref = {} self._menu_style_ref = {} self._prev_selection = None self._add = MenuButton(self._tool_bar, **self.style.dark_button) self._add.pack(side="left") self._add.configure(image=get_icon_image("add", 15, 15)) _types = MenuTree.Node._type_def menu_types = self._tool_bar.make_menu( [( tk.COMMAND, i.title(), get_icon_image(_types[i][0], 14, 14), functools.partial(self.add_item, i), {} ) for i in _types], self._add) menu_types.configure(tearoff=True) self._add.config(menu=menu_types) self._delete_btn = Button(self._tool_bar, image=get_icon_image("delete", 15, 15), **self.style.dark_button, width=25, height=25) self._delete_btn.pack(side="left") self._delete_btn.on_click(self._delete) self._preview_btn = Button(self._tool_bar, image=get_icon_image("play", 15, 15), **self.style.dark_button, width=25, height=25) self._preview_btn.pack(side="left") self._preview_btn.on_click(self._preview) self._pane.pack(side="top", fill="both", expand=True) self._pane.add(self._tree, minsize=350, sticky='nswe', width=350, height=500) self._pane.add(self._editor_pane, minsize=320, sticky='nswe', width=320, height=500) self.load_menu(menu, self._tree) self._show_editor_message(self._MESSAGE_EDITOR_EMPTY) self.enable_centering() self.focus_set() self._load_all_properties() def _show_editor_message(self, message): # Show an overlay message self._editor_pane_cover.config(text=message) self._editor_pane_cover.place(x=0, y=0, relwidth=1, relheight=1) def _clear_editor_message(self): self._editor_pane_cover.place_forget() def _show(self, item): item.pack(fill="x", pady=1) def _hide(self, item): item.pack_forget() def _add_item(self, item): # add a menu item style editor self._style_item_ref[item.name] = item self._show(item) def _add_menu_item(self, item): # add a parent menu style editor self._menu_style_ref[item.name] = item self._show(item) def _load_all_properties(self): # Generate all style editors that may be needed by any of the types of menu items # This needs to be called only once ref = dict(PROPERTY_TABLE) ref.update(MENU_PROPERTY_TABLE) for prop in MENU_PROPERTIES: if not ref.get(prop): continue definition = dict(ref.get(prop)) definition['name'] = prop self._add_item(StyleItem(self._menu_item_styles, definition, self._on_item_change)) menu_prop = get_properties(self._base_menu) for key in menu_prop: definition = menu_prop[key] self._add_menu_item(StyleItem(self._menu_styles, definition, self._on_menu_item_change)) def _on_item_change(self, prop, value): # Called when the style of a menu item changes for node in self._tree.get(): menu_config(node._menu, node.get_index(), **{prop: value}) # For changes in label we need to change the label on the node as well node node.label = node._menu.entrycget(node.get_index(), 'label') def _on_menu_item_change(self, prop, value): nodes = self._tree.get() menus = set([node._menu for node in nodes]) for menu in menus: menu[prop] = value def _refresh_styles(self): # TODO Fix false value change when releasing ctrl key during multi-selecting # called when structure or selection changes nodes = self._tree.get() # get current selection if not nodes: # if no nodes are currently selected display message self._show_editor_message(self._MESSAGE_EDITOR_EMPTY) return self._clear_editor_message() # remove any messages # get intersection of styles for currently selected nodes # these will be the styles common to all the nodes selected, use sets for easy analysis styles = set(nodes[0].get_options().keys()) for node in nodes: styles &= set(node.get_options().keys()) # populate editors with values of the last item # TODO this is not the best approach, no value should be set for an option if it is not the same for all nodes node = nodes[-1] for style_item in self._style_item_ref.values(): # styles for menu items if style_item.name in styles: self._show(style_item) style_item.set(node.get_option(style_item.name)) else: self._hide(style_item) for style_item in self._menu_style_ref.values(): # styles for the menu style_item.set(node._menu.cget(style_item.name)) def _preview(self, *_): self.widget.event_generate("<Button-1>") def _delete(self, *_): # create a copy since the list may change during iteration selected = list(self._tree.get()) for node in selected: self._tree.deselect(node) node.remove() self._refresh_styles() def add_item(self, _type): label = f"{_type.title()}" selected = self._tree.get() if len(selected) == 1 and selected[0].type == tk.CASCADE: node = selected[0] else: node = self._tree if _type != tk.SEPARATOR: node._sub_menu.add(_type, label=label) else: node._sub_menu.add(_type) node.add_menu_item(type=_type, label=label, index=tk.END) def load_menu(self, menu, node): # if the widget has a menu we need to populate the tree when the editor is created # we do this recursively to be able to capture even cascades # we cannot directly access all items in a menu or its size # but we can get the index of the last item and use that to get size and hence viable indexes size = menu.index(tk.END) if size is None: # menu is empty return for i in range(size + 1): if menu.type(i) == tk.CASCADE: label = menu.entrycget(i, "label") # get the cascades sub-menu and load it recursively sub = self.nametowidget(menu.entrycget(i, "menu")) item_node = node.add_menu_item(type=menu.type(i), label=label, index=i, sub_menu=sub) self.load_menu(item_node._sub_menu, item_node) elif menu.type(i) == tk.SEPARATOR: # Does not need a label, set it to the default 'separator' node.add_menu_item(type=menu.type(i), index=i, label='separator') elif menu.type(i) != 'tearoff': # skip any tear_off item since they cannot be directly manipulated label = menu.entrycget(i, "label") node.add_menu_item(type=menu.type(i), label=label, index=i)
class StylePaneFramework: def setup_style_pane(self): self.body = ScrolledFrame(self, **self.style.surface) self.body.pack(side="top", fill="both", expand=True) self._toggle_btn = Button(self.get_header(), image=get_icon_image("chevron_down", 15, 15), **self.style.button, width=25, height=25) self._toggle_btn.pack(side="right") self._toggle_btn.on_click(self._toggle) self._search_btn = Button(self.get_header(), image=get_icon_image("search", 15, 15), width=25, height=25, **self.style.button) self._search_btn.pack(side="right") self._search_btn.on_click(self.start_search) self.groups = [] self._empty_frame = Frame(self.body) self.show_empty() self._current = None self._expanded = False self._is_loading = False self._search_query = None def get_header(self): raise NotImplementedError() @property def supported_groups(self): return [ group for group in self.groups if group.supports_widget(self._current) ] def create_menu(self): return (("command", "Search", get_icon_image("search", 14, 14), self.start_search, {}), ("command", "Expand all", get_icon_image("chevron_down", 14, 14), self.expand_all, {}), ("command", "Collapse all", get_icon_image("chevron_up", 14, 14), self.collapse_all, {})) def extern_apply(self, group_class, prop, value, widget=None, silent=False): for group in self.groups: if group.__class__ == group_class: group.apply(prop, value, widget, silent) return raise ValueError(f"Class {group_class.__class__.__name__} not found") def last_action(self): raise NotImplementedError() def new_action(self, action): raise NotImplementedError() def widget_modified(self, widget): raise NotImplementedError() def add_group(self, group_class, **kwargs) -> StyleGroup: if not issubclass(group_class, StyleGroup): raise ValueError('type required.') group = group_class(self.body.body, self, **kwargs) self.groups.append(group) self.show_group(group) return group def add_group_instance(self, group_instance, show=False): if not isinstance(group_instance, StyleGroup): raise ValueError('Expected object of type StyleGroup.') self.groups.append(group_instance) if show: self.show_group(group_instance) def hide_group(self, group): if group.self_positioned: group._hide_group() return group.pack_forget() def show_group(self, group): if group.self_positioned: group._show_group() return group.pack(side='top', fill='x', pady=12) def show_empty(self): self.remove_empty() self._empty_frame.place(x=0, y=0, relheight=1, relwidth=1) Label(self._empty_frame, text="You have not selected any item", **self.style.text_passive).place(x=0, y=0, relheight=1, relwidth=1) def remove_empty(self): self._empty_frame.clear_children() self._empty_frame.place_forget() def show_loading(self): if platform_is(LINUX) or self._is_loading: # render transitions in linux are very fast and glitch free # for other platforms or at least for windows we need to hide the glitching return self.remove_empty() self._empty_frame.place(x=0, y=0, relheight=1, relwidth=1) Label(self._empty_frame, text="Loading...", **self.style.text_passive).place(x=0, y=0, relheight=1, relwidth=1) self._is_loading = True def remove_loading(self): self.remove_empty() self._is_loading = False def styles_for(self, widget): self._current = widget if widget is None: self.show_empty() return for group in self.groups: if group.supports_widget(widget): self.show_group(group) group.on_widget_change(widget) else: self.hide_group(group) self.remove_loading() self.body.update_idletasks() def layout_for(self, widget): for group in self.groups: if group.handles_layout: group.on_widget_change(widget) self.remove_loading() def on_select(self, widget): self.styles_for(widget) def on_widget_change(self, old_widget, new_widget=None): if new_widget is None: new_widget = old_widget self.styles_for(new_widget) def on_widget_layout_change(self, widget): self.layout_for(widget) def expand_all(self): for group in self.groups: group.expand() self._expanded = True self._toggle_btn.config(image=get_icon_image("chevron_up", 15, 15)) def clear_all(self): for group in self.groups: group.clear_children() def collapse_all(self): for group in self.groups: group.collapse() self._expanded = False self._toggle_btn.config(image=get_icon_image("chevron_down", 15, 15)) def _toggle(self, *_): if not self._expanded: self.expand_all() else: self.collapse_all() def __update_frames(self): for group in self.groups: group.update_state() def start_search(self, *_): if self._current: super().start_search() self.body.scroll_to_start() def on_search_query(self, query): for group in self.groups: group.on_search_query(query) self.__update_frames() self.body.scroll_to_start() self._search_query = query def on_search_clear(self): for group in self.groups: group.on_search_clear() # The search bar is being closed and we need to bring everything back super().on_search_clear() self._search_query = None