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 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 ComponentTree(BaseFeature): name = "Component Tree" icon = "treeview" def __init__(self, master, studio=None, **cnf): super().__init__(master, studio, **cnf) self._tree = ComponentTreeView(self) self._tree.pack(side="top", fill="both", expand=True, pady=4) # self._tree.sample() self._tree.on_select(self._trigger_select) self._toggle_btn = Button(self._header, image=get_icon_image("chevron_down", 15, 15), **self.style.dark_button, width=25, height=25) self._toggle_btn.pack(side="right") self._toggle_btn.on_click(self._toggle) self._selected = None self._expanded = False self.studio.designer.node = self._tree def create_menu(self): return ( ("command", "Expand all", get_icon_image("chevron_down", 14, 14), self._expand, {}), ("command", "Collapse all", get_icon_image("chevron_up", 14, 14), self._collapse, {}) ) def _expand(self): self._tree.expand_all() self._toggle_btn.config(image=get_icon_image("chevron_up", 15, 15)) self._expanded = True def _collapse(self): self._tree.collapse_all() self._toggle_btn.config(image=get_icon_image("chevron_down", 15, 15)) self._expanded = False def _toggle(self, *_): if self._expanded: self._collapse() else: self._expand() def on_widget_add(self, widget: PseudoWidget, parent=None): if parent is None: node = self._tree.add_as_node(widget=widget) else: parent = parent.node node = parent.add_as_node(widget=widget) # let the designer render the menu for us node.bind_all('<Button-3>', lambda e: self.studio.designer.show_menu(e, widget) if self.studio.designer else None) def _trigger_select(self): if self._selected and self._selected.widget == self._tree.get().widget: return self.studio.select(self._tree.get().widget, self) self._selected = self._tree.get() def select(self, widget): if widget: node = widget.node self._selected = node node.select(None, True) # Select node silently to avoid triggering a duplicate selection event elif widget is None: if self._selected: self._selected.deselect() self._selected = None def on_select(self, widget): self.select(widget) def on_widget_delete(self, widget, silently=False): widget.node.remove() def on_widget_restore(self, widget): widget.layout.node.add(widget.node) def on_widget_layout_change(self, widget): node = widget.node if widget.layout == self.studio.designer: self._tree.insert(None, node) else: parent = widget.layout.node parent.insert(None, node) def on_session_clear(self): self._tree.clear() def on_widget_change(self, old_widget, new_widget=None): new_widget = new_widget if new_widget else old_widget new_widget.node.widget_modified(new_widget)
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 ComponentTree(BaseFeature): name = "Component Tree" icon = "treeview" def __init__(self, master, studio=None, **cnf): super().__init__(master, studio, **cnf) self._toggle_btn = Button(self._header, image=get_icon_image("chevron_down", 15, 15), **self.style.button, width=25, height=25) self._toggle_btn.pack(side="right") self._toggle_btn.on_click(self._toggle) self._search_btn = Button( self._header, **self.style.button, image=get_icon_image("search", 15, 15), width=25, height=25, ) self._search_btn.pack(side="right") self._search_btn.on_click(self.start_search) self.body = Frame(self, **self.style.surface) self.body.pack(side="top", fill="both", expand=True) self._empty_label = Label(self.body, **self.style.text_passive) self._selected = None self._expanded = False self._tree = None def on_context_switch(self): if self._tree: self._tree.pack_forget() if self.studio.designer: self.show_empty(None) if self.studio.designer.node: self._tree = self.studio.designer.node else: self._tree = ComponentTreeView(self.body) self._tree.on_select(self._trigger_select) self.studio.designer.node = self._tree self._tree.pack(fill="both", expand=True) else: self.show_empty("No active Designer") def create_menu(self): return (("command", "Expand all", get_icon_image("chevron_down", 14, 14), self._expand, {}), ("command", "Collapse all", get_icon_image("chevron_up", 14, 14), self._collapse, {})) def show_empty(self, text): if text: self._empty_label.lift() self._empty_label.place(x=0, y=0, relwidth=1, relheight=1) self._empty_label['text'] = text else: self._empty_label.place_forget() def _expand(self): self._tree.expand_all() self._toggle_btn.config(image=get_icon_image("chevron_up", 15, 15)) self._expanded = True def _collapse(self): self._tree.collapse_all() self._toggle_btn.config(image=get_icon_image("chevron_down", 15, 15)) self._expanded = False def _toggle(self, *_): if self._expanded: self._collapse() else: self._expand() def on_widget_add(self, widget: PseudoWidget, parent=None): if parent is None: node = self._tree.add_as_node(widget=widget) else: parent = parent.node node = parent.add_as_node(widget=widget) # let the designer render the menu for us MenuUtils.bind_all_context( node, lambda e: self.studio.designer.show_menu(e, widget) if self.studio.designer else None) def _trigger_select(self): if self._selected and self._selected.widget == self._tree.get().widget: return self.studio.select(self._tree.get().widget, self) self._selected = self._tree.get() def select(self, widget): if widget: node = widget.node self._selected = node node.select( None, True ) # Select node silently to avoid triggering a duplicate selection event elif widget is None: if self._selected: self._selected.deselect() self._selected = None def on_select(self, widget): self.select(widget) def on_widget_delete(self, widget, silently=False): widget.node.remove() def on_widget_restore(self, widget): widget.layout.node.add(widget.node) def on_widget_layout_change(self, widget): node = widget.node if widget.layout == self.studio.designer: parent = self._tree else: parent = widget.layout.node if node.parent_node != parent: parent.insert(None, node) def on_context_close(self, context): if hasattr(context, "designer"): # delete context's tree if hasattr(context.designer, "node") and context.designer.node: context.designer.node.destroy() def on_session_clear(self): self._tree.clear() def on_widget_change(self, old_widget, new_widget=None): new_widget = new_widget if new_widget else old_widget new_widget.node.widget_modified(new_widget) def on_search_query(self, query: str): self._tree.search(query) def on_search_clear(self): self._tree.search("") super(ComponentTree, self).on_search_clear()
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))