Ejemplo n.º 1
0
class Image(Text):
    def __init__(self, master, style_def=None):
        super().__init__(master, style_def)
        self._picker = Button(self,
                              **self.style.dark_button,
                              width=25,
                              height=25,
                              text="...")
        self._entry.pack_forget()
        self._picker.pack(side="right")
        self._entry.pack(side="left", fill="x")
        self._picker.on_click(self._pick)

    def _change(self, *_):
        # Do not broadcast changes for invalid paths
        # TODO Add indicator for invalid paths
        if not os.path.exists(self.get()):
            return
        super()._change()

    def _pick(self, *_):
        path = filedialog.askopenfilename(parent=self)
        if path:
            self._entry.set(path)
            if self._on_change:
                self._on_change(path)
Ejemplo n.º 2
0
class CollapseFrame(Frame):
    def __init__(self, master, **cnf):
        super().__init__(master, **cnf)
        self.config(**self.style.dark)
        self._label_frame = Frame(self, **self.style.bright, height=20)
        self._label_frame.pack(side="top", fill="x", padx=2)
        self._label_frame.pack_propagate(0)
        self._label = Label(self._label_frame, **self.style.bright,
                            **self.style.text_bright)
        self._label.pack(side="left")
        self._collapse_btn = Button(self._label_frame,
                                    width=20,
                                    **self.style.bright,
                                    **self.style.text_bright)
        self._collapse_btn.config(text=get_icon("triangle_up"))
        self._collapse_btn.pack(side="right", fill="y")
        self._collapse_btn.on_click(self.toggle)
        self.body = Frame(self, **self.style.dark)
        self.body.pack(side="top", fill="both", pady=2)
        self.__ref = Frame(self.body, height=0, width=0, **self.style.dark)
        self.__ref.pack(side="top")
        self._collapsed = False

    def update_state(self):
        self.__ref.pack(side="top")

    def collapse(self, *_):
        if not self._collapsed:
            self.body.pack_forget()
            self._collapse_btn.config(text=get_icon("triangle_down"))
            self.pack_propagate(0)
            self.config(height=20)
            self._collapsed = True

    def clear_children(self):
        self.body.clear_children()

    def expand(self, *_):
        if self._collapsed:
            self.body.pack(side="top", fill="both")
            self.pack_propagate(1)
            self._collapse_btn.config(text=get_icon("triangle_up"))
            self._collapsed = False

    def toggle(self, *_):
        if self._collapsed:
            self.expand()
        else:
            self.collapse()

    @property
    def label(self):
        return self._label["text"]

    @label.setter
    def label(self, value):
        self._label.config(text=value)
Ejemplo n.º 3
0
 def init_toolbar(self):
     for action in self.actions:
         if len(action) == 1:
             Frame(self._toolbar, width=1, bg=self.style.colors.get("primarydarkaccent")).pack(
                 side='left', fill='y', pady=3, padx=5)
             continue
         btn = Button(self._toolbar, image=action[1], **self.style.button, width=25, height=25)
         btn.pack(side="left", padx=3)
         btn.tooltip(action[3])
         ActionNotifier.bind_event("<Button-1>", btn, action[2], text=action[3])
Ejemplo n.º 4
0
 def _add_button(self, **kw):
     text = kw.get("text")
     focus = kw.get("focus", False)
     # If a button bar does not already exist we need to create one
     if self.bar is None:
         self._make_button_bar()
     btn = Button(self.bar, **self.style.button, text=text, height=25)
     btn.configure(**self.style.highlight_active)
     btn.pack(side="right", padx=5, pady=5)
     # ensure the buttons have a minimum width of _MIN_BUTTON_WIDTH
     btn.configure(width=max(self._MIN_BUTTON_WIDTH, btn.measure_text(text)))
     btn.on_click(kw.get("command", lambda _: self._terminate_with_val(kw.get("value"))))
     if focus:
         btn.focus_set()
         btn.config_all(**self.style.button_highlight)
     return btn
Ejemplo n.º 5
0
class Image(Text):

    def __init__(self, master, style_def=None):
        super().__init__(master, style_def)
        self._picker = Button(self, **self.style.button, width=25, height=25, text="...")
        self._entry.pack_forget()
        self._picker.pack(side="right")
        self._entry.pack(side="left", fill="x")
        self._picker.on_click(self._pick)

    def _change(self, *_):
        # Do not broadcast changes for invalid paths
        # TODO Add indicator for invalid paths
        if not os.path.exists(self.get()):
            return
        super()._change()

    def _pick(self, *_):
        path = filedialog.askopenfilename(parent=self)
        if path:
            try:
                path_opt = get_active_pref(self).get("designer::image_path")
            except:
                path_opt = "absolute"

            if path_opt == "absolute":
                # use path as is (absolute)
                pass
            else:
                path = pathlib.Path(path)
                current = pathlib.Path(os.getcwd())
                if (current in path.parents and path_opt == "mixed") or path_opt == "relative":
                    # use relative path
                    try:
                        # use relative path if possible
                        path = os.path.relpath(path, current)
                    except ValueError:
                        pass
                path = str(path)
            self._entry.set(path)
            if self._on_change:
                self._on_change(path)
Ejemplo n.º 6
0
class SearchBar(Frame):
    def __init__(self, master=None, **cnf):
        super().__init__(master, **cnf)
        self.config(**self.style.no_highlight, **self.style.dark)
        self._entry = Entry(self, **self.style.dark_input)
        self._clear_btn = Button(self,
                                 image=get_icon_image("close", 15, 15),
                                 **self.style.dark_button,
                                 width=25,
                                 height=25)
        self._clear_btn.pack(side="right", fill="y")
        self._clear_btn.on_click(self._clear)
        Label(self,
              **self.style.dark_text,
              image=get_icon_image("search", 15, 15)).pack(side="left")
        self._entry.pack(side="left", fill="both", expand=True, padx=2)
        self._entry.on_entry(self._change)
        self._on_change = None
        self._on_clear = None

    def focus_set(self):
        super().focus_set()
        self._entry.focus_set()

    def on_query_change(self, func, *args, **kwargs):
        self._on_change = lambda val: func(val, *args, **kwargs)

    def on_query_clear(self, func, *args, **kwargs):
        self._on_clear = lambda: func(*args, **kwargs)

    def _clear(self, *_):
        if self._on_clear:
            self._on_clear()

    def _change(self, *_):
        if self._on_change:
            self._on_change(self._entry.get())
Ejemplo n.º 7
0
class ComponentTree(BaseFeature):
    name = "Component Tree"
    icon = "treeview"

    def __init__(self, master, studio=None, **cnf):
        super().__init__(master, studio, **cnf)
        self._toggle_btn = Button(self._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._header,
            **self.style.button,
            image=get_icon_image("search", 15, 15),
            width=25,
            height=25,
        )
        self._search_btn.pack(side="right")
        self._search_btn.on_click(self.start_search)
        self.body = Frame(self, **self.style.surface)
        self.body.pack(side="top", fill="both", expand=True)
        self._empty_label = Label(self.body, **self.style.text_passive)

        self._selected = None
        self._expanded = False
        self._tree = None

    def on_context_switch(self):
        if self._tree:
            self._tree.pack_forget()

        if self.studio.designer:
            self.show_empty(None)
            if self.studio.designer.node:
                self._tree = self.studio.designer.node
            else:
                self._tree = ComponentTreeView(self.body)
                self._tree.on_select(self._trigger_select)
                self.studio.designer.node = self._tree
            self._tree.pack(fill="both", expand=True)
        else:
            self.show_empty("No active Designer")

    def create_menu(self):
        return (("command", "Expand all",
                 get_icon_image("chevron_down", 14, 14), self._expand, {}),
                ("command", "Collapse all",
                 get_icon_image("chevron_up", 14, 14), self._collapse, {}))

    def show_empty(self, text):
        if text:
            self._empty_label.lift()
            self._empty_label.place(x=0, y=0, relwidth=1, relheight=1)
            self._empty_label['text'] = text
        else:
            self._empty_label.place_forget()

    def _expand(self):
        self._tree.expand_all()
        self._toggle_btn.config(image=get_icon_image("chevron_up", 15, 15))
        self._expanded = True

    def _collapse(self):
        self._tree.collapse_all()
        self._toggle_btn.config(image=get_icon_image("chevron_down", 15, 15))
        self._expanded = False

    def _toggle(self, *_):
        if self._expanded:
            self._collapse()
        else:
            self._expand()

    def on_widget_add(self, widget: PseudoWidget, parent=None):
        if parent is None:
            node = self._tree.add_as_node(widget=widget)
        else:
            parent = parent.node
            node = parent.add_as_node(widget=widget)

        # let the designer render the menu for us
        MenuUtils.bind_all_context(
            node, lambda e: self.studio.designer.show_menu(e, widget)
            if self.studio.designer else None)

    def _trigger_select(self):
        if self._selected and self._selected.widget == self._tree.get().widget:
            return
        self.studio.select(self._tree.get().widget, self)
        self._selected = self._tree.get()

    def select(self, widget):
        if widget:
            node = widget.node
            self._selected = node
            node.select(
                None, True
            )  # Select node silently to avoid triggering a duplicate selection event
        elif widget is None:
            if self._selected:
                self._selected.deselect()
                self._selected = None

    def on_select(self, widget):
        self.select(widget)

    def on_widget_delete(self, widget, silently=False):
        widget.node.remove()

    def on_widget_restore(self, widget):
        widget.layout.node.add(widget.node)

    def on_widget_layout_change(self, widget):
        node = widget.node
        if widget.layout == self.studio.designer:
            parent = self._tree
        else:
            parent = widget.layout.node
        if node.parent_node != parent:
            parent.insert(None, node)

    def on_context_close(self, context):
        if hasattr(context, "designer"):
            # delete context's tree
            if hasattr(context.designer, "node") and context.designer.node:
                context.designer.node.destroy()

    def on_session_clear(self):
        self._tree.clear()

    def on_widget_change(self, old_widget, new_widget=None):
        new_widget = new_widget if new_widget else old_widget
        new_widget.node.widget_modified(new_widget)

    def on_search_query(self, query: str):
        self._tree.search(query)

    def on_search_clear(self):
        self._tree.search("")
        super(ComponentTree, self).on_search_clear()
