Esempio n. 1
0
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()
Esempio n. 2
0
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)
Esempio n. 3
0
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