Beispiel #1
0
class ShortcutPicker(MessageDialog):

    def __init__(self, master, message, shortcut_pane=None):
        self.message = message
        super().__init__(master, self.render)
        self.title("Shortcut Picker")
        self.resizable(0, 0)
        self.geometry('350x200')
        self.value = None
        self.key = None
        self.shortcut_pane = shortcut_pane

    def on_key_change(self, event):
        self.key = KeyMap.get_key(event)
        if self.shortcut_pane is not None:
            routine = self.shortcut_pane.routine_from_shortcut(self.key)
            if routine is not None and self.shortcut_pane:
                self._warning['text'] = f"Key already assigned to {routine.desc}"
                self._warning.pack(fill="x")
            else:
                self._warning.pack_forget()
        self.event_pad.config(text=self.key.label)
        # returning break ensures this event does not propagate
        # preventing the event from invoking currently set bindings
        return "break"

    def render(self, _):
        self.detail = Label(self, **self.style.dark_text, text=self.message)
        self.detail.pack(fill="x")
        warn_frame = Frame(self, **self.style.dark)
        self._warning = Label(
            warn_frame,
            **self.style.dark_text_passive,
            padx=5,
            anchor='w',
            compound="left",
            image=get_tk_image("dialog_warning", 15, 15),
        )
        self.event_pad = Label(
            self, **self.style.dark_text_accent)
        self._add_button(text="Cancel", value=None)
        self._add_button(text="Okay", command=self.exit_with_key, focus=True)
        warn_frame.pack(side="bottom", fill="x")
        self.event_pad.config(
            **self.style.bright, takefocus=True,
            text="Tap here to begin capturing shortcuts."
        )
        self.event_pad.bind("<Any-KeyPress>", self.on_key_change)
        self.event_pad.bind("<Button-1>", lambda e: self.event_pad.focus_set())
        self.event_pad.pack(fill="both", expand=True)

    def exit_with_key(self, _):
        self.value = self.key
        self.destroy()

    @classmethod
    def pick(cls, master, message, shortcut_pane=None):
        picker = cls(master, message, shortcut_pane)
        picker.wait_window()
        return picker.value
Beispiel #2
0
    class ShortcutItem(CompoundList.BaseItem):
        def __init__(self, master, value, index, isolated=False):
            super().__init__(master, value, index, isolated)
            initial_key = value[1]
            self.key = initial_key

        def set_key(self, key):
            self.key_label.config(text=key.label)
            self.key = key
            self._value = (self.value[0], key)

        def render(self):
            self.key_label = Label(self,
                                   text=self.value[1].label,
                                   **self.style.dark_text_accent)
            self.key_label.pack(side="right")
            routine = actions.get_routine(self.value[0])
            self.desc = Label(self, text=routine.desc, **self.style.dark_text)
            self.desc.pack(side="left")

        def on_hover_ended(self, *_):
            self.config_all(**self.style.dark)

        def on_hover(self, *_):
            self.config_all(**self.style.bright)

        def disable(self, flag):
            self.disabled(flag)
            if flag:
                self.desc.config(**self.style.dark_text_passive)
                self.key_label.config(**self.style.dark_text_passive)
            else:
                self.desc.config(**self.style.dark_text)
                self.key_label.config(**self.style.dark_text_accent)
Beispiel #3
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)
Beispiel #4
0
class GridConfig(Frame):
    def __init__(self, master, pane, **cnf):
        super().__init__(master)
        self._title = Label(self, **self.style.text_accent)
        self._title.pack(side="top", fill="x")
        self._tab_view = TabView(self)
        self._tab_view.pack(fill="both")
        self.column_config = ColumnConfig(self, pane, **cnf)
        self.row_config = RowConfig(self, pane, **cnf)
        self._tab_view.add(self.column_config, text="Column")
        self._tab_view.add(self.row_config, text="Row")
