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
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)
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)
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")
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())
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)
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)
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()
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)
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()
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)
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()
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()
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("")
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))
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)