Ejemplo n.º 8
0
class VariablePane(BaseFeature):
    name = "Variablepane"
    icon = "text"

    _defaults = {**BaseFeature._defaults, "side": "right"}

    _definitions = {
        "name": {
            "name": "name",
            "type": "text",
        }
    }

    _empty_message = "No variables added"

    def __init__(self, master, studio=None, **cnf):
        super().__init__(master, studio, **cnf)
        f = Frame(self, **self.style.surface)
        f.pack(side="top", fill="both", expand=True, pady=4)
        f.pack_propagate(0)

        self._variable_pane = ScrolledFrame(f, width=150)
        self._variable_pane.place(x=0, y=0, relwidth=0.4, relheight=1)

        self._detail_pane = ScrolledFrame(f, width=150)
        self._detail_pane.place(relx=0.4,
                                y=0,
                                relwidth=0.6,
                                relheight=1,
                                x=15,
                                width=-20)

        Label(self._detail_pane.body,
              **self.style.text_passive,
              text="Type",
              anchor="w").pack(side="top", fill="x")
        self.var_type_lbl = Label(self._detail_pane.body,
                                  **self.style.text,
                                  anchor="w")
        self.var_type_lbl.pack(side="top", fill="x")
        Label(self._detail_pane.body,
              **self.style.text_passive,
              text="Name",
              anchor="w").pack(side="top", fill="x")
        self.var_name = editors.get_editor(self._detail_pane.body,
                                           self._definitions["name"])
        self.var_name.pack(side="top", fill="x")
        Label(self._detail_pane.body,
              **self.style.text_passive,
              text="Value",
              anchor="w").pack(fill="x", side="top")
        self._editors = {}
        self._editor = None

        self._search_btn = Button(self._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._search_query = None

        self._add = MenuButton(self._header, **self.style.button)
        self._add.configure(image=get_icon_image("add", 15, 15))
        self._add.pack(side="right")
        self._delete_btn = Button(self._header,
                                  image=get_icon_image("delete", 15, 15),
                                  width=25,
                                  height=25,
                                  **self.style.button)
        self._delete_btn.pack(side="right")
        self._delete_btn.on_click(self._delete)
        self._var_types_menu = self.make_menu(self._get_add_menu(),
                                              self._add,
                                              title="Add variable")
        self._var_types_menu.configure(tearoff=True)
        self._add.config(menu=self._var_types_menu)
        self._selected = None
        self._links = {}
        self._overlay = Label(f,
                              **self.style.text_passive,
                              text=self._empty_message,
                              compound="top")
        self._overlay.configure(image=get_icon_image("add", 25, 25))
        self._show_overlay(True)

    def start_search(self, *_):
        if self.variables:
            super().start_search()
            self._variable_pane.scroll_to_start()

    def on_search_query(self, query):
        matches = []
        self._variable_pane.clear_children()
        for item in self.variables:
            if query in item.name:
                self._show(item)
                matches.append(item)

        if not matches:
            self._show_overlay(True,
                               text="No matches found",
                               image=get_icon_image("search", 25, 25))
        else:
            self.select(matches[0])
            self._show_overlay(False)
        self._search_query = query

    def on_search_clear(self):
        self.on_search_query("")
        self._search_query = None
        # remove overlay if we have variables otherwise show it
        self._show_overlay(not self.variables)
        super().on_search_clear()

    def _get_add_menu(self):
        _types = VariableItem._types
        return [(tk.COMMAND, _types[i].get("name"),
                 get_icon_image(_types[i].get("icon"), 14,
                                14), functools.partial(self.menu_add_var,
                                                       i), {}) for i in _types]

    def create_menu(self):
        return (
            ("cascade", "Add", get_icon_image("add", 14, 14), None, {
                "menu": self._get_add_menu()
            }),
            ("command", "Delete", get_icon_image("delete", 14,
                                                 14), self._delete, {}),
            ("command", "Search", get_icon_image("search", 14,
                                                 14), self.start_search, {}),
        )

    def _show_overlay(self, flag=True, **kwargs):
        if flag:
            kwargs["text"] = kwargs.get("text", self._empty_message)
            kwargs["image"] = kwargs.get("image",
                                         get_icon_image("add", 25, 25))
            self._overlay.lift()
            self._overlay.configure(**kwargs)
            self._overlay.place(x=0, y=0, relwidth=1, relheight=1)
        else:
            self._overlay.place_forget()

    def menu_add_var(self, var_type, **kw):
        item = self.add_var(var_type, **kw)
        self.select(item)

    def add_var(self, var_type, **kw):
        var = var_type(self.studio)
        item_count = len(
            list(filter(lambda x: x.var_type == var_type, self.variables))) + 1
        name = kw.get('name', f"{var_type.__name__}_{item_count}")
        value = kw.get('value')
        item = VariableItem(self._variable_pane.body, var, name)
        item.bind("<Button-1>", lambda e: self.select(item))
        if value is not None:
            item.set(value)

        self._show(item)
        self._show_overlay(False)
        if self._search_query is not None:
            # reapply search if any
            self.on_search_query(self._search_query)
        elif not self.variables:
            self.select(item)
        VariableManager.add(item)
        return item

    def delete_var(self, var):
        self._hide(var)
        VariableManager.remove(var)

    def _delete(self, *_):
        if self._selected:
            self.delete_var(self._selected)
        if self.variables:
            self.select(self.variables[0])
        else:
            self._show_overlay(True)

    def clear_variables(self):
        # the list is likely to change during iteration, create local copy
        variables = list(self.variables)
        for var in variables:
            self.delete_var(var)
        self._show_overlay(True)

    @property
    def variables(self):
        return VariableManager.variables()

    def select(self, item):
        if item == self._selected:
            return
        item.select()
        if self._selected:
            self._selected.deselect()
        self._selected = item
        self._detail_for(item)

    def _show(self, item):
        item.pack(side="top", fill="x")

    def _hide(self, item):
        item.pack_forget()

    def _get_editor(self, variable):
        editor_type = variable.definition["type"]
        if not self._editors.get(editor_type):
            # we do not have that type of editor yet, create it
            self._editors[editor_type] = editors.get_editor(
                self._detail_pane.body, variable.definition)
        return self._editors[editor_type]

    def refresh(self):
        # redraw variables for current context
        self._variable_pane.body.clear_children()
        has_selection = False
        if not self.variables:
            self._show_overlay(True)
        else:
            self._show_overlay(False)
        for item in self.variables:
            self._show(item)
            if not has_selection:
                self.select(item)
                has_selection = True
        # reapply search query if any
        if self._search_query is not None:
            self.on_search_query(self._search_query)

    def _detail_for(self, variable):
        _editor = self._get_editor(variable)
        if self._editor != _editor:
            # we need to change current editor completely
            if self._editor:
                self._editor.pack_forget()
            self._editor = _editor
        self._editor.set(variable.value)
        self._editor.pack(side="top", fill="x")
        self._editor.on_change(variable.set)

        self.var_name.set(variable.name)
        self.var_name.on_change(variable.set_name)
        self.var_type_lbl["text"] = variable.var_type_name

    def on_session_clear(self):
        self.clear_variables()

    def on_context_switch(self):
        VariableManager.set_context(self.studio.context)
        self.refresh()
Ejemplo n.º 9
0
class VariablePane(BaseFeature):
    name = "Variablepane"
    icon = "text"

    _defaults = {**BaseFeature._defaults, "side": "right"}

    _definitions = {
        "name": {
            "name": "name",
            "type": "text",
        }
    }

    def __init__(self, master, studio=None, **cnf):
        super().__init__(master, studio, **cnf)
        f = Frame(self, **self.style.dark)
        f.pack(side="top", fill="both", expand=True, pady=4)
        f.pack_propagate(0)

        self._variable_pane = ScrolledFrame(f, width=150)
        self._variable_pane.place(x=0, y=0, relwidth=0.4, relheight=1)

        self._detail_pane = ScrolledFrame(f, width=150)
        self._detail_pane.place(relx=0.4,
                                y=0,
                                relwidth=0.6,
                                relheight=1,
                                x=15,
                                width=-20)

        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._add = MenuButton(self._header, **self.style.dark_button)
        self._add.configure(image=get_icon_image("add", 15, 15))
        self._add.pack(side="right")
        self._delete_btn = Button(self._header,
                                  image=get_icon_image("delete", 15, 15),
                                  width=25,
                                  height=25,
                                  **self.style.dark_button)
        self._delete_btn.pack(side="right")
        self._delete_btn.on_click(self._delete)
        self._var_types_menu = self.make_menu(self._get_add_menu(), self._add)
        self._add.config(menu=self._var_types_menu)
        self._selected = None
        self._links = {}
        self._overlay = Label(f,
                              **self.style.dark_text_passive,
                              text="Add variables",
                              compound="top")
        self._overlay.configure(image=get_icon_image("add", 25, 25))
        self._show_overlay(True)
        self._editors = []

    def start_search(self, *_):
        if len(self.variables):
            super().start_search()
            self._variable_pane.scroll_to_start()

    def on_search_query(self, query):
        matches = []
        self._variable_pane.clear_children()
        for item in self.variables:
            if query in item.name:
                self._show(item)
                matches.append(item)

        if not len(matches):
            self._show_overlay(True,
                               text="No matches found",
                               image=get_icon_image("search", 25, 25))
        else:
            self.select(matches[0])
            self._show_overlay(False)

    def on_search_clear(self):
        self.on_search_query("")
        self._show_overlay(False)
        super().on_search_clear()

    def _get_add_menu(self):
        _types = VariableItem._types
        return [(tk.COMMAND, _types[i].get("name"),
                 get_icon_image(_types[i].get("icon"), 14,
                                14), functools.partial(self.add_var, i), {})
                for i in _types]

    def create_menu(self):
        return (
            ("cascade", "Add", get_icon_image("add", 14, 14), None, {
                "menu": self._get_add_menu()
            }),
            ("command", "Delete", get_icon_image("delete", 14,
                                                 14), self._delete, {}),
            ("command", "Search", get_icon_image("search", 14,
                                                 14), self.start_search, {}),
        )

    def _show_overlay(self, flag=True, **kwargs):
        if flag:
            self._overlay.lift()
            self._overlay.configure(**kwargs)
            self._overlay.place(x=0, y=0, relwidth=1, relheight=1)
        else:
            self._overlay.place_forget()

    def add_var(self, var_type, **kw):
        var = var_type(self.studio)
        item_count = len(
            list(filter(lambda x: x.var_type == var_type, self.variables))) + 1
        name = kw.get('name', f"{var_type.__name__}_{item_count}")
        value = kw.get('value')
        item = VariableItem(self._variable_pane.body, var, name)
        item.bind("<Button-1>", lambda e: self.select(item))
        if value is not None:
            item.set(value)
        VariableManager.add(item)
        self._show(item)
        self._show_overlay(False)
        self.select(item)

    def delete_var(self, var):
        self._hide(var)
        VariableManager.remove(var)

    def _delete(self, *_):
        if self._selected:
            self.delete_var(self._selected)
        if len(self.variables):
            self.select(self.variables[0])
        else:
            self._show_overlay(True)

    def clear_variables(self):
        # the list is likely to change during iteration, create local copy
        variables = list(self.variables)
        for var in variables:
            self.delete_var(var)
        self._show_overlay(True)

    @property
    def variables(self):
        return VariableManager.variables

    def select(self, item):
        if item == self._selected:
            return
        item.select()
        if self._selected:
            self._selected.deselect()
        self._selected = item
        self._detail_for(item)

    def _show(self, item):
        item.pack(side="top", fill="x")

    def _hide(self, item):
        item.pack_forget()

    def _detail_for(self, variable):
        self._detail_pane.clear_children()
        Label(self._detail_pane.body,
              **self.style.dark_text_passive,
              text="Type",
              anchor="w").pack(fill="x", side="top")
        Label(self._detail_pane.body,
              **self.style.dark_text,
              text=variable.var_type_name,
              anchor="w").pack(fill="x", side="top")
        Label(self._detail_pane.body,
              **self.style.dark_text_passive,
              text="Name",
              anchor="w").pack(fill="x", side="top")
        name = editors.get_editor(self._detail_pane.body,
                                  self._definitions["name"])
        name.pack(side="top", fill="x")
        name.set(variable.name)
        name.on_change(variable.set_name)
        Label(self._detail_pane.body,
              **self.style.dark_text_passive,
              text="Value",
              anchor="w").pack(fill="x", side="top")
        value = editors.get_editor(self._detail_pane.body, variable.definition)
        value.set(variable.value)
        value.pack(side="top", fill="x")
        value.on_change(variable.set)

    def on_session_clear(self):
        self.clear_variables()
Ejemplo n.º 10
0
class BaseFeature(Pane):
    _instance = None
    name = "Feature"
    pane = None
    bar = None
    icon = "blank"
    _view_mode = None
    _transparency_flag = None
    _side = None
    rec = (20, 20, 300, 300)  # Default window mode position
    _defaults = {
        "mode": "docked",
        "inactive_transparency": False,
        "position": "left",
        "visible": True,
        "side": "left",
        "pane": {
            "height": 300,
            "index":
            1000,  # setting 1000 allows the feature pane to pick an index
        },
        "pos": {
            "initialized": False,
            "x": 20,
            "y": 20,
            "width": 200,
            "height": 200,
        }
    }

    @classmethod
    def update_defaults(cls):
        pref = Preferences.acquire()
        path = "features::{}".format(cls.name)
        if not pref.exists(path):
            pref.set(path, copy.deepcopy(cls._defaults))
        else:
            pref.update_defaults(path, copy.deepcopy(cls._defaults))

    def __init__(self, master, studio=None, **cnf):
        super().__init__(master, **cnf)
        self.update_defaults()
        self.__class__._instance = self
        if not self.__class__._view_mode:
            self.__class__._view_mode = StringVar(None, self.get_pref('mode'))
            self.__class__._transparency_flag = t = BooleanVar(
                None, self.get_pref('inactive_transparency'))
            self.__class__._side = StringVar(None, self.get_pref('side'))
            self.is_visible = BooleanVar(None, self.get_pref('visible'))
            t.trace_add(
                "write",
                lambda *_: self.set_pref('inactive_transparency', t.get()))
        self.studio = studio
        Label(self._header, **self.style.text_accent,
              text=self.name).pack(side="left")
        self._min = Button(self._header,
                           image=get_icon_image("close", 15, 15),
                           **self.style.button,
                           width=25,
                           height=25)
        self._min.pack(side="right")
        self._min.on_click(self.minimize)
        self._pref = MenuButton(self._header, **self.style.button)
        self._pref.configure(image=get_icon_image("settings", 15, 15))
        self._pref.pack(side="right")
        self._pref.tooltip("Options")
        menu = self.make_menu(
            (("cascade", "View Mode", None, None, {
                "menu": (
                    ("radiobutton", "Docked", None, self.open_as_docked, {
                        "variable": self._view_mode,
                        "value": "docked"
                    }),
                    ("radiobutton", "Window", None, self.open_as_window, {
                        "variable": self._view_mode,
                        "value": "window"
                    }),
                )
            }), ("cascade", "Position", None, None, {
                "menu": (
                    ("radiobutton", "Left", None,
                     lambda: self.reposition("left"), {
                         "variable": self._side,
                         "value": "left"
                     }),
                    ("radiobutton", "Right", None,
                     lambda: self.reposition("right"), {
                         "variable": self._side,
                         "value": "right"
                     }),
                )
            }),
             EnableIf(lambda: self._view_mode.get() == 'window',
                      ("cascade", "Window options", None, None, {
                          "menu": (("checkbutton", "Transparent when inactive",
                                    None, None, {
                                        "variable": self._transparency_flag
                                    }), )
                      })), ("command", "Close", get_icon_image(
                          "close", 14, 14), self.minimize, {}),
             ("separator", ), *self.create_menu()), self._pref)
        self._pref.config(menu=menu)
        # self._pref.on_click(self.minimize)
        self.config(**self.style.surface)
        self.indicator = None
        self.window_handle = None
        self.on_focus(self._on_focus_get)
        self.on_focus_lost(self._on_focus_release)
        self.on_close(self.close_window)
        self._mode_map = {
            'window': self.open_as_window,
            'docked': self.open_as_docked
        }

    @classmethod
    def get_pref_path(cls, short_path):
        return "features::{}::{}".format(cls.name, short_path)

    @classmethod
    def get_pref(cls, short_path):
        return Preferences.acquire().get(cls.get_pref_path(short_path))

    @classmethod
    def set_pref(cls, short_path, value):
        Preferences.acquire().set(cls.get_pref_path(short_path), value)

    @classmethod
    def get_instance(cls):
        return cls._instance

    def on_select(self, widget):
        """
        Called when a widget is selected in the designer
        :param widget: selected widget
        :return:None
        """
        pass

    def on_widget_change(self, old_widget, new_widget=None):
        """
        Called when a widget is fundamentally altered
        :param old_widget: Altered widget
        :param new_widget: The new widget taking the older widgets place
        :return: None
        """
        pass

    def on_widget_layout_change(self, widget):
        """
        Called when layout options of a widget are changed
        :param widget: Widget with altered layout options
        :return: None
        """
        pass

    def on_widget_add(self, widget, parent):
        """
        Called when a new widget is added to the designer
        :param widget: widget
        :param parent: the container widget to which thw widget is added
        :return: None
        """
        pass

    def on_widget_delete(self, widget, silently=False):
        """
        Called when a widget is deleted from the designer
        :param widget: deleted widget
        :param silently: flag indicating whether the deletion should be treated implicitly
        which is useful for instance when you don't want the deletion to be logged in the
        undo stack
        :return: None
        """
        pass

    def on_widget_restore(self, widget):
        """
        Called when a deleted widget is restored
        :param widget: restored widget
        :return: None
        """
        pass

    def on_session_clear(self):
        """
        Override to perform operations before a session is cleared and the studio
        resets to a new design
        :return: None
        """
        pass

    def on_context_switch(self):
        """
        Override to perform operations when the active tab changes
        """
        pass

    def on_context_close(self, context):
        """
        Override to perform operations when a tab context is closed
        """
        pass

    def on_app_close(self) -> bool:
        """
        Override to perform operations before the studio app closes.
        :return: True to allow shutdown to proceed or False to abort shutdown
        """
        return True

    def minimize(self, *_):
        if self.window_handle:
            self.close_window()
            return
        self.studio.minimize(self)
        self.set_pref("visible", False)
        self.is_visible.set(False)

    def maximize(self):
        if self.get_pref("mode") == "window":
            self.open_as_window()
            self.bar.select(self)
        else:
            self.studio.maximize(self)
        self.set_pref("visible", True)
        self.is_visible.set(True)

    def toggle(self):
        if self.get_pref("visible"):
            self.minimize()
        else:
            self.maximize()

    def create_menu(self):
        """
        Override this method to provide additional menu options
        :return: tuple of menu templates i.e. (type, label, image, callback, **additional_config)
        """
        # return an empty tuple as default
        return ()

    def _on_focus_release(self):
        if self._transparency_flag.get() and self.window_handle:
            if self.window_handle:
                self.window_handle.wm_attributes('-alpha', 0.3)
        if self.window_handle:
            self.save_window_pos()

    def _on_focus_get(self):
        if self.window_handle:
            self.window_handle.wm_attributes('-alpha', 1.0)

    def open_as_docked(self):
        self._view_mode.set("docked")
        self.set_pref('mode', 'docked')
        if self.window_handle:
            self.master.window.wm_forget(self)
            self.window_handle = None
            self.maximize()

    def reposition(self, side):
        self._side.set(side)
        self.studio.reposition(self, side)

    def open_as_window(self):
        if TkVersion < 8.5:
            logging.error("Window mode is not supported in current tk version")
            return
        self.master.window.wm_forget(self)
        rec = absolute_position(
            self) if not self.get_pref("pos::initialized") else (
                self.get_pref("pos::x"),
                self.get_pref("pos::y"),
                self.get_pref("pos::width"),
                self.get_pref("pos::height"),
            )
        self.window.wm_manage(self)
        # Allow us to create a hook in the close method of the window manager
        self.bind_close()
        self.title(self.name)
        self.geometry('{}x{}+{}+{}'.format(rec[2], rec[3], rec[0], rec[1]))
        self.update_idletasks()
        self.window_handle = self
        self._view_mode.set("window")
        self.set_pref("mode", "window")
        self.studio._adjust_pane(self.pane)
        self.transient(self.master.window)
        self.save_window_pos()
        if self.focus_get() != self and self.get_pref("inactive_transparency"):
            self.window_handle.wm_attributes('-alpha', 0.3)

    def save_window_pos(self):
        if not self.window_handle:
            if self.winfo_ismapped():
                self.set_pref("pane::height", self.height)
            return
        self.update_idletasks()
        geometry = parse_geometry(self.geometry(), default=0)
        if geometry:
            # more accurate
            # cast geometry values returned to int
            self.set_pref(
                "pos",
                dict({k: int(v)
                      for k, v in geometry.items()},
                     initialized=True))
        else:
            raise Exception("Could not parse window geometry")

    def close_window(self):
        if self.window_handle:
            # Store the current position of our window handle to used when it is reopened
            self.save_window_pos()
            self.master.window.wm_forget(self)
            self.window_handle = None
            self.studio.minimize(self)
            self.set_pref("visible", False)
Ejemplo n.º 11
0
class BaseFeature(Frame):
    _instance = None
    name = "Feature"
    pane = None
    bar = None
    icon = "blank"
    _view_mode = None
    _transparency_flag = None
    _side = None
    rec = (20, 20, 300, 300)  # Default window mode position
    _defaults = {
        "mode": "docked",
        "inactive_transparency": False,
        "position": "left",
        "visible": True,
        "side": "left",
        "pos": {
            "initialized": False,
            "x": 20,
            "y": 20,
            "width": 200,
            "height": 200,
        }
    }

    @classmethod
    def update_defaults(cls):
        path = "features::{}".format(cls.name)
        if not pref.exists(path):
            pref.set(path, dict(**cls._defaults))
        else:
            pref.update_defaults(path, dict(**cls._defaults))

    def __init__(self, master, studio=None, **cnf):
        super().__init__(master, **cnf)
        self.update_defaults()
        self.__class__._instance = self
        if not self.__class__._view_mode:
            self.__class__._view_mode = StringVar(None, self.get_pref('mode'))
            self.__class__._transparency_flag = t = BooleanVar(None, self.get_pref('inactive_transparency'))
            self.__class__._side = side = StringVar(None, self.get_pref('side'))
            t.trace_add("write", lambda *_: self.set_pref('inactive_transparency', t.get()))
        self.studio = studio
        self._header = Frame(self, **self.style.dark, **self.style.dark_highlight_dim, height=30)
        self._header.pack(side="top", fill="x")
        self._header.pack_propagate(0)
        self._header.allow_drag = True
        Label(self._header, **self.style.dark_text_passive, text=self.name).pack(side="left")
        self._min = Button(self._header, image=get_icon_image("close", 15, 15), **self.style.dark_button, width=25,
                           height=25)
        self._min.pack(side="right")
        self._min.on_click(self.minimize)
        self._pref = MenuButton(self._header, **self.style.dark_button)
        self._pref.configure(image=get_icon_image("settings", 15, 15))
        self._pref.pack(side="right")
        self._pref.tooltip("Options")
        self._search_bar = SearchBar(self._header, height=20)
        self._search_bar.on_query_clear(self.on_search_clear)
        self._search_bar.on_query_change(self.on_search_query)
        menu = self.make_menu((
            ("cascade", "View Mode", None, None, {"menu": (
                ("radiobutton", "Docked", None, self.open_as_docked, {"variable": self._view_mode, "value": "docked"}),
                ("radiobutton", "Window", None, self.open_as_window, {"variable": self._view_mode, "value": "window"}),
            )}),
            ("cascade", "Position", None, None, {"menu": (
                ("radiobutton", "Left", None, lambda: self.reposition("left"),
                 {"variable": self._side, "value": "left"}),
                ("radiobutton", "Right", None, lambda: self.reposition("right"),
                 {"variable": self._side, "value": "right"}),
            )}),
            EnableIf(lambda: self._view_mode.get() == 'window',
                     ("cascade", "Window options", None, None, {"menu": (
                         (
                             "checkbutton", "Transparent when inactive", None, None,
                             {"variable": self._transparency_flag}),
                     )})),
            ("command", "Close", get_icon_image("close", 14, 14), self.minimize, {}),
            ("separator",),
            *self.create_menu()
        ), self._pref)
        self._pref.config(menu=menu)
        # self._pref.on_click(self.minimize)
        self.config(**self.style.dark)
        self.indicator = None
        self.window_handle = None
        self.on_focus(self._on_focus_get)
        self.on_focus_lost(self._on_focus_release)
        self.on_close(self.close_window)
        self._mode_map = {
            'window': self.open_as_window,
            'docked': self.open_as_docked
        }

    @classmethod
    def get_pref_path(cls, short_path):
        return "features::{}::{}".format(cls.name, short_path)

    @classmethod
    def get_pref(cls, short_path):
        return pref.get(cls.get_pref_path(short_path))

    @classmethod
    def set_pref(cls, short_path, value):
        pref.set(cls.get_pref_path(short_path), value)

    @classmethod
    def get_instance(cls):
        return cls._instance

    def start_search(self, *_):
        self._search_bar.place(relwidth=1, relheight=1)
        self._search_bar.lift()
        self._search_bar.focus_set()

    def quit_search(self, *_):
        self._search_bar.place_forget()

    def on_search_query(self, query: str):
        """
        Called when inbuilt search feature is queried. Use the query string to display the
        necessary search results
        :param query: String of current search query
        :return: None
        """
        pass

    def on_search_clear(self):
        """
        Called when the user terminates the search bar. Ensure you make a call to the super
        method for the bar to actually get closed. This method can be used to restore the
        feature state to when not performing a search
        :return:
        """
        self.quit_search()
        pass

    def on_select(self, widget):
        """
        Called when a widget is selected in the designer
        :param widget: selected widget
        :return:None
        """
        pass

    def on_widget_change(self, old_widget, new_widget=None):
        """
        Called when a widget is fundamentally altered
        :param old_widget: Altered widget
        :param new_widget: The new widget taking the older widgets place
        :return: None
        """
        pass

    def on_widget_layout_change(self, widget):
        """
        Called when layout options of a widget are changed
        :param widget: Widget with altered layout options
        :return: None
        """
        pass

    def on_widget_add(self, widget, parent):
        """
        Called when a new widget is added to the designer
        :param widget: widget
        :param parent: the container widget to which thw widget is added
        :return: None
        """
        pass

    def on_widget_delete(self, widget, silently=False):
        """
        Called when a widget is deleted from the designer
        :param widget: deleted widget
        :param silently: flag indicating whether the deletion should be treated implicitly
        which is useful for instance when you don't want the deletion to be logged in the
        undo stack
        :return: None
        """
        pass

    def on_widget_restore(self, widget):
        """
        Called when a deleted widget is restored
        :param widget: restored widget
        :return: None
        """
        pass

    def on_session_clear(self):
        """
        Override to perform operations before a session is cleared and the studio
        resets to a new design
        :return: None
        """
        pass

    def on_app_close(self) -> bool:
        """
        Override to perform operations before the studio app closes.
        :return: True to allow shutdown to proceed or False to abort shutdown
        """
        return True

    def minimize(self, *_):
        if self.window_handle:
            self.close_window()
            return
        self.studio.minimize(self)
        self.set_pref("visible", False)

    def maximize(self):
        if self.get_pref("mode") == "window":
            self.open_as_window()
            self.bar.select(self)
        else:
            self.studio.maximize(self)
        self.set_pref("visible", True)

    def toggle(self):
        if self.get_pref("visible"):
            self.minimize()
        else:
            self.maximize()

    def create_menu(self):
        """
        Override this method to provide additional menu options
        :return: tuple of menu templates i.e. (type, label, image, callback, **additional_config)
        """
        # return an empty tuple as default
        return ()

    def _on_focus_release(self):
        if self._transparency_flag.get() and self.window_handle:
            if self.window_handle:
                self.window_handle.wm_attributes('-alpha', 0.3)
        if self.window_handle:
            self.save_window_pos()

    def _on_focus_get(self):
        if self.window_handle:
            self.window_handle.wm_attributes('-alpha', 1.0)

    def open_as_docked(self):
        self._view_mode.set("docked")
        self.set_pref('mode', 'docked')
        if self.window_handle:
            self.master.window.wm_forget(self)
            self.window_handle = None
            self.maximize()

    def reposition(self, side):
        self._side.set(side)
        self.studio.reposition(self, side)

    def open_as_window(self):
        if TkVersion < 8.5:
            logging.error("Window mode is not supported in current tk version")
            return
        self.master.window.wm_forget(self)
        rec = absolute_position(self) if not self.get_pref("pos::initialized") else (
            self.get_pref("pos::x"),
            self.get_pref("pos::y"),
            self.get_pref("pos::width"),
            self.get_pref("pos::height"),
        )
        self.window.wm_manage(self)
        # Allow us to create a hook in the close method of the window manager
        self.bind_close()
        self.title(self.name)
        self.transient(self.master.window)
        self.geometry('{}x{}+{}+{}'.format(rec[2], rec[3], rec[0], rec[1]))
        self.update_idletasks()
        self.window_handle = self
        self._view_mode.set("window")
        self.set_pref("mode", "window")
        self.studio._adjust_pane(self.pane)
        self.save_window_pos()
        if self.focus_get() != self and self.get_pref("inactive_transparency"):
            self.window_handle.wm_attributes('-alpha', 0.3)

    def save_window_pos(self):
        if not self.window_handle:
            return
        self.set_pref("pos", dict(
            x=self.winfo_x(),
            y=self.winfo_y(),
            width=self.width,
            height=self.height,
            initialized=True
        ))

    def close_window(self):
        if self.window_handle:
            # Store the current position of our window handle to used when it is reopened
            self.save_window_pos()
            self.master.window.wm_forget(self)
            self.window_handle = None
            self.studio.minimize(self)
            self.set_pref("visible", False)
Ejemplo n.º 12
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
Ejemplo n.º 13
0
class CollapseFrame(Frame):
    __icons_loaded = False
    EXPAND = None
    COLLAPSE = None

    def __init__(self, master, **cnf):
        super().__init__(master, **cnf)
        self._load_icons()
        self.config(**self.style.surface)
        self._label_frame = Frame(self, **self.style.bright, height=20)
        self._label_frame.pack(side="top", fill="x", padx=2)
        self._label_frame.pack_propagate(0)
        self._label = Label(self._label_frame, **self.style.bright,
                            **self.style.text_bright)
        self._label.pack(side="left")
        self._collapse_btn = Button(self._label_frame,
                                    width=20,
                                    **self.style.bright,
                                    **self.style.text_bright)
        self._collapse_btn.config(image=self.COLLAPSE)
        self._collapse_btn.pack(side="right", fill="y")
        self._collapse_btn.on_click(self.toggle)
        self.body = Frame(self, **self.style.surface)
        self.body.pack(side="top", fill="both", pady=2)
        self.__ref = Frame(self.body, height=0, width=0, **self.style.surface)
        self.__ref.pack(side="top")
        self._collapsed = False

    @classmethod
    def _load_icons(cls):
        if cls.__icons_loaded:
            return
        cls.EXPAND = get_icon_image("triangle_down", 14, 14)
        cls.COLLAPSE = get_icon_image("triangle_up", 14, 14)

    def update_state(self):
        self.__ref.pack(side="top")

    def collapse(self, *_):
        if not self._collapsed:
            self.body.pack_forget()
            self._collapse_btn.config(image=self.EXPAND)
            self.pack_propagate(0)
            self.config(height=20)
            self._collapsed = True

    def clear_children(self):
        self.body.clear_children()

    def expand(self, *_):
        if self._collapsed:
            self.body.pack(side="top", fill="both")
            self.pack_propagate(1)
            self._collapse_btn.config(image=self.COLLAPSE)
            self._collapsed = False

    def toggle(self, *_):
        if self._collapsed:
            self.expand()
        else:
            self.collapse()

    @property
    def label(self):
        return self._label["text"]

    @label.setter
    def label(self, value):
        self._label.config(text=value)
Ejemplo n.º 14
0
class ComponentPane(BaseFeature):
    CLASSES = {
        "native": {
            "widgets": native.widgets
        },
        "legacy": {
            "widgets": legacy.widgets
        },
    }
    name = "Components"
    _var_init = False
    _defaults = {**BaseFeature._defaults, "widget_set": "native"}

    def __init__(self, master, studio=None, **cnf):
        if not self._var_init:
            self._init_var(studio)
        super().__init__(master, studio, **cnf)

        f = Frame(self, **self.style.dark)
        f.pack(side="top", fill="both", expand=True, pady=4)
        f.pack_propagate(0)

        self._widget_set = Spinner(self._header, width=150)
        self._widget_set.config(**self.style.no_highlight)
        self._widget_set.set_values(list(self.CLASSES.keys()))
        self._widget_set.pack(side="left")
        self._widget_set.on_change(self.collect_groups)
        self._select_pane = ScrolledFrame(f, width=150)
        self._select_pane.place(x=0, y=0, relwidth=0.4, relheight=1)

        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._search_selector = Label(self._select_pane.body,
                                      **self.style.dark_text,
                                      text="search",
                                      anchor="w")
        self._search_selector.configure(**self.style.dark_on_hover)

        self._widget_pane = ScrolledFrame(f, width=150, bg="orange")
        self._select_pane.body.config(**self.style.dark)
        self._widget_pane.place(relx=0.4, y=0, relwidth=0.6, relheight=1)

        self._pool = {}
        self._selectors = []
        self._selected = None
        self._component_cache = None
        self.collect_groups(self.get_pref("widget_set"))

    def _init_var(self, master=None):
        self._var_init = True
        for widget_set in self.CLASSES:
            self.CLASSES[widget_set]["var"] = BooleanVar(master, False)

    def _widget_sets_as_menu(self):
        return [
            (
                "checkbutton",  # Type checkbutton
                i.capitalize(),  # Label as title case
                None,  # Image
                partial(self.collect_groups, i),  # The callback
                {
                    "variable": self.CLASSES[i]["var"]
                }  # Additional config including the variable associated
            ) for i in self.CLASSES
        ]

    @property
    def selectors(self):
        return self._selectors

    def create_menu(self):
        return (
            ("command", "Search", get_icon_image("search", 14,
                                                 14), self.start_search, {}),
            ("cascade", "Widget set", None, None, {
                "menu": (*self._widget_sets_as_menu(), )
            }),
        )

    def collect_groups(self, widget_set):
        for other_set in [i for i in self.CLASSES if i != widget_set]:
            self.CLASSES[other_set]["var"].set(False)
        self.CLASSES[widget_set]["var"].set(True)
        self._widget_set.set(widget_set)
        self._select_pane.clear_children()
        self._pool = {}
        components = self.CLASSES.get(widget_set)["widgets"]
        for component in components:
            group = component.group.name
            if group in self._pool:
                self._pool[group].append(
                    Component(self._widget_pane.body, component))
            else:
                self._pool[group] = [
                    Component(self._widget_pane.body, component)
                ]
        self.render_groups()
        # component pool has changed so invalidate the cache
        self._component_cache = None
        self.set_pref("widget_set", widget_set)

    def get_components(self):
        if self._component_cache:
            return self._component_cache
        else:
            # flatten component pool and store to cache
            self._component_cache = [j for i in self._pool.values() for j in i]
            return self._component_cache

    def select(self, selector):
        if self._selected is not None:
            self._selected.deselect()
        selector.select()
        self._selected = selector
        self._widget_pane.clear_children()
        for component in self._pool[selector.name]:
            component.pack(side="top", pady=2, fill="x")

    def render_groups(self):
        self._selectors = []
        for group in self._pool:
            self.add_selector(Selector(self._select_pane.body, text=group))
        if len(self._selectors):
            self.select(self._selectors[0])

    def add_selector(self, selector):
        self._selectors.append(selector)
        selector.bind("<Button-1>", lambda *_: self.select(selector))
        selector.pack(side="top", pady=2, fill="x")

    def hide_selectors(self):
        for selector in self._selectors:
            selector.pack_forget()

    def show_selectors(self):
        for selector in self._selectors:
            selector.pack(side="top", pady=2, fill="x")

    def start_search(self, *_):
        super().start_search()
        self._widget_pane.scroll_to_start()
        if self._selected is not None:
            self._selected.deselect()
        self.hide_selectors()
        self._search_selector.pack(side="top", pady=2, fill="x")
        self._widget_pane.clear_children()
        # Display all components by running an empty query
        self.on_search_query("")

    def on_search_clear(self):
        super().on_search_clear()
        if len(self._selectors):
            self.select(self._selectors[0])
        self._search_selector.pack_forget()
        self.show_selectors()

    def on_search_query(self, query):
        for component in self.get_components():
            if query.lower() in component.component.display_name.lower():
                component.pack(side="top", pady=2, fill="x")
            else:
                component.pack_forget()
Ejemplo n.º 15
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)
Ejemplo n.º 16
0
class ComponentTree(BaseFeature):
    name = "Component Tree"
    icon = "treeview"

    def __init__(self, master, studio=None,  **cnf):
        super().__init__(master, studio, **cnf)
        self._tree = ComponentTreeView(self)
        self._tree.pack(side="top", fill="both", expand=True, pady=4)
        # self._tree.sample()
        self._tree.on_select(self._trigger_select)
        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._selected = None
        self._expanded = False

        self.studio.designer.node = self._tree

    def create_menu(self):
        return (
            ("command", "Expand all", get_icon_image("chevron_down", 14, 14), self._expand, {}),
            ("command", "Collapse all", get_icon_image("chevron_up", 14, 14), self._collapse, {})
        )

    def _expand(self):
        self._tree.expand_all()
        self._toggle_btn.config(image=get_icon_image("chevron_up", 15, 15))
        self._expanded = True

    def _collapse(self):
        self._tree.collapse_all()
        self._toggle_btn.config(image=get_icon_image("chevron_down", 15, 15))
        self._expanded = False

    def _toggle(self, *_):
        if self._expanded:
            self._collapse()
        else:
            self._expand()

    def on_widget_add(self, widget: PseudoWidget, parent=None):
        if parent is None:
            node = self._tree.add_as_node(widget=widget)
        else:
            parent = parent.node
            node = parent.add_as_node(widget=widget)

        # let the designer render the menu for us
        node.bind_all('<Button-3>',
                      lambda e: self.studio.designer.show_menu(e, widget) if self.studio.designer else None)

    def _trigger_select(self):
        if self._selected and self._selected.widget == self._tree.get().widget:
            return
        self.studio.select(self._tree.get().widget, self)
        self._selected = self._tree.get()

    def select(self, widget):
        if widget:
            node = widget.node
            self._selected = node
            node.select(None, True)  # Select node silently to avoid triggering a duplicate selection event
        elif widget is None:
            if self._selected:
                self._selected.deselect()
                self._selected = None

    def on_select(self, widget):
        self.select(widget)

    def on_widget_delete(self, widget, silently=False):
        widget.node.remove()

    def on_widget_restore(self, widget):
        widget.layout.node.add(widget.node)

    def on_widget_layout_change(self, widget):
        node = widget.node
        if widget.layout == self.studio.designer:
            self._tree.insert(None, node)
        else:
            parent = widget.layout.node
            parent.insert(None, node)

    def on_session_clear(self):
        self._tree.clear()

    def on_widget_change(self, old_widget, new_widget=None):
        new_widget = new_widget if new_widget else old_widget
        new_widget.node.widget_modified(new_widget)