Beispiel #5
0
class Component(Frame):

    def __init__(self, master, component: PseudoWidget.__class__, _=None):
        super().__init__(master)
        self.config(**self.style.surface)
        self._icon = Label(self, **self.style.text_accent, image=get_icon_image(component.icon, 15, 15))
        self._icon.pack(side="left")
        self._text = Label(self, **self.style.text, anchor="w", text=component.display_name)
        self._text.pack(side="left", fill="x")
        self.bind("<Enter>", self.select)
        self.bind("<Leave>", self.deselect)
        self.component = component
        self.allow_drag = True

    def select(self, *_):
        self.config_all(**self.style.hover)

    def deselect(self, *_):
        self.config_all(**self.style.surface)

    def render_drag(self, window):
        Label(window, **self.style.text_accent, image=get_icon_image(self.component.icon, 15, 15)).pack(side="left")
        Label(window, **self.style.text, anchor="w", text=self.component.display_name).pack(side="left", fill="x")

    def drag_start_pos(self, event):
        window = self.window.drag_window
        if window:
            window.update_idletasks()
            return (
                event.x_root - int(window.winfo_width() / 2),
                event.y_root - int(window.winfo_height() / 2)
            )
        return super(Component, self).drag_start_pos(event)

    def _adjust_event(self, event):
        # adjust event position so it appears out the drag window
        # this allows us to determine studio widget at said position
        if self.window.drag_window:
            event.x_root = self.window.drag_window.get_center()[0]
            event.y_root = self.window.drag_window.pos[1] - 1

    def on_drag(self, event):
        self._adjust_event(event)
        widget = self.event_first(event, self, Container)
        if widget and self.window.drag_window:
            widget.react(*self.window.drag_window.get_center())

    def on_drag_end(self, event):
        self._adjust_event(event)
        widget = self.event_first(event, self, Container)
        if isinstance(widget, Container):
            widget.add_new(self.component, *self.window.drag_window.get_center())
Beispiel #6
0
class Component(Frame):
    drag_popup = None
    drag_active = None

    def __init__(self, master, component: PseudoWidget.__class__):
        super().__init__(master)
        self.config(**self.style.dark)
        self._icon = Label(self,
                           **self.style.dark_text_accent,
                           image=get_icon_image(component.icon, 15, 15))
        self._icon.pack(side="left")
        self._text = Label(self,
                           **self.style.dark_text,
                           anchor="w",
                           text=component.display_name)
        self._text.pack(side="left", fill="x")
        self.bind("<Enter>", self.select)
        self.bind("<Leave>", self.deselect)
        self.component = component

        self.bind_all("<Motion>", self.drag)
        self.bind_all("<ButtonRelease-1>", self.release)

    def select(self, *_):
        self.config_all(**self.style.dark_on_hover)

    def deselect(self, *_):
        self.config_all(**self.style.dark)

    def drag(self, event):
        # If cursor is moved while holding the left button down for the first time we begin drag
        if event.state & EventMask.MOUSE_BUTTON_1 and not self.drag_active:
            self.drag_popup = DragWindow(self.window).set_position(
                event.x_root, event.y_root)
            Label(self.drag_popup, text=self.component.display_name).pack()
            self.drag_active = True
        elif self.drag_active:
            widget = self.event_first(event, self, Designer)
            if isinstance(widget, Designer):
                widget.react(event)
            self.drag_popup.set_position(event.x_root, event.y_root)

    def release(self, event):
        if not self.drag_active:
            return
        self.drag_active = False
        self.drag_popup.destroy()
        self.drag_popup = None
        widget = self.event_first(event, self, Container)
        if isinstance(widget, Container):
            widget.add_new(self.component, event.x_root, event.y_root)
Beispiel #7
0
class CoordinateIndicator(Frame):
    def __init__(self, master, **cnf):
        super().__init__(master, **cnf)
        Label(self, **self.style.dark_text_accent_1, text="x: ",
              width=3).pack(side='left')
        self._x = Label(self, **self.style.dark_text, width=5, anchor='w')
        self._x.pack(side='left')
        Label(self, **self.style.dark_text_accent_1, text="y: ",
              width=3).pack(side='left')
        self._y = Label(self, **self.style.dark_text, width=5, anchor='w')
        self._y.pack(side='left')

    def set_coord(self, x, y):
        self._y['text'] = int(y)
        self._x['text'] = int(x)
Beispiel #8
0
class ComponentTreeView(MalleableTreeView):
    class Node(MalleableTreeView.Node):
        def __init__(self, master=None, **config):
            super().__init__(master, **config)
            self.widget: PseudoWidget = config.get("widget")
            self.widget.node = self
            self.name_pad.configure(text=self.widget.id)
            self.icon_pad.configure(
                image=get_icon_image(self.widget.icon, 15, 15))

        def widget_modified(self, widget):
            self.widget = widget
            self.name_pad.configure(text=self.widget.id)
            self.icon_pad.configure(
                image=get_icon_image(self.widget.icon, 15, 15))

    def initialize_tree(self):
        super(ComponentTreeView, self).initialize_tree()
        self._empty = Frame(self, **self.style.surface)
        self._empty_text = Label(self._empty, **self.style.text_passive)
        self._empty_text.pack(fill="both", expand=True, pady=30)
        self._show_empty("No items created yet")

    def add(self, node):
        super().add(node)
        self._remove_empty()

    def insert(self, index=None, *nodes):
        super(ComponentTreeView, self).insert(index, *nodes)
        self._remove_empty()

    def remove(self, node):
        super().remove(node)
        if len(self.nodes) == 0:
            self._show_empty("No items created yet")

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

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

    def search(self, query):
        if not super().search(query):
            self._show_empty("No items match your search")
        else:
            self._remove_empty()
