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 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 StylePane(BaseFeature): name = "Style pane" icon = "edit" _defaults = { **BaseFeature._defaults, "side": "right", } def __init__(self, master, studio, **cnf): super().__init__(master, studio, **cnf) self.body = ScrolledFrame(self, **self.style.dark) self.body.pack(side="top", fill="both", expand=True) self._toggle_btn = Button(self._header, image=get_icon_image("chevron_down", 15, 15), **self.style.dark_button, width=25, height=25) self._toggle_btn.pack(side="right") self._toggle_btn.on_click(self._toggle) self._search_btn = Button(self._header, image=get_icon_image("search", 15, 15), width=25, height=25, **self.style.dark_button) self._search_btn.pack(side="right") self._search_btn.on_click(self.start_search) self.groups = [] self._identity_group = self.add_group(IdentityGroup) self._layout_group = self.add_group(LayoutGroup) self._attribute_group = self.add_group(AttributeGroup) self._empty_frame = Frame(self.body) self.show_empty() self._current = None self._expanded = False def create_menu(self): return (("command", "Search", get_icon_image("search", 14, 14), self.start_search, {}), ("command", "Expand all", get_icon_image("chevron_down", 14, 14), self.expand_all, {}), ("command", "Collapse all", get_icon_image("chevron_up", 14, 14), self.collapse_all, {})) def add_group(self, group_class) -> StyleGroup: if not issubclass(group_class, StyleGroup): raise ValueError('type required.') group = group_class(self.body.body) self.groups.append(group) group.pack(side='top', fill='x', pady=4) return group def show_empty(self): self.remove_empty() self._empty_frame.place(x=0, y=0, relheight=1, relwidth=1) Label(self._empty_frame, text="You have not selected any item", **self.style.dark_text_passive).place(x=0, y=0, relheight=1, relwidth=1) def remove_empty(self): self._empty_frame.clear_children() self._empty_frame.place_forget() def show_loading(self): self.remove_empty() self._empty_frame.place(x=0, y=0, relheight=1, relwidth=1) Label(self._empty_frame, text="Loading...", **self.style.dark_text_passive).place(x=0, y=0, relheight=1, relwidth=1) def styles_for(self, widget): self._current = widget if widget is None: self.show_empty() return self.show_loading() for group in self.groups: group.on_widget_change(widget) self.remove_empty() self.body.update_idletasks() def layout_for(self, widget): self._layout_group.on_widget_change(widget) def on_select(self, widget): self.styles_for(widget) def on_widget_change(self, old_widget, new_widget=None): self.styles_for(new_widget) def on_widget_layout_change(self, widget): self.layout_for(widget) def expand_all(self): for group in self.groups: group.expand() self._expanded = True self._toggle_btn.config(image=get_icon_image("chevron_up", 15, 15)) def clear_all(self): for group in self.groups: group.clear_children() def collapse_all(self): for group in self.groups: group.collapse() self._expanded = False self._toggle_btn.config(image=get_icon_image("chevron_down", 15, 15)) def _toggle(self, *_): if not self._expanded: self.expand_all() else: self.collapse_all() def __update_frames(self): for group in self.groups: group.update_state() def start_search(self, *_): if self._current: super().start_search() self.body.scroll_to_start() def on_search_query(self, query): for group in self.groups: group.on_search_query(query) self.__update_frames() def on_search_clear(self): for group in self.groups: group.on_search_clear() # The search bar is being closed and we need to bring everything back super().on_search_clear()
class StylePaneFramework: def setup_style_pane(self): self.body = ScrolledFrame(self, **self.style.surface) self.body.pack(side="top", fill="both", expand=True) self._toggle_btn = Button(self.get_header(), image=get_icon_image("chevron_down", 15, 15), **self.style.button, width=25, height=25) self._toggle_btn.pack(side="right") self._toggle_btn.on_click(self._toggle) self._search_btn = Button(self.get_header(), image=get_icon_image("search", 15, 15), width=25, height=25, **self.style.button) self._search_btn.pack(side="right") self._search_btn.on_click(self.start_search) self.groups = [] self._empty_frame = Frame(self.body) self.show_empty() self._current = None self._expanded = False self._is_loading = False self._search_query = None def get_header(self): raise NotImplementedError() @property def supported_groups(self): return [ group for group in self.groups if group.supports_widget(self._current) ] def create_menu(self): return (("command", "Search", get_icon_image("search", 14, 14), self.start_search, {}), ("command", "Expand all", get_icon_image("chevron_down", 14, 14), self.expand_all, {}), ("command", "Collapse all", get_icon_image("chevron_up", 14, 14), self.collapse_all, {})) def extern_apply(self, group_class, prop, value, widget=None, silent=False): for group in self.groups: if group.__class__ == group_class: group.apply(prop, value, widget, silent) return raise ValueError(f"Class {group_class.__class__.__name__} not found") def last_action(self): raise NotImplementedError() def new_action(self, action): raise NotImplementedError() def widget_modified(self, widget): raise NotImplementedError() def add_group(self, group_class, **kwargs) -> StyleGroup: if not issubclass(group_class, StyleGroup): raise ValueError('type required.') group = group_class(self.body.body, self, **kwargs) self.groups.append(group) self.show_group(group) return group def add_group_instance(self, group_instance, show=False): if not isinstance(group_instance, StyleGroup): raise ValueError('Expected object of type StyleGroup.') self.groups.append(group_instance) if show: self.show_group(group_instance) def hide_group(self, group): if group.self_positioned: group._hide_group() return group.pack_forget() def show_group(self, group): if group.self_positioned: group._show_group() return group.pack(side='top', fill='x', pady=12) def show_empty(self): self.remove_empty() self._empty_frame.place(x=0, y=0, relheight=1, relwidth=1) Label(self._empty_frame, text="You have not selected any item", **self.style.text_passive).place(x=0, y=0, relheight=1, relwidth=1) def remove_empty(self): self._empty_frame.clear_children() self._empty_frame.place_forget() def show_loading(self): if platform_is(LINUX) or self._is_loading: # render transitions in linux are very fast and glitch free # for other platforms or at least for windows we need to hide the glitching return self.remove_empty() self._empty_frame.place(x=0, y=0, relheight=1, relwidth=1) Label(self._empty_frame, text="Loading...", **self.style.text_passive).place(x=0, y=0, relheight=1, relwidth=1) self._is_loading = True def remove_loading(self): self.remove_empty() self._is_loading = False def styles_for(self, widget): self._current = widget if widget is None: self.show_empty() return for group in self.groups: if group.supports_widget(widget): self.show_group(group) group.on_widget_change(widget) else: self.hide_group(group) self.remove_loading() self.body.update_idletasks() def layout_for(self, widget): for group in self.groups: if group.handles_layout: group.on_widget_change(widget) self.remove_loading() def on_select(self, widget): self.styles_for(widget) def on_widget_change(self, old_widget, new_widget=None): if new_widget is None: new_widget = old_widget self.styles_for(new_widget) def on_widget_layout_change(self, widget): self.layout_for(widget) def expand_all(self): for group in self.groups: group.expand() self._expanded = True self._toggle_btn.config(image=get_icon_image("chevron_up", 15, 15)) def clear_all(self): for group in self.groups: group.clear_children() def collapse_all(self): for group in self.groups: group.collapse() self._expanded = False self._toggle_btn.config(image=get_icon_image("chevron_down", 15, 15)) def _toggle(self, *_): if not self._expanded: self.expand_all() else: self.collapse_all() def __update_frames(self): for group in self.groups: group.update_state() def start_search(self, *_): if self._current: super().start_search() self.body.scroll_to_start() def on_search_query(self, query): for group in self.groups: group.on_search_query(query) self.__update_frames() self.body.scroll_to_start() self._search_query = query def on_search_clear(self): for group in self.groups: group.on_search_clear() # The search bar is being closed and we need to bring everything back super().on_search_clear() self._search_query = None
class VariablePane(BaseFeature): name = "Variablepane" icon = "text" _defaults = {**BaseFeature._defaults, "side": "right"} _definitions = { "name": { "name": "name", "type": "text", } } def __init__(self, master, studio=None, **cnf): super().__init__(master, studio, **cnf) f = Frame(self, **self.style.dark) f.pack(side="top", fill="both", expand=True, pady=4) f.pack_propagate(0) self._variable_pane = ScrolledFrame(f, width=150) self._variable_pane.place(x=0, y=0, relwidth=0.4, relheight=1) self._detail_pane = ScrolledFrame(f, width=150) self._detail_pane.place(relx=0.4, y=0, relwidth=0.6, relheight=1, x=15, width=-20) self._search_btn = Button(self._header, image=get_icon_image("search", 15, 15), width=25, height=25, **self.style.dark_button) self._search_btn.pack(side="right") self._search_btn.on_click(self.start_search) self._add = MenuButton(self._header, **self.style.dark_button) self._add.configure(image=get_icon_image("add", 15, 15)) self._add.pack(side="right") self._delete_btn = Button(self._header, image=get_icon_image("delete", 15, 15), width=25, height=25, **self.style.dark_button) self._delete_btn.pack(side="right") self._delete_btn.on_click(self._delete) self._var_types_menu = self.make_menu(self._get_add_menu(), self._add) self._add.config(menu=self._var_types_menu) self._selected = None self._links = {} self._overlay = Label(f, **self.style.dark_text_passive, text="Add variables", compound="top") self._overlay.configure(image=get_icon_image("add", 25, 25)) self._show_overlay(True) self._editors = [] def start_search(self, *_): if len(self.variables): super().start_search() self._variable_pane.scroll_to_start() def on_search_query(self, query): matches = [] self._variable_pane.clear_children() for item in self.variables: if query in item.name: self._show(item) matches.append(item) if not len(matches): self._show_overlay(True, text="No matches found", image=get_icon_image("search", 25, 25)) else: self.select(matches[0]) self._show_overlay(False) def on_search_clear(self): self.on_search_query("") self._show_overlay(False) super().on_search_clear() def _get_add_menu(self): _types = VariableItem._types return [(tk.COMMAND, _types[i].get("name"), get_icon_image(_types[i].get("icon"), 14, 14), functools.partial(self.add_var, i), {}) for i in _types] def create_menu(self): return ( ("cascade", "Add", get_icon_image("add", 14, 14), None, { "menu": self._get_add_menu() }), ("command", "Delete", get_icon_image("delete", 14, 14), self._delete, {}), ("command", "Search", get_icon_image("search", 14, 14), self.start_search, {}), ) def _show_overlay(self, flag=True, **kwargs): if flag: self._overlay.lift() self._overlay.configure(**kwargs) self._overlay.place(x=0, y=0, relwidth=1, relheight=1) else: self._overlay.place_forget() def add_var(self, var_type, **kw): var = var_type(self.studio) item_count = len( list(filter(lambda x: x.var_type == var_type, self.variables))) + 1 name = kw.get('name', f"{var_type.__name__}_{item_count}") value = kw.get('value') item = VariableItem(self._variable_pane.body, var, name) item.bind("<Button-1>", lambda e: self.select(item)) if value is not None: item.set(value) VariableManager.add(item) self._show(item) self._show_overlay(False) self.select(item) def delete_var(self, var): self._hide(var) VariableManager.remove(var) def _delete(self, *_): if self._selected: self.delete_var(self._selected) if len(self.variables): self.select(self.variables[0]) else: self._show_overlay(True) def clear_variables(self): # the list is likely to change during iteration, create local copy variables = list(self.variables) for var in variables: self.delete_var(var) self._show_overlay(True) @property def variables(self): return VariableManager.variables def select(self, item): if item == self._selected: return item.select() if self._selected: self._selected.deselect() self._selected = item self._detail_for(item) def _show(self, item): item.pack(side="top", fill="x") def _hide(self, item): item.pack_forget() def _detail_for(self, variable): self._detail_pane.clear_children() Label(self._detail_pane.body, **self.style.dark_text_passive, text="Type", anchor="w").pack(fill="x", side="top") Label(self._detail_pane.body, **self.style.dark_text, text=variable.var_type_name, anchor="w").pack(fill="x", side="top") Label(self._detail_pane.body, **self.style.dark_text_passive, text="Name", anchor="w").pack(fill="x", side="top") name = editors.get_editor(self._detail_pane.body, self._definitions["name"]) name.pack(side="top", fill="x") name.set(variable.name) name.on_change(variable.set_name) Label(self._detail_pane.body, **self.style.dark_text_passive, text="Value", anchor="w").pack(fill="x", side="top") value = editors.get_editor(self._detail_pane.body, variable.definition) value.set(variable.value) value.pack(side="top", fill="x") value.on_change(variable.set) def on_session_clear(self): self.clear_variables()