Ejemplo n.º 17
0
class Updater(Frame):
    def __init__(self, master):
        super().__init__(master)
        self.config(**self.style.surface)
        self._progress = ProgressBar(self)
        self._progress.pack(side="top", fill="x", padx=20, pady=20)
        self._progress_text = Label(self, **self.style.text_small, anchor="w")
        self._progress_text.pack(side="top", fill="x", padx=20, pady=10)
        self._message = Label(
            self,
            **self.style.text,
            anchor="w",
            compound="left",
            wrap=400,
            justify="left",
            pady=5,
            padx=5,
        )
        self._action_btn = Button(self,
                                  text="Retry",
                                  **self.style.button_highlight,
                                  width=80,
                                  height=25)
        self.extra_info = Text(self,
                               width=40,
                               height=6,
                               state='disabled',
                               font='consolas 10')
        self.pack(fill="both", expand=True)
        self.check_for_update()

    def show_button(self, text, func):
        self._action_btn.config(text=text)
        self._action_btn.on_click(func)
        self._action_btn.pack(side="bottom", anchor="e", pady=5, padx=5)

    def show_progress(self, message):
        self.clear_children()
        self._progress_text.configure(text=message)
        self._progress.pack(side="top", fill="x", padx=20, pady=20)
        self._progress_text.pack(side="top", fill="x", padx=20, pady=10)

    def show_error(self, message, retry_func):
        self.show_error_plain(message)
        self.show_button("Retry", retry_func)

    def show_error_plain(self, message):
        self.show_message(message, MessageDialog.ICON_ERROR)

    def update_found(self, version):
        self.show_info(
            f"New version formation-studio {version} found. Do you want to install?"
        )
        self.show_button("Install", lambda _: self.install(version))

    def show_info(self, message):
        self.show_message(message, MessageDialog.ICON_INFO)

    def show_message(self, message, icon):
        self.clear_children()
        self._message.configure(text=message,
                                image=get_icon_image(icon, 50, 50))
        self._message.pack(side="top", padx=20, pady=10)

    def install(self, version):
        self.show_progress(f"Updating to formation-studio {version}")
        self.upgrade(version)

    @classmethod
    def check(cls, master):
        dialog = MessageDialog(master, cls)
        dialog.title("Formation updater")
        dialog.focus_set()
        return dialog

    @as_thread
    def check_for_update(self, *_):
        self._progress.mode(ProgressBar.INDETERMINATE)
        self.show_progress("Checking for updates ...")
        try:
            content = urlopen(
                "https://pypi.org/pypi/formation-studio/json").read()
            data = json.loads(content)
            ver = data["info"]["version"]
            if ver <= formation.__version__:
                self.show_info("You are using the latest version")
            else:
                self.update_found(ver)
        except URLError:
            self.show_error(
                "Failed to connect. Check your internet connection"
                " and try again.", self.check_for_update)

    @as_thread
    def upgrade(self, version):
        try:
            # run formation cli upgrade command
            proc_info = subprocess.run([sys.executable, "-m", "studio", "-u"],
                                       capture_output=True)
            if proc_info.returncode != 0 or proc_info.stderr:
                self.show_error_plain(
                    "Something went wrong. Failed to upgrade formation-studio"
                    f" to version {version} ,"
                    f" Exited with code: {proc_info.returncode}")
                if proc_info.stderr:
                    self.extra_info.config(state="normal")
                    self.extra_info.pack(side="top",
                                         fill="x",
                                         padx=20,
                                         pady=10)
                    self.extra_info.clear()
                    self.extra_info.set(str(proc_info.stderr))
                    self.extra_info.config(state="disabled")
            else:
                self.show_info(
                    "Upgrade successful. Restart to complete installation")
                self.show_button(
                    "Restart",
                    lambda _: get_routine("STUDIO_RESTART").invoke())
                return
        except Exception as e:
            self.show_error_plain(e)

        self.show_button("Retry", lambda _: self.install(version))