Beispiel #9
0
class Dimension(Number):
    SHORT_FORMS = {
        "pixels": "px",
    }

    def __init__(self, master, style_def=None):
        super().__init__(master, style_def)
        self._entry.config(from_=0, to=1e6)
        self._entry.set_validator(numeric_limit, 0, 1e6)
        self._entry.pack_forget()
        self._unit = Label(self, **self.style.dark_text_passive)
        self._unit.pack(side="right")
        self.set_def(style_def)
        self._entry.pack(side="left", fill="x")

    def set_def(self, definition):
        self._unit['text'] = self.SHORT_FORMS.get(
            definition.get("units", "pixels"), 'px')
        super().set_def(definition)
Beispiel #10
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()
Beispiel #11
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)
Beispiel #12
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()
Beispiel #13
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()
Beispiel #14
0
class StyleGroup(CollapseFrame):
    """
    Main subdivision of the Style pane
    """
    handles_layout = False
    self_positioned = False

    def __init__(self, master, pane, **cnf):
        super().__init__(master)
        self.style_pane = pane
        self.configure(**{**self.style.surface, **cnf})
        self._empty_message = "Select an item to see styles"
        self._empty = Frame(self.body, **self.style.surface)
        self._empty_label = Label(
            self._empty,
            **self.style.text_passive,
        )
        self._empty_label.pack(fill="both", expand=True, pady=15)
        self._widget = None
        self._prev_widget = None
        self._has_initialized = False  # Flag to mark whether Style Items have been created
        self.items = {}

    @property
    def widget(self):
        return self._widget

    def can_optimize(self):
        return False

    def add(self, style_item):
        self.items[style_item.name] = style_item
        if self.style_pane._search_query is not None:
            if self._match_query(style_item.definition,
                                 self.style_pane._search_query):
                self._show(style_item)
            # make sure item is not available for reuse whether it
            # is displayed or not
            style_item._make_available(False)
        else:
            self._show(style_item)

    def remove(self, style_item):
        if style_item.name in self.items:
            self.items.pop(style_item.name)
        self._hide(style_item)

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

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

    def _get_prop(self, prop, widget):
        return widget.get_prop(prop)

    def _set_prop(self, prop, value, widget):
        widget.configure(**{prop: value})

    def _hide_group(self):
        pass

    def _show_group(self):
        pass

    def _match_query(self, definition, query):
        return query in definition["name"] or query in definition[
            "display_name"]

    def _show_empty(self, text=None):
        self._empty.pack(fill="both", expand=True)
        text = self._empty_message if text is None else text
        self._empty_label["text"] = text

    def _remove_empty(self):
        self._empty.pack_forget()

    def on_widget_change(self, widget):
        self._widget = widget
        if widget is None:
            self.collapse()
            return
        definitions = self.get_definition()
        if self.can_optimize():
            for prop in definitions:
                self.items[prop]._re_purposed(definitions[prop])
        else:
            self.style_pane.show_loading()
            # this unmaps all style items returning them to the pool for reuse
            self.clear_children()
            # make all items held by group available for reuse
            ReusableStyleItem.free_all(self.items.values())
            self.items.clear()
            add = self.add
            list(
                map(
                    lambda p: add(
                        ReusableStyleItem.acquire(self, definitions[p], self.
                                                  apply), ), definitions))
            if not self.items:
                self._show_empty()
            else:
                self._remove_empty()
            # self.style_pane.body.scroll_to_start()

        self._has_initialized = True
        self._prev_widget = widget

    def _apply_action(self, prop, value, widget, data):
        self.apply(prop, value, widget, True)

    def _get_action_data(self, widget, prop):
        return {}

    def _get_key(self, widget, prop):
        return f"{widget}:{self.__class__.__name__}:{prop}"

    def apply(self, prop, value, widget=None, silent=False):
        is_external = widget is not None
        widget = self.widget if widget is None else widget
        if widget is None:
            return
        try:
            prev_val = self._get_prop(prop, widget)
            data = self._get_action_data(widget, prop)
            self._set_prop(prop, value, widget)
            new_data = self._get_action_data(widget, prop)
            self.style_pane.widget_modified(widget)
            if is_external:
                if widget == self.widget:
                    self.items[prop].set_silently(value)
            if silent:
                return
            key = self._get_key(widget, prop)
            action = self.style_pane.last_action()
            if action is None or action.key != key:
                self.style_pane.new_action(
                    Action(
                        lambda _: self._apply_action(prop, prev_val, widget,
                                                     data),
                        lambda _: self._apply_action(prop, value, widget,
                                                     new_data),
                        key=key,
                    ))
            else:
                action.update_redo(lambda _: self._apply_action(
                    prop, value, widget, new_data))
        except Exception as e:
            # Empty string values are too common to be useful in logger debug
            if value != '':
                logging.error(e)
                logging.error(
                    f"Could not set {self.__class__.__name__} {prop} as {value}",
                )

    def get_definition(self):
        return {}

    def supports_widget(self, widget):
        return True

    def on_search_query(self, query):
        item_found = False
        for item in self.items.values():
            if self._match_query(item.definition, query):
                self._show(item)
                item_found = True
            else:
                self._hide(item)
        if not item_found:
            self._show_empty("No items match your search")
        else:
            self._remove_empty()

    def on_search_clear(self):
        # Calling search query with empty query ensures all items are displayed
        self.clear_children()
        self.on_search_query("")