Ejemplo n.º 18
0
class ElementPane(Pane):
    name = "Widget tree"
    MAX_STARTING_DEPTH = 4

    def __init__(self, master, debugger):
        super(ElementPane, self).__init__(master)
        Label(self._header, **self.style.text_accent,
              text=self.name).pack(side="left")

        ElementTreeView.Node.debugger = debugger
        self._tree = ElementTreeView(self)
        self._tree.pack(side="top", fill="both", expand=True, pady=4)
        self._tree.allow_multi_select(True)
        self._tree.on_select(self.on_select)

        self._search_btn = Button(
            self._header,
            **self.style.button,
            image=get_icon_image("search", 15, 15),
            width=25,
            height=25,
        )
        self._search_btn.pack(side="right", padx=2)
        self._search_btn.on_click(self.start_search)

        self._reload_btn = Button(
            self._header,
            **self.style.button,
            image=get_icon_image("rotate_clockwise", 15, 15),
            width=25,
            height=25,
        )
        self._reload_btn.pack(side="right", padx=2)
        self._reload_btn.tooltip("reload tree")

        self._toggle_btn = Button(self._header,
                                  image=get_icon_image("chevron_down", 15, 15),
                                  **self.style.button,
                                  width=25,
                                  height=25)
        self._toggle_btn.pack(side="right", padx=2)

        self._select_btn = ToggleButton(
            self._header,
            **self.style.button,
            image=get_icon_image("cursor", 15, 15),
            width=25,
            height=25,
        )
        self._select_btn.pack(side="right", padx=2)
        self._select_btn.tooltip("select element to inspect")

        self.debugger = debugger
        self._tree.add_as_node(
            widget=debugger.root).update_preload_status(False)

        self.debugger.bind("<<WidgetCreated>>", self.on_widget_created)
        self.debugger.bind("<<WidgetDeleted>>", self.on_widget_deleted)
        self.debugger.bind("<<WidgetModified>>", self.on_widget_modified)

        tkinter.Misc.bind_all(self.debugger.root, "<Motion>", self.on_motion)
        tkinter.Misc.bind_all(self.debugger.root, "<Button-1>",
                              self.on_widget_tap)
        tkinter.Misc.bind_all(self.debugger.root, "<Button-3>",
                              self.on_widget_tap)
        tkinter.Misc.bind_all(self.debugger.root, "<Map>", self.on_widget_map)
        tkinter.Misc.bind_all(self.debugger.root, "<Unmap>",
                              self.on_widget_unmap)
        self.highlighted = None

    @property
    def selected(self):
        return self._tree.get()

    def on_select(self):
        self.debugger.update_selection(self._tree.get())

    def on_widget_tap(self, event):
        if self._select_btn.get():
            try:
                # widget = self.debugger.root.winfo_containing(event.x_root, event.y_root)
                widget = event.widget
                # print(widget)
                if widget.winfo_toplevel() == self.debugger or getattr(
                        widget, "_dbg_ignore", False):
                    widget = None
            except (KeyError, AttributeError):
                widget = None

            if widget:
                node = self._tree.expand_to(widget)
                if node:
                    node.select(event)

    def on_motion(self, event):
        if self._select_btn.get():
            try:
                # widget = self.debugger.root.winfo_containing(event.x_root, event.y_root)
                widget = event.widget
                if widget.winfo_toplevel() == self.debugger or getattr(
                        widget, "_dbg_ignore", False):
                    widget = None
                # print(f"motion : {widget} <> {event.widget}")
            except (KeyError, AttributeError):
                widget = None
            self.debugger.highlight_widget(widget)
            self.highlighted = widget

    def on_widget_created(self, _):
        widget = self.debugger.active_widget
        parent_node = getattr(widget.master, "_dbg_node", None)
        if parent_node:
            if parent_node.loaded:
                parent_node.add_as_node(widget=widget)
                self.debugger.hook_widget(widget)
            else:
                parent_node.update_preload_status(True)

    def on_widget_deleted(self, _):
        widget = self.debugger.active_widget
        parent_node = getattr(widget.master, "_dbg_node", None)
        if parent_node:
            if parent_node.loaded:
                node = widget._dbg_node
                if node in self.selected:
                    self._tree.toggle_from_selection(node)
                parent_node.remove(widget._dbg_node)
            else:
                parent_node.update_preload_status(False)

    def on_widget_modified(self, _):
        if self.debugger.active_widget not in self.selected:
            return

    def on_widget_map(self, _):
        pass

    def on_widget_unmap(self, _):
        pass
Ejemplo n.º 19
0
class EventPane(BaseFeature):
    name = "Event pane"
    icon = "blank"
    _defaults = {
        **BaseFeature._defaults,
        "side": "right",
    }
    NO_SELECTION_MSG = "You have not selected any widget selected"
    NO_EVENT_MSG = "You have not added any bindings"
    NO_MATCH_MSG = "No items match your search"

    def __init__(self, master, studio, **cnf):
        super().__init__(master, studio, **cnf)
        self.header = Frame(self, **self.style.surface)
        self.header.pack(side="top", fill="x")
        for i, title in enumerate(("Sequence", "Handler", "Add", " " * 3)):
            Label(
                self.header,
                **self.style.text_passive,
                text=title,
                anchor="w",
            ).grid(row=0, column=i, sticky='ew')

        # set the first two columns to expand evenly
        for column in range(2):
            self.header.grid_columnconfigure(column, weight=1, uniform=1)

        self.bindings = BindingsTable(self)
        self.bindings.on_value_change(self.modify_item)
        self.bindings.on_item_delete(self.delete_item)
        self.bindings.pack(fill="both", expand=True)

        self._add = Button(self._header,
                           **self.style.button,
                           width=25,
                           height=25,
                           image=get_icon_image("add", 15, 15))
        self._add.pack(side="right")
        self._add.tooltip("Add event binding")
        self._add.on_click(self.add_new)

        self._search_btn = Button(
            self._header,
            **self.style.button,
            image=get_icon_image("search", 15, 15),
            width=25,
            height=25,
        )
        self._search_btn.pack(side="right")
        self._search_btn.on_click(self.start_search)

        self._empty_frame = Label(self.bindings, **self.style.text_passive)
        self._show_empty(self.NO_SELECTION_MSG)

    def _show_empty(self, message):
        self._empty_frame.place(x=0, y=0, relwidth=1, relheight=1)
        self._empty_frame["text"] = message

    def _remove_empty(self):
        self._empty_frame.place_forget()

    def add_new(self, *_):
        if self.studio.selected is None:
            return
        self._remove_empty()
        new_binding = make_event("<>", "", False)
        widget = self.studio.selected
        if not hasattr(widget, "_event_map_"):
            setattr(widget, "_event_map_", {})
        widget._event_map_[new_binding.id] = new_binding
        self.bindings.add(new_binding)

    def delete_item(self, item):
        widget = self.studio.selected
        if widget is None:
            return
        widget._event_map_.pop(item.id)
        self.bindings.remove(item.id)

    def modify_item(self, value: EventBinding):
        widget = self.studio.selected
        widget._event_map_[value.id] = value

    def on_select(self, widget):
        if widget is None:
            self._show_empty(self.NO_SELECTION_MSG)
            return
        self._remove_empty()
        bindings = getattr(widget, "_event_map_", {})
        values = bindings.values()
        self.bindings.clear()
        self.bindings.add(*values)
        if not values:
            self._show_empty(self.NO_EVENT_MSG)

    def start_search(self, *_):
        if self.studio.selected:
            super().start_search()

    def on_search_query(self, query: str):
        showing = 0
        self._remove_empty()
        self.bindings.hide_all()
        for item in self.bindings.items:
            if query in item.value.sequence or query in item.value.handler:
                item.show()
                showing += 1
        if not showing:
            self._show_empty(self.NO_MATCH_MSG)

    def on_search_clear(self):
        self._remove_empty()
        self.bindings.hide_all()
        for item in self.bindings.items:
            item.show()
        super().on_search_clear()