Beispiel #15
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))
Beispiel #16
0
class ResourceLoader(Application):
    _default_icon_path = _primary_location
    _cache_icon_path = _primary_location

    def __init__(self, pref):
        super().__init__()
        self.pref = pref
        self.load_styles(pref.get("resource::theme"))
        try:
            self.wm_attributes("-type", "splash")
        except:
            self.enable_centering()
            self.overrideredirect(1)

        self.configure(**self.style.surface)
        image = load_tk_image(
            get_resource_path("studio", "resources/images/logo.png"), 240, 77)
        Label(self, image=image, **self.style.surface).pack(side="top",
                                                            fill="y",
                                                            padx=20,
                                                            pady=20)
        self._progress = ProgressBar(self)
        self._progress.pack(side="top", fill="x", padx=20, pady=10)
        self._progress.set(0)
        self._progress_text = Label(self,
                                    **self.style.text_small,
                                    text="Waiting for resource loader...",
                                    anchor="w")
        self._progress_text.pack(side="top", fill="x", padx=20, pady=10)
        self.update_idletasks()
        # give the loader some time to render before starting load
        self.after(200, self.start_load)

    def start_load(self):
        self.check_resources()
        self.destroy()

    def update_progress(self, value, append=True):
        value = value if not append else self._progress.get() + float(value)
        self._progress.set(value)

    def _message(self, text):
        self._progress_text["text"] = text

    @classmethod
    def _cache_exists(cls, path):
        if os.path.exists(path):
            return True
        # for windows we may need the extension
        return os.path.exists(path + ".dat")

    @classmethod
    def _cache_is_stale(cls):
        # check whether cache is outdated
        with shelve.open(cls._cache_icon_path) as cache:
            with shelve.open(cls._default_icon_path) as defaults:
                # return false if all keys in default are in cache
                return len(defaults.keys() - cache.keys())

    @classmethod
    def load(cls, pref):
        cache_color = pref.get("resource::icon_cache_color")
        style = StyleDelegator(get_theme_path(pref.get("resource::theme")))
        cache_path = pref.get_cache_dir()
        cls._cache_icon_path = os.path.join(cache_path, "image")
        if style.colors["accent"] != cache_color \
                or not cls._cache_exists(cls._cache_icon_path)\
                or cls._cache_is_stale():
            if not os.path.exists(cache_path):
                make_path(cache_path)
            cls(pref).mainloop()

        set_image_resource_path(cls._cache_icon_path)
        pref.set("resource::icon_cache_color", style.colors["accent"])

    def check_resources(self):

        self._message("Preparing graphic resources...")
        with shelve.open(self._cache_icon_path) as cache:
            with shelve.open(self._default_icon_path) as defaults:
                color = parse_color(self.style.colors["accent"], self)
                step = 1 / len(defaults) * 1
                for image in defaults:
                    if not image.startswith("_"):
                        cache[image] = _recolor(defaults[image], color)
                    else:
                        cache[image] = defaults[image]
                    self.update_progress(step)