Ejemplo n.º 20
0
class ComponentPane(BaseFeature):
    CLASSES = {
        "native": {"widgets": native.widgets},
        "legacy": {"widgets": legacy.widgets},
    }
    name = "Components"
    _var_init = False
    _defaults = {
        **BaseFeature._defaults,
        "widget_set": "native"
    }
    _custom_pref_path = "studio::custom_widget_paths"

    def __init__(self, master, studio=None, **cnf):
        if not self._var_init:
            self._init_var(studio)
        super().__init__(master, studio, **cnf)

        f = Frame(self, **self.style.surface)
        f.pack(side="top", fill="both", expand=True, pady=4)
        f.pack_propagate(0)

        self._widget_set = Spinner(self._header, width=150)
        self._widget_set.config(**self.style.no_highlight)
        self._widget_set.set_values(list(self.CLASSES.keys()))
        self._widget_set.pack(side="left")
        self._widget_set.on_change(self.collect_groups)
        self._select_pane = ScrolledFrame(f, width=150)
        self._select_pane.place(x=0, y=0, relwidth=0.4, relheight=1)

        self._search_btn = Button(self._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._search_selector = Label(self._select_pane.body, **self.style.text, text="search", anchor="w")
        self._search_selector.configure(**self.style.hover)

        self._widget_pane = ScrolledFrame(f, width=150)
        self._select_pane.body.config(**self.style.surface)
        self._widget_pane.place(relx=0.4, y=0, relwidth=0.6, relheight=1)

        self._pool = {}
        self._selectors = []
        self._selected = None
        self._component_cache = None
        self._extern_groups = []
        self._widget = None
        self.collect_groups(self.get_pref("widget_set"))
        # add custom widgets config to settings
        templates.update(_widget_pref_template)
        self._custom_group = None
        self._custom_widgets = []
        Preferences.acquire().add_listener(self._custom_pref_path, self._init_custom)
        self._reload_custom()

    @property
    def custom_widgets(self):
        return self._custom_widgets

    def auto_find_load_custom(self, *modules):
        # locate and load all custom widgets in modules
        # module can be a module or a path to module file
        self._custom_widgets = []
        errors = {}
        for module in modules:
            if isinstance(module, str):
                try:
                    module = import_path(module)
                except Exception as e:
                    errors[module] = e
                    continue
            for attr in dir(module):
                if type(getattr(module, attr)) == WidgetMeta:
                    self._custom_widgets.append(getattr(module, attr))
        if errors:
            error_msg = "\n\n".join(
                [f"{path}\n{error}" for path, error in errors.items()]
            )
            MessageDialog.show_error(
                parent=self.window,
                message=f"Error loading widgets \n\n{error_msg}"
            )

        return self._custom_widgets

    def _init_custom(self, paths):
        # reload custom widget modules
        try:
            widgets = self.auto_find_load_custom(*paths)
        except Exception as e:

            return

        if not widgets:
            if self._custom_group is not None:
                self.unregister_group(self._custom_group)
                self._custom_group = None
            return

        if self._custom_group is None:
            self._custom_group = self.register_group(
                "Custom",
                widgets,
                ComponentGroup,
            )
        else:
            self._custom_group.update_components(widgets)
            # this will force group to be re-rendered
            self.select(self._custom_group.selector)

    def _reload_custom(self):
        self._init_custom(Preferences.acquire().get(self._custom_pref_path))

    def _init_var(self, master=None):
        self._var_init = True
        for widget_set in self.CLASSES:
            self.CLASSES[widget_set]["var"] = BooleanVar(master, False)

    def _widget_sets_as_menu(self):
        return [
            ("checkbutton",  # Type checkbutton
             i.capitalize(),  # Label as title case
             None,  # Image
             partial(self.collect_groups, i),  # The callback
             {"variable": self.CLASSES[i]["var"]}  # Additional config including the variable associated
             ) for i in self.CLASSES
        ]

    @property
    def selectors(self):
        return self._selectors

    def create_menu(self):
        return (
            (
                "command", "Reload custom widgets",
                get_icon_image("rotate_clockwise", 14, 14), self._reload_custom, {}
            ),
            (
                "command", "Search",
                get_icon_image("search", 14, 14), self.start_search, {}
            ),
            ("cascade", "Widget set", get_icon_image("blank", 14, 14), None, {"menu": (
                *self._widget_sets_as_menu(),
            )}),
        )

    def collect_groups(self, widget_set):
        for other_set in [i for i in self.CLASSES if i != widget_set]:
            self.CLASSES[other_set]["var"].set(False)
        self.CLASSES[widget_set]["var"].set(True)
        self._widget_set.set(widget_set)
        self._select_pane.clear_children()
        self._pool = {}
        components = self.CLASSES.get(widget_set)["widgets"]
        for component in components:
            group = component.group.name
            if group in self._pool:
                self._pool[group].append(Component(self._widget_pane.body, component))
            else:
                self._pool[group] = [Component(self._widget_pane.body, component)]
        self.render_groups()
        # component pool has changed so invalidate the cache
        self._component_cache = None
        self.set_pref("widget_set", widget_set)

    def get_components(self):
        if self._component_cache:
            # cache hit
            return self._component_cache
        # flatten component pool and store to cache
        self._component_cache = [j for i in self._pool.values() for j in i]
        self._component_cache.extend(
            [item for g in self._extern_groups for item in g.components]
        )
        return self._component_cache

    def select(self, selector):
        if self._selected is not None:
            self._selected.deselect()
        selector.select()
        self._selected = selector
        self._widget_pane.clear_children()

        if isinstance(selector.group, ComponentGroup):
            components = selector.group.components
        else:
            components = self._pool[selector.name]

        for component in components:
            component.pack(side="top", pady=2, fill="x")

    def _auto_select(self):
        # automatically pick a selector when no groups have
        # been explicitly selected and the pane is in limbo
        if self._selectors:
            self.select(self._selectors[0])
        else:
            self._widget_pane.clear_children()
            self._selected = None

    def render_groups(self):
        self._selectors = []
        for group in self._pool:
            self.add_selector(Selector(self._select_pane.body, text=group))
        self._auto_select()
        self.render_extern_groups()

    def render_extern_groups(self):
        for group in self._extern_groups:
            if group.supports(self._widget):
                self.add_selector(group.selector)
            else:
                self.remove_selector(group.selector)
                if self._selected == group.selector:
                    self._auto_select()

    def add_selector(self, selector):
        if selector in self._selectors:
            return
        self._selectors.append(selector)
        selector.bind("<Button-1>", lambda *_: self.select(selector))
        selector.pack(side="top", pady=2, fill="x")

    def remove_selector(self, selector):
        if selector in self._selectors:
            self._selectors.remove(selector)
        selector.pack_forget()

    def hide_selectors(self):
        for selector in self._selectors:
            selector.pack_forget()

    def show_selectors(self):
        for selector in self._selectors:
            selector.pack(side="top", pady=2, fill="x")

    def register_group(self, name, items, group_class, evaluator=None, component_class=None):
        group = group_class(self._widget_pane.body, name, items, evaluator, component_class)
        self._extern_groups.append(group)
        # link up selector and group
        group.selector = Selector(self._select_pane.body, text=group.name)
        group.selector.group = group
        self.render_extern_groups()
        return group

    def unregister_group(self, group):
        if group in self._extern_groups:
            self.remove_selector(group.selector)
            self._extern_groups.remove(group)
            self._auto_select()

    def on_select(self, widget):
        self._widget = widget
        self.render_extern_groups()

    def start_search(self, *_):
        super().start_search()
        self._widget_pane.scroll_to_start()
        if self._selected is not None:
            self._selected.deselect()
        self.hide_selectors()
        self._search_selector.pack(side="top", pady=2, fill="x")
        self._widget_pane.clear_children()
        # Display all components by running an empty query
        self.on_search_query("")

    def on_search_clear(self):
        super().on_search_clear()
        if self._selectors:
            self.select(self._selectors[0])
        self._search_selector.pack_forget()
        self.show_selectors()

    def on_search_query(self, query):
        for component in self.get_components():
            if query.lower() in component.component.display_name.lower():
                component.pack(side="top", pady=2, fill="x")
            else:
                component.pack_forget()
Ejemplo n.º 21
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()