def render(self, _): self.detail = Label(self, **self.style.text, text=self.message) self.detail.pack(fill="x") warn_frame = Frame(self, **self.style.surface) self._warning = Label( warn_frame, **self.style.text_passive, padx=5, anchor='w', compound="left", image=get_tk_image("dialog_warning", 15, 15), ) self.event_pad = Label( self, **self.style.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) # for some reason alt needs to be bound separately self.event_pad.bind("<Alt-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)
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)
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 __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()
class Pane(Frame): def __init__(self, master, **cnf): super(Pane, self).__init__(master, **cnf) self.config(**self.style.surface) self._header = Frame(self, **self.style.surface, **self.style.highlight_dim, height=30) self._header.pack(side="top", fill="x") self._header.pack_propagate(0) self._search_bar = SearchBar(self._header, height=20) self._search_bar.on_query_clear(self.on_search_clear) self._search_bar.on_query_change(self.on_search_query) def on_search_query(self, query: str): """ Called when inbuilt search feature is queried. Use the query string to display the necessary search results :param query: String of current search query :return: None """ pass def on_search_clear(self): """ Called when the user terminates the search bar. Ensure you make a call to the super method for the bar to actually get closed. This method can be used to restore the feature state to when not performing a search :return: """ self.quit_search() pass def start_search(self, *_): self._search_bar.place(relwidth=1, relheight=1) self._search_bar.lift() self._search_bar.focus_set() def quit_search(self, *_): self._search_bar.place_forget()
def __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"))
class MessageDialog(Window): """ Main class for creation of hoverset themed alert dialogs. It has the various initialization methods for the common dialogs. The supported forms/types are shown below: .. _forms: * OKAY_CANCEL :py:meth:`MessageDialog.ask_okay_cancel` * YES_NO :py:meth:`MessageDialog.ask_question` * RETRY_CANCEL :py:meth:`MessageDialog.ask_retry_cancel` * SHOW_PROGRESS :py:meth:`MessageDialog.show_progress` * SHOW_WARNING :py:meth:`MessageDialog.show_warning` * SHOW_ERROR :py:meth:`MessageDialog.show_error` * SHOW_INFO :py:meth:`MessageDialog.show_info` There are three icons used in the dialogs: .. _icons: * :py:attr:`MessageDialog.ICON_ERROR` * :py:attr:`MessageDialog.ICON_INFO` * :py:attr:`MessageDialog.ICON_WARNING` Some dialogs return values while others just notify. Below illustrates using the various dialogs .. code-block:: python # assuming we have a hoverset Application object app # program will wait for value to be obtained val = MessageDialog.ask_retry_cancel( title="ask_okay", message="This is an ask-okay-cancel message", parent=app ) # do whatever you need with the value, in this case let's just print the response if val == True: print("retry") elif val == False: print("cancel") elif val is None: print("no value selected") # show dialogs don't need to return values MessageDialog.show_error( title="Error", message="This is an error message", parent=app ) val = MessageDialog.builder( # define the buttons {"text": "Continue", "value": "continue", "focus": True}, {"text": "Pause", "value": "pause"}, {"text": "Cancel", "value": False}, wait=True, title="Builder", message="We just built this dialog from scratch", parent=app, icon="flame" ) print(val) # prints 'continue', 'pause' or False depending on the value of button clicked """ ICON_ERROR = "dialog_error" ICON_INFO = "dialog_info" ICON_WARNING = "dialog_warning" OKAY_CANCEL = "OKAY_CANCEL" YES_NO = "YES_NO" RETRY_CANCEL = "RETRY_CANCEL" SHOW_ERROR = "SHOW_ERROR" SHOW_WARNING = "SHOW_WARNING" SHOW_INFO = "SHOW_INFO" SHOW_PROGRESS = "SHOW_PROGRESS" BUILDER = "BUILDER" INDETERMINATE = ProgressBar.INDETERMINATE DETERMINATE = ProgressBar.DETERMINATE _MIN_BUTTON_WIDTH = 60 _MAX_SHAKES = 10 def __init__(self, master, render_routine=None, **kw): super().__init__(master) self.configure(**self.style.dark) # ensure the dialog is above the parent window at all times self.transient(master) # take the screen focus self.grab_set() # prevent resizing by default self.resizable(False, False) self.bar = None # Common dialogs routines = { "OKAY_CANCEL": self._ask_okay_cancel, "YES_NO": self._ask_yes_no, "RETRY_CANCEL": self._ask_retry_cancel, "SHOW_ERROR": self._show_error, "SHOW_WARNING": self._show_warning, "SHOW_INFO": self._show_info, "SHOW_PROGRESS": self._show_progress, "BUILDER": self._builder # Allows building custom dialogs } if render_routine in routines: # Completely custom dialogs routines[render_routine](**kw) # noqa elif render_routine is not None: render_routine(self) self.enable_centering() self.value = None self.bind('<Visibility>', lambda _: self.grab_set()) self.bind('<FocusOut>', lambda _: self.grab_release()) self.bind('<FocusIn>', lambda _: self.grab_set()) self._reaction = -1 self.bind('<Button>', self._on_event) def _react(self, step): """ Shake the dialog window to gain user attention :param step: displacement in pixels, use negative values for left displacement and positive for right displacement """ # a value of -1 in self._reaction is a sentinel indicating no reaction is ongoing if self._reaction > MessageDialog._MAX_SHAKES: self._reaction = -1 return try: x, y = self.winfo_x() + step, self.winfo_y() self.geometry('+{}+{}'.format(x, y)) self.after(100, lambda: self._react(step * -1)) self._reaction += 1 except Exception: self._reaction = -1 pass def _on_event(self, event): if not self.event_in(event, self): # the user tried to click outside the focused dialog window # we'll try to bring their attention back to the dialog self.bell() if self._reaction < 0: # displace by 2 pixels on each shake # use larger value for more pronounced displacement self._react(2) def _make_button_bar(self): self.bar = Frame(self, **self.style.dark, **self.style.dark_highlight_dim) self.bar.pack(side="bottom", fill="x") def _add_button(self, **kw): text = kw.get("text") focus = kw.get("focus", False) # If a button bar does not already exist we need to create one if self.bar is None: self._make_button_bar() btn = Button(self.bar, **self.style.dark_button, text=text, height=25) btn.configure(**self.style.dark_highlight_active) btn.pack(side="right", padx=5, pady=5) # ensure the buttons have a minimum width of _MIN_BUTTON_WIDTH btn.configure( width=max(self._MIN_BUTTON_WIDTH, btn.measure_text(text))) btn.on_click( kw.get("command", lambda _: self._terminate_with_val(kw.get("value")))) if focus: btn.focus_set() btn.config_all(**self.style.button_highlight) return btn def _message(self, text, icon=None): # set default icon to INFO if icon is None: icon = self.ICON_INFO Label(self, **self.style.dark_text, text=text, anchor="w", compound="left", wrap=600, justify="left", pady=5, padx=15, image=get_icon_image(icon, 50, 50)).pack(side="top", fill="x") def _ask_okay_cancel(self, **kw): self.title(kw.get("title", self.title())) self._message(kw.get("message"), kw.get("icon", self.ICON_INFO)) self._add_button(text="Cancel", focus=True, command=lambda _: self._terminate_with_val(False)) self._add_button(text="Ok", command=lambda _: self._terminate_with_val(True)) def _ask_yes_no(self, **kw): self.title(kw.get("title", self.title())) self._message(kw.get("message"), kw.get("icon", self.ICON_INFO)) self._add_button(text="No", focus=True, command=lambda _: self._terminate_with_val(False)) self._add_button(text="Yes", command=lambda _: self._terminate_with_val(True)) def _ask_retry_cancel(self, **kw): self.title(kw.get("title", self.title())) self._message(kw.get("message"), kw.get("icon", self.ICON_WARNING)) self._add_button(text="Cancel", command=lambda _: self._terminate_with_val(False)) self._add_button(text="Retry", focus=True, command=lambda _: self._terminate_with_val(True)) def _show_error(self, **kw): self.title(kw.get("title", self.title())) self._message(kw.get("message"), kw.get("icon", self.ICON_ERROR)) self._add_button(text="Ok", focus=True, command=lambda _: self.destroy()) def _show_warning(self, **kw): self.title(kw.get("title", self.title())) self._message(kw.get("message"), kw.get("icon", self.ICON_WARNING)) self._add_button(text="Ok", focus=True, command=lambda _: self.destroy()) def _show_info(self, **kw): self.title(kw.get("title", self.title())) self._message(kw.get("message"), kw.get("icon", self.ICON_INFO)) self._add_button(text="Ok", focus=True, command=lambda _: self.destroy()) def _show_progress(self, **kw): self.title(kw.get("title", self.title())) text = kw.get('message', 'progress') icon = None if kw.get('icon'): icon = get_icon_image(kw.get('icon'), 50, 50) Label(self, **self.style.dark_text, text=text, anchor="w", compound="left", wrap=600, justify="left", pady=5, padx=15, image=icon).pack(side="top", fill="x") self.progress = ProgressBar(self) self.progress.pack(side='top', fill='x', padx=20, pady=20) self.progress.mode(kw.get('mode', ProgressBar.DETERMINATE)) self.progress.color( kw.get('colors', self.style.colors.get('accent', 'white'))) self.progress.interval(kw.get('interval', ProgressBar.DEFAULT_INTERVAL)) def _terminate_with_val(self, value): self.value = value self.destroy() def _builder(self, **kw): self.title(kw.get("title", self.title())) self._message(kw.get("message"), kw.get("icon", self.ICON_WARNING)) actions = kw.get("actions") for action in actions: self._add_button(**action) @classmethod def ask(cls, form, **kw): """ General method for creation common dialogs. You do not have to use this method directly since there are specialized methods as shown below. .. table:: :align: center =================================================== ================================== instead of use this =================================================== ================================== MessageDialog.ask(MessageDialog.OKAY_CANCEL, ...) MessageDialog.ask_okay_cancel(...) MessageDialog.ask(MessageDialog.RETRY_CANCEL, ...) MessageDialog.ask_retry_cancel(...) MessageDialog.ask(MessageDialog.SHOW_INFO, ...) MessageDialog.show_info(...) MessageDialog.ask(MessageDialog.SHOW_ERROR, ...) MessageDialog.show_error(...) MessageDialog.ask(MessageDialog.SHOW_PROGRESS, ...) MessageDialog.show_progress(...) MessageDialog.ask(MessageDialog.SHOW_WARNING, ...) MessageDialog.show_warning(...) MessageDialog.ask(MessageDialog.YES_NO, ...) MessageDialog.ask_question(...) =================================================== ================================== :param form: The type of dialog to be created as defined in forms_ above :param kw: The keywords arguments included. These are the common arguments: .. _common_args: * **parent**: (Required) A hoverset based toplevel widget such as :class:`hoverset.ui.widgets.Application` * **title**: Title to be used for the dialog window * **message**: Message to be displayed in the alert dialog * **icon**: Icon to be displayed in the dialog. Should be one of these icons_. .. warning:: The ``parent`` argument is mandatory and should always be provided. Absence of the parent argument will result in missing theme style definitions and cause errors. :return: A value or ``None`` depending on the dialog type. See specialized method for mor details """ parent = kw.get("parent") dialog = MessageDialog(parent, form, **kw) dialog.wait_window() return dialog.value @classmethod def ask_okay_cancel(cls, **kw): """ Show a dialog windows with two buttons: ``okay`` and ``cancel`` :param kw: Keyword arguments as defined in common_args_. :return: Returns ``True`` if "okay" is selected, ``False`` if "cancel" is selected or ``None`` if no choice is selected """ return cls.ask(MessageDialog.OKAY_CANCEL, **kw) @classmethod def ask_question(cls, **kw): """ Show a dialog window with two button: ``yes`` and ``no`` :param kw: Keyword arguments as defined in common_args_. :return: Returns ``True`` if "yes" is selected, ``False`` if "no" is selected or ``None`` if no choice is selected """ return cls.ask(MessageDialog.YES_NO, **kw) @classmethod def ask_retry_cancel(cls, **kw): """ Show a dialog window with two buttons: ``retry`` and ``cancel`` :param kw: Keyword arguments as defined in common_args_. :return: Returns ``True`` if "retry" is selected, ``False`` if "cancel" is selected or ``None`` if no choice is selected """ return cls.ask(MessageDialog.RETRY_CANCEL, **kw) @classmethod def show_error(cls, **kw): """ Show an error message with an error icon. :param kw: Keyword arguments as defined in common_args_. :return: ``None`` """ parent = kw.get("parent") cls(parent, MessageDialog.SHOW_ERROR, **kw) @classmethod def show_warning(cls, **kw): """ Show an warning message with a warning icon. :param kw: Keyword arguments as defined in common_args_. :return: ``None`` """ parent = kw.get("parent") cls(parent, MessageDialog.SHOW_WARNING, **kw) @classmethod def show_info(cls, **kw): """ Show an info message with an info icon. :param kw: Keyword arguments as defined in common_args_. :return: ``None`` """ parent = kw.get("parent") cls(parent, MessageDialog.SHOW_INFO, **kw) @classmethod def show_progress(cls, **kw): """ :param kw: Config options for the progress dialog * **parent**: A hoverset based toplevel widget such as :class:`hoverset.ui.widgets.Application` * **title**: Title to be used for the dialog window * **message**: Message to be displayed in the alert dialog * **icon**: Icon to be displayed in the dialog. Should be one of these icons_. * **mode**: One of the two modes * :py:attr:`MessageDialog.DETERMINATE` * :py:attr:`MessageDialog.INDETERMINATE` * **color**: Color to be used for the progressbar * **interval**: The update interval in :py:attr:`MessageDialog.INDETERMINATE` in milliseconds. The smaller the interval the faster the animation. :return: The dialog window. The underlying progressbar can then be accessed through the property :py:attr:`MessageDialog.progress`. The progressbar is a :class:`hoverset.ui.widgets.ProgressBar` object and can be updated as required """ parent = kw.get("parent") dialog = cls(parent, MessageDialog.SHOW_PROGRESS, **kw) return dialog @classmethod def builder(cls, *buttons, **kw): """ Create custom dialogs with custom buttons, icons and return values. An example of the use of a builder has been provided at the beginning of this page. :param buttons: A tuple containing a dictionary defining the custom buttons with the following keys * **text**: Text to be displayed in the button * **value**: Value returned when button is clicked * **focus**: Whether to focus on button when dialog is displayed. Only a single button should have ``focus`` set to ``True`` :param kw: config options for the builder: * **parent**: A hoverset based toplevel widget such as :class:`hoverset.ui.widgets.Application` * **title**: Title to be used for the dialog window * **message**: Message to be displayed in the alert dialog * **icon**: Icon to be displayed in the dialog. Should be one of these icons_. * **wait**: Set to ``True`` to suspend the program and wait a value to be returned or ``False`` to just continue program execution without waiting for a value. Useful when you just need to display a message :return: A custom value depending on the custom button clicked. If ``wait = False`` no value is returned. If no value is selected by the user ``None`` is returned """ parent = kw.get("parent") kw["actions"] = buttons dialog = cls(parent, MessageDialog.BUILDER, **kw) if kw.get("wait", False): dialog.wait_window() return dialog.value
def __init__(self, master=None, **cnf): super().__init__(master, **cnf) # Load icon asynchronously to prevent issues which have been known to occur when loading it synchronously icon_image = load_tk_image(self.ICON_PATH) self.iconphoto(True, icon_image) self.pref = pref self._restore_position() self.title('Formation Studio') self.protocol('WM_DELETE_WINDOW', self._on_close) self.shortcuts = ShortcutManager(self, pref) self.shortcuts.bind_all() self._register_actions() self._toolbar = Frame(self, **self.style.dark, height=30) self._toolbar.pack(side="top", fill="x") self._toolbar.pack_propagate(0) self._statusbar = Frame(self, **self.style.dark, height=20) self._statusbar.pack(side="bottom", fill="x") self._statusbar.pack_propagate(0) body = Frame(self, **self.style.dark) body.pack(fill="both", expand=True, side="top") self._right_bar = SideBar(body) self._right_bar.pack(side="right", fill="y") self._left_bar = SideBar(body) self._left_bar.pack(side="left", fill="y") self._pane = PanedWindow(body, **self.style.dark_pane_horizontal) self._pane.pack(side="left", fill="both", expand=True) self._left = PanedWindow(self._pane, **self.style.dark_pane_vertical) self._center = PanedWindow(self._pane, **self.style.dark_pane_vertical) self._right = PanedWindow(self._pane, **self.style.dark_pane_vertical) self._bin = [] self._clipboard = None self._undo_stack = [] self._redo_stack = [] self.current_preview = None self._pane.add(self._left, minsize=320, sticky='nswe', width=320) self._pane.add(self._center, minsize=400, width=16000, sticky='nswe') self._pane.add(self._right, minsize=320, sticky='nswe', width=320) self._panes = { "left": (self._left, self._left_bar), "right": (self._right, self._right_bar), "center": (self._center, None) } icon = get_icon_image self.actions = ( ("Delete", icon("delete", 20, 20), lambda e: self.delete(), "Delete selected widget"), ("Undo", icon("undo", 20, 20), lambda e: self.undo(), "Undo action"), ("Redo", icon("redo", 20, 20), lambda e: self.redo(), "Redo action"), ("Cut", icon("cut", 20, 20), lambda e: self.cut(), "Cut selected widget"), ("separator", ), ("Fullscreen", icon("image_editor", 20, 20), lambda e: self.close_all(), "Design mode"), ("Separate", icon("separate", 20, 20), lambda e: self.features_as_windows(), "Open features in window mode"), ("Dock", icon("flip_horizontal", 15, 15), lambda e: self.features_as_docked(), "Dock all features"), ("separator", ), ("New", icon("add", 20, 20), lambda e: self.open_new(), "New design"), ("Save", icon("save", 20, 20), lambda e: self.save(), "Save design"), ("Preview", icon("play", 20, 20), lambda e: self.preview(), "Preview design"), ) self.init_toolbar() self.selected = None # set the image option to blank if there is no image for the menu option self.blank_img = blank_img = icon("blank", 14, 14) # -------------------------------------------- menu definition ------------------------------------------------ self.menu_template = (EnableIf( lambda: self.selected, ("separator", ), ("command", "copy", icon("copy", 14, 14), actions.get('STUDIO_COPY'), {}), ("command", "paste", icon("clipboard", 14, 14), actions.get('STUDIO_PASTE'), {}), ("command", "cut", icon("cut", 14, 14), actions.get('STUDIO_CUT'), {}), ("separator", ), ("command", "delete", icon("delete", 14, 14), actions.get('STUDIO_DELETE'), {}), ), ) self.menu_bar = MenuUtils.make_dynamic( (("cascade", "File", None, None, { "menu": ( ("command", "New", icon( "add", 14, 14), actions.get('STUDIO_NEW'), {}), ("command", "Open", icon( "folder", 14, 14), actions.get('STUDIO_OPEN'), {}), ("cascade", "Recent", icon("clock", 14, 14), None, { "menu": self._create_recent_menu() }), ("separator", ), ("command", "Save", icon( "save", 14, 14), actions.get('STUDIO_SAVE'), {}), ("command", "Save As", icon( "save", 14, 14), actions.get('STUDIO_SAVE_AS'), {}), ("separator", ), ("command", "Settings", icon("settings", 14, 14), actions.get('STUDIO_SETTINGS'), {}), ("command", "Exit", icon( "exit", 14, 14), actions.get('STUDIO_EXIT'), {}), ) }), ("cascade", "Edit", None, None, { "menu": ( EnableIf(lambda: len(self._undo_stack), ("command", "undo", icon("undo", 14, 14), actions.get('STUDIO_UNDO'), {})), EnableIf(lambda: len(self._redo_stack), ("command", "redo", icon("redo", 14, 14), actions.get('STUDIO_REDO'), {})), *self.menu_template, ) }), ("cascade", "Code", None, None, { "menu": (EnableIf(lambda: self.designer and self.designer.root_obj, ("command", "Preview design", icon("play", 14, 14), actions.get('STUDIO_PREVIEW'), {}), ("command", "close preview", icon("close", 14, 14), actions.get('STUDIO_PREVIEW_CLOSE'), {}))) }), ("cascade", "Window", None, None, { "menu": (("command", "show all", blank_img, actions.get('FEATURE_SHOW_ALL'), {}), ("command", "close all", icon( "close", 14, 14), actions.get('FEATURE_CLOSE_ALL'), {}), ("command", "close all on the right", blank_img, actions.get('FEATURE_CLOSE_RIGHT'), {}), ("command", "close all on the left", blank_img, actions.get('FEATURE_CLOSE_LEFT'), {}), ("separator", ), ("command", "Undock all windows", blank_img, actions.get('FEATURE_UNDOCK_ALL'), {}), ("command", "Dock all windows", blank_img, actions.get('FEATURE_DOCK_ALL'), {}), ("separator", ), LoadLater(self.get_features_as_menu), ("separator", ), ("command", "Save window positions", blank_img, actions.get('FEATURE_SAVE_POS'), {})) }), ("cascade", "Tools", None, None, { "menu": ToolManager.get_tools_as_menu(self) }), ("cascade", "Help", None, None, { "menu": ( ("command", "Help", icon('dialog_info', 14, 14), actions.get('STUDIO_HELP'), {}), ("command", "Check for updates", icon("cloud", 14, 14), None, {}), ("separator", ), ("command", "About Studio", blank_img, lambda: about_window(self), {}), ) })), self, self.style, False) self.config(menu=self.menu_bar) self.features = [] self.designer = Designer(self._center, self) self._center.add(self.designer, sticky='nswe') self.install(ComponentPane) self.install(ComponentTree) self.install(StylePane) self.install(VariablePane) self._startup() self._restore_position()
class StudioApplication(Application): ICON_PATH = get_resource_path(studio, "resources/images/formation_icon.png") def __init__(self, master=None, **cnf): super().__init__(master, **cnf) # Load icon asynchronously to prevent issues which have been known to occur when loading it synchronously icon_image = load_tk_image(self.ICON_PATH) self.iconphoto(True, icon_image) self.pref = pref self._restore_position() self.title('Formation Studio') self.protocol('WM_DELETE_WINDOW', self._on_close) self.shortcuts = ShortcutManager(self, pref) self.shortcuts.bind_all() self._register_actions() self._toolbar = Frame(self, **self.style.dark, height=30) self._toolbar.pack(side="top", fill="x") self._toolbar.pack_propagate(0) self._statusbar = Frame(self, **self.style.dark, height=20) self._statusbar.pack(side="bottom", fill="x") self._statusbar.pack_propagate(0) body = Frame(self, **self.style.dark) body.pack(fill="both", expand=True, side="top") self._right_bar = SideBar(body) self._right_bar.pack(side="right", fill="y") self._left_bar = SideBar(body) self._left_bar.pack(side="left", fill="y") self._pane = PanedWindow(body, **self.style.dark_pane_horizontal) self._pane.pack(side="left", fill="both", expand=True) self._left = PanedWindow(self._pane, **self.style.dark_pane_vertical) self._center = PanedWindow(self._pane, **self.style.dark_pane_vertical) self._right = PanedWindow(self._pane, **self.style.dark_pane_vertical) self._bin = [] self._clipboard = None self._undo_stack = [] self._redo_stack = [] self.current_preview = None self._pane.add(self._left, minsize=320, sticky='nswe', width=320) self._pane.add(self._center, minsize=400, width=16000, sticky='nswe') self._pane.add(self._right, minsize=320, sticky='nswe', width=320) self._panes = { "left": (self._left, self._left_bar), "right": (self._right, self._right_bar), "center": (self._center, None) } icon = get_icon_image self.actions = ( ("Delete", icon("delete", 20, 20), lambda e: self.delete(), "Delete selected widget"), ("Undo", icon("undo", 20, 20), lambda e: self.undo(), "Undo action"), ("Redo", icon("redo", 20, 20), lambda e: self.redo(), "Redo action"), ("Cut", icon("cut", 20, 20), lambda e: self.cut(), "Cut selected widget"), ("separator", ), ("Fullscreen", icon("image_editor", 20, 20), lambda e: self.close_all(), "Design mode"), ("Separate", icon("separate", 20, 20), lambda e: self.features_as_windows(), "Open features in window mode"), ("Dock", icon("flip_horizontal", 15, 15), lambda e: self.features_as_docked(), "Dock all features"), ("separator", ), ("New", icon("add", 20, 20), lambda e: self.open_new(), "New design"), ("Save", icon("save", 20, 20), lambda e: self.save(), "Save design"), ("Preview", icon("play", 20, 20), lambda e: self.preview(), "Preview design"), ) self.init_toolbar() self.selected = None # set the image option to blank if there is no image for the menu option self.blank_img = blank_img = icon("blank", 14, 14) # -------------------------------------------- menu definition ------------------------------------------------ self.menu_template = (EnableIf( lambda: self.selected, ("separator", ), ("command", "copy", icon("copy", 14, 14), actions.get('STUDIO_COPY'), {}), ("command", "paste", icon("clipboard", 14, 14), actions.get('STUDIO_PASTE'), {}), ("command", "cut", icon("cut", 14, 14), actions.get('STUDIO_CUT'), {}), ("separator", ), ("command", "delete", icon("delete", 14, 14), actions.get('STUDIO_DELETE'), {}), ), ) self.menu_bar = MenuUtils.make_dynamic( (("cascade", "File", None, None, { "menu": ( ("command", "New", icon( "add", 14, 14), actions.get('STUDIO_NEW'), {}), ("command", "Open", icon( "folder", 14, 14), actions.get('STUDIO_OPEN'), {}), ("cascade", "Recent", icon("clock", 14, 14), None, { "menu": self._create_recent_menu() }), ("separator", ), ("command", "Save", icon( "save", 14, 14), actions.get('STUDIO_SAVE'), {}), ("command", "Save As", icon( "save", 14, 14), actions.get('STUDIO_SAVE_AS'), {}), ("separator", ), ("command", "Settings", icon("settings", 14, 14), actions.get('STUDIO_SETTINGS'), {}), ("command", "Exit", icon( "exit", 14, 14), actions.get('STUDIO_EXIT'), {}), ) }), ("cascade", "Edit", None, None, { "menu": ( EnableIf(lambda: len(self._undo_stack), ("command", "undo", icon("undo", 14, 14), actions.get('STUDIO_UNDO'), {})), EnableIf(lambda: len(self._redo_stack), ("command", "redo", icon("redo", 14, 14), actions.get('STUDIO_REDO'), {})), *self.menu_template, ) }), ("cascade", "Code", None, None, { "menu": (EnableIf(lambda: self.designer and self.designer.root_obj, ("command", "Preview design", icon("play", 14, 14), actions.get('STUDIO_PREVIEW'), {}), ("command", "close preview", icon("close", 14, 14), actions.get('STUDIO_PREVIEW_CLOSE'), {}))) }), ("cascade", "Window", None, None, { "menu": (("command", "show all", blank_img, actions.get('FEATURE_SHOW_ALL'), {}), ("command", "close all", icon( "close", 14, 14), actions.get('FEATURE_CLOSE_ALL'), {}), ("command", "close all on the right", blank_img, actions.get('FEATURE_CLOSE_RIGHT'), {}), ("command", "close all on the left", blank_img, actions.get('FEATURE_CLOSE_LEFT'), {}), ("separator", ), ("command", "Undock all windows", blank_img, actions.get('FEATURE_UNDOCK_ALL'), {}), ("command", "Dock all windows", blank_img, actions.get('FEATURE_DOCK_ALL'), {}), ("separator", ), LoadLater(self.get_features_as_menu), ("separator", ), ("command", "Save window positions", blank_img, actions.get('FEATURE_SAVE_POS'), {})) }), ("cascade", "Tools", None, None, { "menu": ToolManager.get_tools_as_menu(self) }), ("cascade", "Help", None, None, { "menu": ( ("command", "Help", icon('dialog_info', 14, 14), actions.get('STUDIO_HELP'), {}), ("command", "Check for updates", icon("cloud", 14, 14), None, {}), ("separator", ), ("command", "About Studio", blank_img, lambda: about_window(self), {}), ) })), self, self.style, False) self.config(menu=self.menu_bar) self.features = [] self.designer = Designer(self._center, self) self._center.add(self.designer, sticky='nswe') self.install(ComponentPane) self.install(ComponentTree) self.install(StylePane) self.install(VariablePane) self._startup() self._restore_position() def _startup(self): on_startup = pref.get("studio::on_startup") if on_startup == "new": self.open_new() elif on_startup == "recent": latest = pref.get_latest() if latest: self.open_file(latest) # if blank do nothing def _save_position(self): # self.update_idletasks() pref.set( "studio::pos", dict( width=self.width, height=self.height, x=self.winfo_x(), y=self.winfo_y(), state=self.state(), # window state either zoomed or normal )) def _restore_position(self): pos = pref.get("studio::pos") if pos.get("state") == 'zoomed': if platform_is(WINDOWS): self.state('zoomed') else: self.wm_attributes('-zoomed', True) return self.state('normal') self.geometry('{width}x{height}+{x}+{y}'.format(**pos)) def new_action(self, action: Action): """ Register a undo redo point :param action: An action object implementing undo and redo methods :return: """ self._undo_stack.append(action) self._redo_stack.clear() def undo(self): if not len(self._undo_stack): # Let's avoid popping an empty list to prevent raising IndexError return action = self._undo_stack.pop() action.undo() self._redo_stack.append(action) def redo(self): if not len(self._redo_stack): return action = self._redo_stack.pop() action.redo() self._undo_stack.append(action) def copy(self): if self.selected: # store the current object as an xml node in the clipboard self._clipboard = self.designer.as_xml_node(self.selected) def install_status_widget(self, widget_class, *args, **kwargs): widget = widget_class(self._statusbar, *args, **kwargs) widget.pack(side='right', padx=2, fill='y') return widget def get_pane_info(self, pane): return self._panes.get(pane, [self._right, self._right_bar]) def paste(self): if self._clipboard is not None: self.designer.paste(self._clipboard) def close_all_on_side(self, side): for feature in self.features: if feature.side == side: feature.minimize() # To avoid errors when side is not a valid pane identifier we default to the right pane self._panes.get(side, (self._right, self._right_bar))[1].close_all() def close_all(self, *_): for feature in self.features: feature.minimize() self._right_bar.close_all() self._left_bar.close_all() def init_toolbar(self): for action in self.actions: if len(action) == 1: Frame(self._toolbar, width=1, bg=self.style.colors.get("primarydarkaccent")).pack( side='left', fill='y', pady=3, padx=5) continue btn = Button(self._toolbar, image=action[1], **self.style.dark_button, width=25, height=25) btn.pack(side="left", padx=3) btn.tooltip(action[3]) ActionNotifier.bind_event("<Button-1>", btn, action[2], text=action[3]) def uninstall(self, feature): self.features.remove(feature) feature.bar.remove(feature) feature.pane.forget(feature) self._adjust_pane(feature.pane) def get_pane_bar(self, side): if side in self._panes: return self._panes.get(side, (self._left, self._left_bar)) def reposition(self, feature: BaseFeature, side): if self.get_pane_bar(side): pane, bar = self.get_pane_bar(side) feature.bar.remove(feature) feature.pane.forget(feature) self._adjust_pane(feature.pane) feature.bar = bar feature.pane = pane bar.add_feature(feature) if feature.get_pref("mode") == "docked": pane.add(feature, minsize=100, height=300, sticky='nswe') feature.set_pref("side", side) def install(self, feature) -> BaseFeature: obj = feature(self, self) pane, bar = self._panes.get(obj.get_pref('side'), (self._left, self._left_bar)) obj.pane = pane obj.bar = bar self.features.append(obj) if bar is not None: bar.add_feature(obj) if not obj.get_pref('visible'): bar.deselect(obj) self._adjust_pane(pane) else: bar.select(obj) obj.maximize() return obj def show_all_windows(self): for feature in self.features: feature.maximize() def features_as_windows(self): for feature in self.features: feature.open_as_window() def features_as_docked(self): for feature in self.features: feature.open_as_docked() def set_path(self, path): if path: self.title("Formation studio" + " - " + path) @dynamic_menu def _create_recent_menu(self, menu): # Dynamically create recent file menu every time menu is posted menu.image = get_icon_image("close", 14, 14) menu.config(**self.style.dark_context_menu) recent = pref.get_recent() for path, label in recent: menu.add_command( label=label, command=functools.partial(self.open_recent, path), image=self.blank_img, compound='left', ) menu.add_command(label="Clear", image=menu.image, command=pref.clear_recent, compound="left") def open_file(self, path=None): if path is None: path = filedialog.askopenfilename(parent=self, filetypes=[('XML', '*.xml')]) elif not os.path.exists(path): MessageDialog.show_error( parent=self, title="Missing File", message="File {} does not exist".format(path), ) return if path: self.designer.open_xml(path) self.set_path(path) pref.update_recent(path) def open_recent(self, path): self.open_file(path) def open_new(self): self.designer.open_new() self.set_path('untitled') def save(self): path = self.designer.save() self.set_path(path) pref.update_recent(path) def save_as(self): path = self.designer.save(new_path=True) self.set_path(path) pref.update_recent(path) def get_feature(self, feature_class) -> BaseFeature: for feature in self.features: if feature.__class__ == feature_class: return feature # returns None by if feature is not found def get_features_as_menu(self): # For each feature we create a menu template # The command value is the self.maximize method which will reopen the feature return [ ( "command", # Type f.name, get_icon_image(f.icon, 14, 14), # Label, image functools.partial(f.toggle), # Command built from feature {}) for f in self.features ] def save_window_positions(self): for feature in self.features: feature.save_window_pos() self._save_position() def _adjust_pane(self, pane): if len(pane.panes()) == 0: self._pane.paneconfig(pane, minsize=0, width=0) self._pane.paneconfig(self._center, width=16000) else: self._pane.paneconfig(pane, minsize=320) def minimize(self, feature): feature.pane.forget(feature) feature.bar.deselect(feature) self._adjust_pane(feature.pane) def maximize(self, feature): feature.pane.add(feature, height=300, sticky='nswe') feature.bar.select(feature) self._adjust_pane(feature.pane) def select(self, widget, source=None): self.selected = widget if source != self.designer: # Select from the designer explicitly so the selection does not end up being re-fired self.designer.select(widget, True) for feature in self.features: if feature != source: feature.on_select(widget) def add(self, widget, parent=None): for feature in self.features: feature.on_widget_add(widget, parent) def widget_modified(self, widget1, source=None, widget2=None): for feature in self._all_features(): if feature != source: feature.on_widget_change(widget1, widget2) def widget_layout_changed(self, widget): for feature in self.features: feature.on_widget_layout_change(widget) def delete(self, widget=None, source=None): widget = self.selected if widget is None else widget if widget is None: return if self.selected == widget: self.select(None) if source != self.designer: self.designer.delete(widget) for feature in self.features: feature.on_widget_delete(widget) def cut(self, widget=None, source=None): widget = self.selected if widget is None else widget if not widget: return if self.selected == widget: self.select(None) self._clipboard = self.designer.as_xml_node(widget) if source != self.designer: self.designer.delete(widget, True) for feature in self.features: feature.on_widget_delete(widget, True) def on_restore(self, widget): for feature in self.features: feature.on_widget_restore(widget) def on_feature_change(self, new, old): self.features.insert(self.features.index(old), new) self.features.remove(old) def on_session_clear(self, source): self._redo_stack.clear() self._undo_stack.clear() for feature in self._all_features(): if feature != source: feature.on_session_clear() def preview(self): if self.designer.root_obj is None: # If there is no root object show a warning MessageDialog.show_warning( parent=self, title='Empty design', message='There is nothing to preview. Please add a root widget' ) return # close previous preview if any self.close_preview() window = self.current_preview = Toplevel(self) window.wm_transient(self) window.build = AppBuilder(window, node=self.designer.to_xml()) name = self.designer.design_path if self.designer.design_path is not None else "Untitled" window.build._app.title(os.path.basename(name)) def close_preview(self): if self.current_preview: self.current_preview.destroy() def _all_features(self): """ Return a tuple of all features including the designer instance :return: tuple of features """ # We cannot unpack directly at the return statement due to a flaw in python versions < 3.8 features = *self.features, self.designer return features def _on_close(self): self._save_position() # pass the on window close event to the features for feature in self._all_features(): # if any feature returns false abort shut down if not feature.on_app_close(): return self.destroy() def get_help(self): # Entry point for studio help functionality pass def settings(self): open_preferences(self) def _register_actions(self): CTRL, ALT, SHIFT = KeyMap.CONTROL, KeyMap.ALT, KeyMap.SHIFT routine = actions.Routine # These actions are best bound separately to avoid interference with text entry widgets actions.add( routine(self.cut, 'STUDIO_CUT', 'Cut selected widget', 'studio', CTRL + CharKey('x')), routine(self.copy, 'STUDIO_COPY', 'Copy selected widget', 'studio', CTRL + CharKey('c')), routine(self.paste, 'STUDIO_PASTE', 'Paste selected widget', 'studio', CTRL + CharKey('v')), routine(self.delete, 'STUDIO_DELETE', 'Delete selected widget', 'studio', KeyMap.DELETE), ) self.shortcuts.add_routines( routine(self.undo, 'STUDIO_UNDO', 'Undo last action', 'studio', CTRL + CharKey('Z')), routine(self.redo, 'STUDIO_REDO', 'Redo action', 'studio', CTRL + CharKey('Y')), # ----------------------------- routine(self.open_new, 'STUDIO_NEW', 'Open new design', 'studio', CTRL + CharKey('n')), routine(self.open_file, 'STUDIO_OPEN', 'Open design from file', 'studio', CTRL + CharKey('o')), routine(self.save, 'STUDIO_SAVE', 'Save current design', 'studio', CTRL + CharKey('s')), routine(self.save_as, 'STUDIO_SAVE_AS', 'Save current design under a new file', 'studio', CTRL + SHIFT + CharKey('s')), routine(self.get_help, 'STUDIO_HELP', 'Show studio help', 'studio', KeyMap.F(12)), routine(self.settings, 'STUDIO_SETTINGS', 'Open studio settings', 'studio', ALT + CharKey('s')), routine(self._on_close, 'STUDIO_EXIT', 'Exit application', 'studio', CTRL + CharKey('q')), # ------------------------------ routine(self.show_all_windows, 'FEATURE_SHOW_ALL', 'Show all feature windows', 'studio', ALT + CharKey('a')), routine(self.close_all, 'FEATURE_CLOSE_ALL', 'Close all feature windows', 'studio', ALT + CharKey('x')), routine(lambda: self.close_all_on_side('right'), 'FEATURE_CLOSE_RIGHT', 'Close feature windows to the right', 'studio', ALT + CharKey('R')), routine(lambda: self.close_all_on_side('left'), 'FEATURE_CLOSE_LEFT', 'Close feature windows to the left', 'studio', ALT + CharKey('L')), routine(self.features_as_docked, 'FEATURE_DOCK_ALL', 'Dock all feature windows', 'studio', ALT + CharKey('d')), routine(self.features_as_windows, 'FEATURE_UNDOCK_ALL', 'Undock all feature windows', 'studio', ALT + CharKey('u')), routine(self.save_window_positions, 'FEATURE_SAVE_POS', 'Save window positions', 'studio', ALT + SHIFT + CharKey('s')), # ----------------------------- routine(self.preview, 'STUDIO_PREVIEW', 'Show preview', 'studio', KeyMap.F(5)), routine(self.close_preview, 'STUDIO_PREVIEW_CLOSE', 'Close any preview', 'studio', ALT + KeyMap.F(5)), )
class BaseFeature(Frame): _instance = None name = "Feature" pane = None bar = None icon = "blank" _view_mode = None _transparency_flag = None _side = None rec = (20, 20, 300, 300) # Default window mode position _defaults = { "mode": "docked", "inactive_transparency": False, "position": "left", "visible": True, "side": "left", "pos": { "initialized": False, "x": 20, "y": 20, "width": 200, "height": 200, } } @classmethod def update_defaults(cls): path = "features::{}".format(cls.name) if not pref.exists(path): pref.set(path, dict(**cls._defaults)) else: pref.update_defaults(path, dict(**cls._defaults)) def __init__(self, master, studio=None, **cnf): super().__init__(master, **cnf) self.update_defaults() self.__class__._instance = self if not self.__class__._view_mode: self.__class__._view_mode = StringVar(None, self.get_pref('mode')) self.__class__._transparency_flag = t = BooleanVar(None, self.get_pref('inactive_transparency')) self.__class__._side = side = StringVar(None, self.get_pref('side')) t.trace_add("write", lambda *_: self.set_pref('inactive_transparency', t.get())) self.studio = studio self._header = Frame(self, **self.style.dark, **self.style.dark_highlight_dim, height=30) self._header.pack(side="top", fill="x") self._header.pack_propagate(0) self._header.allow_drag = True Label(self._header, **self.style.dark_text_passive, text=self.name).pack(side="left") self._min = Button(self._header, image=get_icon_image("close", 15, 15), **self.style.dark_button, width=25, height=25) self._min.pack(side="right") self._min.on_click(self.minimize) self._pref = MenuButton(self._header, **self.style.dark_button) self._pref.configure(image=get_icon_image("settings", 15, 15)) self._pref.pack(side="right") self._pref.tooltip("Options") self._search_bar = SearchBar(self._header, height=20) self._search_bar.on_query_clear(self.on_search_clear) self._search_bar.on_query_change(self.on_search_query) menu = self.make_menu(( ("cascade", "View Mode", None, None, {"menu": ( ("radiobutton", "Docked", None, self.open_as_docked, {"variable": self._view_mode, "value": "docked"}), ("radiobutton", "Window", None, self.open_as_window, {"variable": self._view_mode, "value": "window"}), )}), ("cascade", "Position", None, None, {"menu": ( ("radiobutton", "Left", None, lambda: self.reposition("left"), {"variable": self._side, "value": "left"}), ("radiobutton", "Right", None, lambda: self.reposition("right"), {"variable": self._side, "value": "right"}), )}), EnableIf(lambda: self._view_mode.get() == 'window', ("cascade", "Window options", None, None, {"menu": ( ( "checkbutton", "Transparent when inactive", None, None, {"variable": self._transparency_flag}), )})), ("command", "Close", get_icon_image("close", 14, 14), self.minimize, {}), ("separator",), *self.create_menu() ), self._pref) self._pref.config(menu=menu) # self._pref.on_click(self.minimize) self.config(**self.style.dark) self.indicator = None self.window_handle = None self.on_focus(self._on_focus_get) self.on_focus_lost(self._on_focus_release) self.on_close(self.close_window) self._mode_map = { 'window': self.open_as_window, 'docked': self.open_as_docked } @classmethod def get_pref_path(cls, short_path): return "features::{}::{}".format(cls.name, short_path) @classmethod def get_pref(cls, short_path): return pref.get(cls.get_pref_path(short_path)) @classmethod def set_pref(cls, short_path, value): pref.set(cls.get_pref_path(short_path), value) @classmethod def get_instance(cls): return cls._instance def start_search(self, *_): self._search_bar.place(relwidth=1, relheight=1) self._search_bar.lift() self._search_bar.focus_set() def quit_search(self, *_): self._search_bar.place_forget() def on_search_query(self, query: str): """ Called when inbuilt search feature is queried. Use the query string to display the necessary search results :param query: String of current search query :return: None """ pass def on_search_clear(self): """ Called when the user terminates the search bar. Ensure you make a call to the super method for the bar to actually get closed. This method can be used to restore the feature state to when not performing a search :return: """ self.quit_search() pass def on_select(self, widget): """ Called when a widget is selected in the designer :param widget: selected widget :return:None """ pass def on_widget_change(self, old_widget, new_widget=None): """ Called when a widget is fundamentally altered :param old_widget: Altered widget :param new_widget: The new widget taking the older widgets place :return: None """ pass def on_widget_layout_change(self, widget): """ Called when layout options of a widget are changed :param widget: Widget with altered layout options :return: None """ pass def on_widget_add(self, widget, parent): """ Called when a new widget is added to the designer :param widget: widget :param parent: the container widget to which thw widget is added :return: None """ pass def on_widget_delete(self, widget, silently=False): """ Called when a widget is deleted from the designer :param widget: deleted widget :param silently: flag indicating whether the deletion should be treated implicitly which is useful for instance when you don't want the deletion to be logged in the undo stack :return: None """ pass def on_widget_restore(self, widget): """ Called when a deleted widget is restored :param widget: restored widget :return: None """ pass def on_session_clear(self): """ Override to perform operations before a session is cleared and the studio resets to a new design :return: None """ pass def on_app_close(self) -> bool: """ Override to perform operations before the studio app closes. :return: True to allow shutdown to proceed or False to abort shutdown """ return True def minimize(self, *_): if self.window_handle: self.close_window() return self.studio.minimize(self) self.set_pref("visible", False) def maximize(self): if self.get_pref("mode") == "window": self.open_as_window() self.bar.select(self) else: self.studio.maximize(self) self.set_pref("visible", True) def toggle(self): if self.get_pref("visible"): self.minimize() else: self.maximize() def create_menu(self): """ Override this method to provide additional menu options :return: tuple of menu templates i.e. (type, label, image, callback, **additional_config) """ # return an empty tuple as default return () def _on_focus_release(self): if self._transparency_flag.get() and self.window_handle: if self.window_handle: self.window_handle.wm_attributes('-alpha', 0.3) if self.window_handle: self.save_window_pos() def _on_focus_get(self): if self.window_handle: self.window_handle.wm_attributes('-alpha', 1.0) def open_as_docked(self): self._view_mode.set("docked") self.set_pref('mode', 'docked') if self.window_handle: self.master.window.wm_forget(self) self.window_handle = None self.maximize() def reposition(self, side): self._side.set(side) self.studio.reposition(self, side) def open_as_window(self): if TkVersion < 8.5: logging.error("Window mode is not supported in current tk version") return self.master.window.wm_forget(self) rec = absolute_position(self) if not self.get_pref("pos::initialized") else ( self.get_pref("pos::x"), self.get_pref("pos::y"), self.get_pref("pos::width"), self.get_pref("pos::height"), ) self.window.wm_manage(self) # Allow us to create a hook in the close method of the window manager self.bind_close() self.title(self.name) self.transient(self.master.window) self.geometry('{}x{}+{}+{}'.format(rec[2], rec[3], rec[0], rec[1])) self.update_idletasks() self.window_handle = self self._view_mode.set("window") self.set_pref("mode", "window") self.studio._adjust_pane(self.pane) self.save_window_pos() if self.focus_get() != self and self.get_pref("inactive_transparency"): self.window_handle.wm_attributes('-alpha', 0.3) def save_window_pos(self): if not self.window_handle: return self.set_pref("pos", dict( x=self.winfo_x(), y=self.winfo_y(), width=self.width, height=self.height, initialized=True )) def close_window(self): if self.window_handle: # Store the current position of our window handle to used when it is reopened self.save_window_pos() self.master.window.wm_forget(self) self.window_handle = None self.studio.minimize(self) self.set_pref("visible", False)
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 MenuEditor(BaseToolWindow): # TODO Add context menu for nodes # TODO Add style search # TODO Handle widget change from the studio main control _MESSAGE_EDITOR_EMPTY = "No item selected" def __init__(self, master, widget, menu=None): super().__init__(master, widget) self.title(f'Edit menu for {widget.id}') if not isinstance(menu, tk.Menu): menu = tk.Menu(widget, tearoff=False) widget.configure(menu=menu) self._base_menu = menu self._tool_bar = Frame(self, **self.style.dark, **self.style.dark_highlight_dim, height=30) self._tool_bar.pack(side="top", fill="x") self._tool_bar.pack_propagate(False) self._pane = PanedWindow(self, **self.style.dark_pane_horizontal) self._tree = MenuTree(self._pane, widget, menu) self._tree.allow_multi_select(True) self._tree.on_select(self._refresh_styles) self._tree.on_structure_change(self._refresh_styles) self._editor_pane = ScrolledFrame(self._pane) self._editor_pane_cover = Label(self._editor_pane, **self.style.dark_text_passive) self._editor_pane.pack(side="top", fill="both", expand=True) self._menu_item_styles = CollapseFrame(self._editor_pane.body) self._menu_item_styles.pack(side="top", fill="x", pady=4) self._menu_item_styles.label = "Menu Item attributes" self._menu_styles = CollapseFrame(self._editor_pane.body) self._menu_styles.pack(side="top", fill="x", pady=4) self._menu_styles.label = "Menu attributes" self._style_item_ref = {} self._menu_style_ref = {} self._prev_selection = None self._add = MenuButton(self._tool_bar, **self.style.dark_button) self._add.pack(side="left") self._add.configure(image=get_icon_image("add", 15, 15)) _types = MenuTree.Node._type_def menu_types = self._tool_bar.make_menu( [( tk.COMMAND, i.title(), get_icon_image(_types[i][0], 14, 14), functools.partial(self.add_item, i), {} ) for i in _types], self._add) menu_types.configure(tearoff=True) self._add.config(menu=menu_types) self._delete_btn = Button(self._tool_bar, image=get_icon_image("delete", 15, 15), **self.style.dark_button, width=25, height=25) self._delete_btn.pack(side="left") self._delete_btn.on_click(self._delete) self._preview_btn = Button(self._tool_bar, image=get_icon_image("play", 15, 15), **self.style.dark_button, width=25, height=25) self._preview_btn.pack(side="left") self._preview_btn.on_click(self._preview) self._pane.pack(side="top", fill="both", expand=True) self._pane.add(self._tree, minsize=350, sticky='nswe', width=350, height=500) self._pane.add(self._editor_pane, minsize=320, sticky='nswe', width=320, height=500) self.load_menu(menu, self._tree) self._show_editor_message(self._MESSAGE_EDITOR_EMPTY) self.enable_centering() self.focus_set() self._load_all_properties() def _show_editor_message(self, message): # Show an overlay message self._editor_pane_cover.config(text=message) self._editor_pane_cover.place(x=0, y=0, relwidth=1, relheight=1) def _clear_editor_message(self): self._editor_pane_cover.place_forget() def _show(self, item): item.pack(fill="x", pady=1) def _hide(self, item): item.pack_forget() def _add_item(self, item): # add a menu item style editor self._style_item_ref[item.name] = item self._show(item) def _add_menu_item(self, item): # add a parent menu style editor self._menu_style_ref[item.name] = item self._show(item) def _load_all_properties(self): # Generate all style editors that may be needed by any of the types of menu items # This needs to be called only once ref = dict(PROPERTY_TABLE) ref.update(MENU_PROPERTY_TABLE) for prop in MENU_PROPERTIES: if not ref.get(prop): continue definition = dict(ref.get(prop)) definition['name'] = prop self._add_item(StyleItem(self._menu_item_styles, definition, self._on_item_change)) menu_prop = get_properties(self._base_menu) for key in menu_prop: definition = menu_prop[key] self._add_menu_item(StyleItem(self._menu_styles, definition, self._on_menu_item_change)) def _on_item_change(self, prop, value): # Called when the style of a menu item changes for node in self._tree.get(): menu_config(node._menu, node.get_index(), **{prop: value}) # For changes in label we need to change the label on the node as well node node.label = node._menu.entrycget(node.get_index(), 'label') def _on_menu_item_change(self, prop, value): nodes = self._tree.get() menus = set([node._menu for node in nodes]) for menu in menus: menu[prop] = value def _refresh_styles(self): # TODO Fix false value change when releasing ctrl key during multi-selecting # called when structure or selection changes nodes = self._tree.get() # get current selection if not nodes: # if no nodes are currently selected display message self._show_editor_message(self._MESSAGE_EDITOR_EMPTY) return self._clear_editor_message() # remove any messages # get intersection of styles for currently selected nodes # these will be the styles common to all the nodes selected, use sets for easy analysis styles = set(nodes[0].get_options().keys()) for node in nodes: styles &= set(node.get_options().keys()) # populate editors with values of the last item # TODO this is not the best approach, no value should be set for an option if it is not the same for all nodes node = nodes[-1] for style_item in self._style_item_ref.values(): # styles for menu items if style_item.name in styles: self._show(style_item) style_item.set(node.get_option(style_item.name)) else: self._hide(style_item) for style_item in self._menu_style_ref.values(): # styles for the menu style_item.set(node._menu.cget(style_item.name)) def _preview(self, *_): self.widget.event_generate("<Button-1>") def _delete(self, *_): # create a copy since the list may change during iteration selected = list(self._tree.get()) for node in selected: self._tree.deselect(node) node.remove() self._refresh_styles() def add_item(self, _type): label = f"{_type.title()}" selected = self._tree.get() if len(selected) == 1 and selected[0].type == tk.CASCADE: node = selected[0] else: node = self._tree if _type != tk.SEPARATOR: node._sub_menu.add(_type, label=label) else: node._sub_menu.add(_type) node.add_menu_item(type=_type, label=label, index=tk.END) def load_menu(self, menu, node): # if the widget has a menu we need to populate the tree when the editor is created # we do this recursively to be able to capture even cascades # we cannot directly access all items in a menu or its size # but we can get the index of the last item and use that to get size and hence viable indexes size = menu.index(tk.END) if size is None: # menu is empty return for i in range(size + 1): if menu.type(i) == tk.CASCADE: label = menu.entrycget(i, "label") # get the cascades sub-menu and load it recursively sub = self.nametowidget(menu.entrycget(i, "menu")) item_node = node.add_menu_item(type=menu.type(i), label=label, index=i, sub_menu=sub) self.load_menu(item_node._sub_menu, item_node) elif menu.type(i) == tk.SEPARATOR: # Does not need a label, set it to the default 'separator' node.add_menu_item(type=menu.type(i), index=i, label='separator') elif menu.type(i) != 'tearoff': # skip any tear_off item since they cannot be directly manipulated label = menu.entrycget(i, "label") node.add_menu_item(type=menu.type(i), label=label, index=i)
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)
class EventPane(BaseFeature): name = "Event pane" icon = "blank" _defaults = { **BaseFeature._defaults, "side": "right", } NO_SELECTION_MSG = "You have not selected any widget selected" NO_EVENT_MSG = "You have not added any bindings" NO_MATCH_MSG = "No items match your search" def __init__(self, master, studio, **cnf): super().__init__(master, studio, **cnf) self.header = Frame(self, **self.style.surface) self.header.pack(side="top", fill="x") for i, title in enumerate(("Sequence", "Handler", "Add", " " * 3)): Label( self.header, **self.style.text_passive, text=title, anchor="w", ).grid(row=0, column=i, sticky='ew') # set the first two columns to expand evenly for column in range(2): self.header.grid_columnconfigure(column, weight=1, uniform=1) self.bindings = BindingsTable(self) self.bindings.on_value_change(self.modify_item) self.bindings.on_item_delete(self.delete_item) self.bindings.pack(fill="both", expand=True) self._add = Button(self._header, **self.style.button, width=25, height=25, image=get_icon_image("add", 15, 15)) self._add.pack(side="right") self._add.tooltip("Add event binding") self._add.on_click(self.add_new) self._search_btn = Button( self._header, **self.style.button, image=get_icon_image("search", 15, 15), width=25, height=25, ) self._search_btn.pack(side="right") self._search_btn.on_click(self.start_search) self._empty_frame = Label(self.bindings, **self.style.text_passive) self._show_empty(self.NO_SELECTION_MSG) def _show_empty(self, message): self._empty_frame.place(x=0, y=0, relwidth=1, relheight=1) self._empty_frame["text"] = message def _remove_empty(self): self._empty_frame.place_forget() def add_new(self, *_): if self.studio.selected is None: return self._remove_empty() new_binding = make_event("<>", "", False) widget = self.studio.selected if not hasattr(widget, "_event_map_"): setattr(widget, "_event_map_", {}) widget._event_map_[new_binding.id] = new_binding self.bindings.add(new_binding) def delete_item(self, item): widget = self.studio.selected if widget is None: return widget._event_map_.pop(item.id) self.bindings.remove(item.id) def modify_item(self, value: EventBinding): widget = self.studio.selected widget._event_map_[value.id] = value def on_select(self, widget): if widget is None: self._show_empty(self.NO_SELECTION_MSG) return self._remove_empty() bindings = getattr(widget, "_event_map_", {}) values = bindings.values() self.bindings.clear() self.bindings.add(*values) if not values: self._show_empty(self.NO_EVENT_MSG) def start_search(self, *_): if self.studio.selected: super().start_search() def on_search_query(self, query: str): showing = 0 self._remove_empty() self.bindings.hide_all() for item in self.bindings.items: if query in item.value.sequence or query in item.value.handler: item.show() showing += 1 if not showing: self._show_empty(self.NO_MATCH_MSG) def on_search_clear(self): self._remove_empty() self.bindings.hide_all() for item in self.bindings.items: item.show() super().on_search_clear()
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 StudioApplication(Application): ICON_PATH = get_resource_path(studio, "resources/images/formation_icon.png") THEME_PATH = pref.get("resource::theme") def __init__(self, master=None, **cnf): super().__init__(master, **cnf) # Load icon asynchronously to prevent issues which have been known to occur when loading it synchronously icon_image = load_tk_image(self.ICON_PATH) self.load_styles(self.THEME_PATH) self.iconphoto(True, icon_image) self.pref = pref self._restore_position() self.title('Formation Studio') self.protocol('WM_DELETE_WINDOW', self._on_close) self.shortcuts = ShortcutManager(self, pref) self.shortcuts.bind_all() self._register_actions() self._toolbar = Frame(self, **self.style.surface, height=30) self._toolbar.pack(side="top", fill="x") self._toolbar.pack_propagate(0) self._statusbar = Frame(self, **self.style.surface, height=20) self._statusbar.pack(side="bottom", fill="x") self._statusbar.pack_propagate(0) body = Frame(self, **self.style.surface) body.pack(fill="both", expand=True, side="top") self._right_bar = SideBar(body) self._right_bar.pack(side="right", fill="y") self._left_bar = SideBar(body) self._left_bar.pack(side="left", fill="y") self._pane = PanedWindow(body, **self.style.pane_horizontal) self._pane.pack(side="left", fill="both", expand=True) self._left = FeaturePane(self._pane, **self.style.pane_vertical) self._center = PanedWindow(self._pane, **self.style.pane_vertical) self._right = FeaturePane(self._pane, **self.style.pane_vertical) self._bin = [] self._clipboard = None self.current_preview = None self._pane.add(self._left, minsize=320, sticky='nswe', width=320) self._pane.add(self._center, minsize=400, width=16000, sticky='nswe') self._pane.add(self._right, minsize=320, sticky='nswe', width=320) self._panes = { "left": (self._left, self._left_bar), "right": (self._right, self._right_bar), "center": (self._center, None) } icon = get_icon_image self.actions = ( ("Delete", icon("delete", 20, 20), lambda e: self.delete(), "Delete selected widget"), ("Undo", icon("undo", 20, 20), lambda e: self.undo(), "Undo action"), ("Redo", icon("redo", 20, 20), lambda e: self.redo(), "Redo action"), ("Cut", icon("cut", 20, 20), lambda e: self.cut(), "Cut selected widget"), ("separator",), ("Fullscreen", icon("image_editor", 20, 20), lambda e: self.close_all(), "Design mode"), ("Separate", icon("separate", 20, 20), lambda e: self.features_as_windows(), "Open features in window mode"), ("Dock", icon("flip_horizontal", 15, 15), lambda e: self.features_as_docked(), "Dock all features"), ("separator",), ("New", icon("add", 20, 20), lambda e: self.open_new(), "New design"), ("Save", icon("save", 20, 20), lambda e: self.save(), "Save design"), ("Preview", icon("play", 20, 20), lambda e: self.preview(), "Preview design"), ) self.init_toolbar() self.selected = None # set the image option to blank if there is no image for the menu option self.blank_img = blank_img = icon("blank", 14, 14) self.tool_manager = ToolManager(self) # -------------------------------------------- menu definition ------------------------------------------------ self.menu_template = (EnableIf( lambda: self.selected, ("separator",), ("command", "copy", icon("copy", 14, 14), actions.get('STUDIO_COPY'), {}), ("command", "duplicate", icon("copy", 14, 14), actions.get('STUDIO_DUPLICATE'), {}), EnableIf( lambda: self._clipboard is not None, ("command", "paste", icon("clipboard", 14, 14), actions.get('STUDIO_PASTE'), {}) ), ("command", "cut", icon("cut", 14, 14), actions.get('STUDIO_CUT'), {}), ("separator",), ("command", "delete", icon("delete", 14, 14), actions.get('STUDIO_DELETE'), {}), ),) self.menu_bar = MenuUtils.make_dynamic( (( ("cascade", "formation", None, None, {"menu": ( ("command", "Restart", None, actions.get('STUDIO_RESTART'), {}), ("separator", ), ("command", "About Formation", icon("formation", 14, 14), lambda: about_window(self), {}), ), "name": "apple"}), ) if platform_is(MAC) else ()) + ( ("cascade", "File", None, None, {"menu": ( ("command", "New", icon("add", 14, 14), actions.get('STUDIO_NEW'), {}), ("command", "Open", icon("folder", 14, 14), actions.get('STUDIO_OPEN'), {}), ("cascade", "Recent", icon("clock", 14, 14), None, {"menu": self._create_recent_menu()}), ("separator",), EnableIf( lambda: self.designer, ("command", "Save", icon("save", 14, 14), actions.get('STUDIO_SAVE'), {}), ("command", "Save As", icon("blank", 14, 14), actions.get('STUDIO_SAVE_AS'), {}) ), EnableIf( # more than one design contexts open lambda: len([i for i in self.contexts if isinstance(i, DesignContext)]) > 1, ("command", "Save All", icon("blank", 14, 14), actions.get('STUDIO_SAVE_ALL'), {}) ), ("separator",), ("command", "Settings", icon("settings", 14, 14), actions.get('STUDIO_SETTINGS'), {}), ("command", "Restart", icon("blank", 14, 14), actions.get('STUDIO_RESTART'), {}), ("command", "Exit", icon("close", 14, 14), actions.get('STUDIO_EXIT'), {}), )}), ("cascade", "Edit", None, None, {"menu": ( EnableIf(lambda: self.context and self.context.has_undo(), ("command", "undo", icon("undo", 14, 14), actions.get('STUDIO_UNDO'), {})), EnableIf(lambda: self.context and self.context.has_redo(), ("command", "redo", icon("redo", 14, 14), actions.get('STUDIO_REDO'), {})), *self.menu_template, )}), ("cascade", "Code", None, None, {"menu": ( EnableIf( lambda: self.designer and self.designer.root_obj, ("command", "Preview design", icon("play", 14, 14), actions.get('STUDIO_PREVIEW'), {}), ("command", "close preview", icon("close", 14, 14), actions.get('STUDIO_PREVIEW_CLOSE'), {}), ("separator", ), EnableIf( lambda: self.designer and self.designer.design_path, ("command", "Reload design file", icon("rotate_clockwise", 14, 14), actions.get('STUDIO_RELOAD'), {}), ), ) )}), ("cascade", "View", None, None, {"menu": ( ("command", "show all panes", blank_img, actions.get('FEATURE_SHOW_ALL'), {}), ("command", "close all panes", icon("close", 14, 14), actions.get('FEATURE_CLOSE_ALL'), {}), ("command", "close all panes on the right", blank_img, actions.get('FEATURE_CLOSE_RIGHT'), {}), ("command", "close all panes on the left", blank_img, actions.get('FEATURE_CLOSE_LEFT'), {}), ("separator",), ("command", "Undock all windows", blank_img, actions.get('FEATURE_UNDOCK_ALL'), {}), ("command", "Dock all windows", blank_img, actions.get('FEATURE_DOCK_ALL'), {}), ("separator",), LoadLater(self.get_features_as_menu), ("separator",), EnableIf( lambda: self.context, ("command", "close tab", icon("close", 14, 14), actions.get('CONTEXT_CLOSE'), {}), ("command", "close all tabs", blank_img, actions.get('CONTEXT_CLOSE_ALL'), {}), EnableIf( lambda: self.context and len(self.tab_view.tabs()) > 1, ("command", "close other tabs", blank_img, actions.get('CONTEXT_CLOSE_OTHER'), {}) ), EnableIf( lambda: self.context and self.context._contexts_right(), ("command", "close all tabs on the right", blank_img, actions.get('CONTEXT_CLOSE_OTHER_RIGHT'), {}) ) ), ("separator",), ("command", "Save window positions", blank_img, actions.get('FEATURE_SAVE_POS'), {}) )}), ("cascade", "Tools", None, None, {"menu": (LoadLater(self.tool_manager.get_tools_as_menu), )}), ("cascade", "Help", None, None, {"menu": ( ("command", "Help", icon('dialog_info', 14, 14), actions.get('STUDIO_HELP'), {}), ("command", "Check for updates", icon("cloud", 14, 14), self._check_updates, {}), ("separator",), ("command", "About Formation", icon("formation", 14, 14), lambda: about_window(self), {}), )}) ), self, self.style, False) self.config(menu=self.menu_bar) if platform_is(MAC): self.createcommand("tk::mac::ShowPreferences", lambda: actions.get('STUDIO_SETTINGS').invoke()) self.createcommand("tk::mac::ShowHelp", lambda: actions.get('STUDIO_HELP').invoke()) self.createcommand("tk::mac::Quit", lambda: actions.get('STUDIO_EXIT').invoke()) self.features = [] self.context = None self.contexts = [] self.tab_view = TabView(self._center) self.tab_view.malleable(True) self.tab_view.bind("<<TabSelectionChanged>>", self.on_context_switch) self.tab_view.bind("<<TabClosed>>", self.on_context_close) self.tab_view.bind("<<TabAdded>>", self.on_context_add) self.tab_view.bind("<<TabOrderChanged>>", lambda _: self.save_tab_status()) self._center.add(self.tab_view, sticky='nswe') self._tab_view_empty = Label( self.tab_view, **self.style.text_passive, compound='top', image=get_icon_image("paint", 60, 60) ) self._tab_view_empty.config(**self.style.bright) # install features for feature in FEATURES: self.install(feature) # common feature references self.style_pane = self.get_feature(StylePane) # initialize tools with everything ready self.tool_manager.initialize() self._ignore_tab_status = False self._startup() self._exit_failures = 0 self._is_shutting_down = False def on_context_switch(self, _): selected = self.tab_view.selected if isinstance(self.context, BaseContext): self.context.on_context_unset() if isinstance(selected, BaseContext): self.context = selected else: self.context = None for feature in self.features: feature.on_context_switch() self.tool_manager.on_context_switch() if self.context: selected.on_context_set() # switch selection to that of the new context if self.designer: self.select(self.designer.current_obj, self.designer) else: self.select(None) self.save_tab_status() def on_context_close(self, context): if not self.tab_view.tabs(): self._show_empty("Open a design file") if context in self.contexts: self.contexts.remove(context) for feature in self.features: feature.on_context_close(context) self.tool_manager.on_context_close(context) self.save_tab_status() def on_context_add(self, _): self._show_empty(None) def add_context(self, context, select=True): self.contexts.append(context) tab = self.tab_view.add( context, None, False, text=context.name, icon=context.icon, closeable=True ) context.tab_handle = tab if select: self.tab_view.select(tab) context.on_context_mount() self.save_tab_status() def create_context(self, context, *args, select=True, **kwargs): new_context = context(self.tab_view, self, *args, **kwargs) self.add_context(new_context, select) return new_context def close_context(self): if self.context: self.context.close() def close_all_contexts(self): if self.check_unsaved_changes(): for context in list(self.contexts): context.close(force=True) def close_other_contexts(self): if self.context: self.context.close_other() def close_other_contexts_right(self): if self.context: self.context.close_other_right() @property def designer(self): if isinstance(self.context, DesignContext): return self.context.designer def _show_empty(self, text): if text: self._tab_view_empty.lift() self._tab_view_empty['text'] = text self._tab_view_empty.place(x=0, y=0, relwidth=1, relheight=1) else: self._tab_view_empty.place_forget() def _startup(self): on_startup = pref.get("studio::on_startup") if on_startup == "new": self.open_new() elif on_startup == "recent": self.restore_tabs() else: self._show_empty("Open a design file") def _get_window_state(self): try: if self.wm_attributes("-zoomed"): return 'zoomed' return 'normal' except: # works for windows and mac os return self.state() def _set_window_state(self, state): try: # works in windows and mac os self.state(state) except: self.wm_attributes('-zoomed', state == 'zoomed') def _save_position(self): # self.update_idletasks() pref.set("studio::pos", dict( geometry=self.geometry(), state=self._get_window_state(), # window state either zoomed or normal )) def _restore_position(self): pos = pref.get("studio::pos") state = pos.get('state', 'zoomed') self._set_window_state(state) if state == 'normal' and pos.get('geometry'): self.geometry(pos['geometry']) def new_action(self, action: Action): """ Register a undo redo point :param action: An action object implementing undo and redo methods :return: """ if self.context: self.context.new_action(action) def undo(self): if self.context: self.context.undo() def redo(self): if self.context: self.context.redo() def last_action(self): if self.context: return self.context.last_action() def pop_last_action(self, key=None): if self.context: self.context.pop_last_action(key) def copy(self): if self.designer and self.selected: # store the current object as node in the clipboard self._clipboard = self.designer.as_node(self.selected) def install_status_widget(self, widget_class, *args, **kwargs): widget = widget_class(self._statusbar, *args, **kwargs) widget.pack(side='right', padx=2, fill='y') return widget def get_pane_info(self, pane): return self._panes.get(pane, [self._right, self._right_bar]) def paste(self): if self.designer and self._clipboard is not None: self.designer.paste(self._clipboard) def close_all_on_side(self, side): for feature in self.features: if feature.side == side: feature.minimize() # To avoid errors when side is not a valid pane identifier we default to the right pane self._panes.get(side, (self._right, self._right_bar))[1].close_all() def close_all(self, *_): for feature in self.features: feature.minimize() self._right_bar.close_all() self._left_bar.close_all() def init_toolbar(self): for action in self.actions: if len(action) == 1: Frame(self._toolbar, width=1, bg=self.style.colors.get("primarydarkaccent")).pack( side='left', fill='y', pady=3, padx=5) continue btn = Button(self._toolbar, image=action[1], **self.style.button, width=25, height=25) btn.pack(side="left", padx=3) btn.tooltip(action[3]) ActionNotifier.bind_event("<Button-1>", btn, action[2], text=action[3]) def uninstall(self, feature): self.features.remove(feature) feature.bar.remove(feature) feature.pane.forget(feature) self._adjust_pane(feature.pane) def get_pane_bar(self, side): if side in self._panes: return self._panes.get(side, (self._left, self._left_bar)) def reposition(self, feature: BaseFeature, side): if self.get_pane_bar(side): pane, bar = self.get_pane_bar(side) feature.bar.remove(feature) feature.pane.forget(feature) self._adjust_pane(feature.pane) feature.bar = bar feature.pane = pane bar.add_feature(feature) if feature.get_pref("mode") == "docked": pane.add(feature, minsize=100) feature.set_pref("side", side) def install(self, feature) -> BaseFeature: obj = feature(self, self) pane, bar = self._panes.get(obj.get_pref('side'), (self._left, self._left_bar)) obj.pane = pane obj.bar = bar self.features.append(obj) if bar is not None: bar.add_feature(obj) if not obj.get_pref('visible'): bar.deselect(obj) self._adjust_pane(pane) else: bar.select(obj) obj.maximize() return obj def show_all_windows(self): for feature in self.features: feature.maximize() def features_as_windows(self): for feature in self.features: feature.open_as_window() def features_as_docked(self): for feature in self.features: feature.open_as_docked() def set_path(self, path): if path: file_dir = os.path.dirname(path) if os.path.exists(file_dir): # change working directory os.chdir(file_dir) path = path or "untitled" self.title("Formation studio" + " - " + str(path)) @dynamic_menu def _create_recent_menu(self, menu): # Dynamically create recent file menu every time menu is posted menu.image = get_icon_image("close", 14, 14) menu.config(**self.style.context_menu) recent = pref.get_recent() for path, label in recent: menu.add_command( label=label, command=functools.partial(self.open_recent, path), image=self.blank_img, compound='left', ) menu.add_command( label="Clear", image=menu.image, command=pref.clear_recent, compound="left" ) def open_file(self, path=None): if path is None: path = filedialog.askopenfilename(parent=self, filetypes=get_file_types()) elif not os.path.exists(path): MessageDialog.show_error( parent=self, title="Missing File", message="File {} does not exist".format(path), ) return if path: # find if path is already open on the designer for context in self.contexts: if isinstance(context, DesignContext) and context.path == path: # path is open, select context.select() break else: self.create_context(DesignContext, path) self.set_path(path) pref.update_recent(path) def open_recent(self, path): self.open_file(path) def open_new(self): context = self.create_context(DesignContext) self.set_path(context.name) def save(self): if self.designer: path = self.context.save() if path: self.set_path(path) self.save_tab_status() pref.update_recent(path) def save_as(self): if self.designer: path = self.context.save(new_path=True) if path: self.set_path(path) self.save_tab_status() pref.update_recent(path) def save_all(self): contexts = [ i for i in self.contexts if isinstance(i, DesignContext) and i.designer.has_changed() ] for context in contexts: if context.save() is None: # save has been cancelled break def get_feature(self, feature_class) -> BaseFeature: for feature in self.features: if feature.__class__ == feature_class: return feature # returns None by if feature is not found def get_features_as_menu(self): # For each feature we create a menu template # The command value is the self.maximize method which will reopen the feature return [("checkbutton", # Type f.name, None, # Label, image functools.partial(f.toggle), # Command built from feature {"variable": f.is_visible}) for f in self.features] def save_window_positions(self): for feature in self.features: feature.save_window_pos() self._save_position() def _adjust_pane(self, pane): if len(pane.panes()) == 0: self._pane.paneconfig(pane, minsize=0, width=0) self._pane.paneconfig(self._center, width=16000) else: self._pane.paneconfig(pane, minsize=320) def minimize(self, feature): feature.pane.forget(feature) feature.bar.deselect(feature) self._adjust_pane(feature.pane) def maximize(self, feature): feature.pane.add(feature, minsize=100) feature.bar.select(feature) self._adjust_pane(feature.pane) def select(self, widget, source=None): self.selected = widget if self.designer and source != self.designer: # Select from the designer explicitly so the selection does not end up being re-fired self.designer.select(widget, True) for feature in self.features: if feature != source: feature.on_select(widget) self.tool_manager.on_select(widget) def add(self, widget, parent=None): for feature in self.features: feature.on_widget_add(widget, parent) self.tool_manager.on_widget_add(widget, parent) def widget_modified(self, widget1, source=None, widget2=None): for feature in self.features: if feature != source: feature.on_widget_change(widget1, widget2) if self.designer and self.designer != source: self.designer.on_widget_change(widget1, widget2) self.tool_manager.on_widget_change(widget1, widget2) def widget_layout_changed(self, widget): for feature in self.features: feature.on_widget_layout_change(widget) self.tool_manager.on_widget_layout_change(widget) def delete(self, widget=None, source=None): widget = self.selected if widget is None else widget if widget is None: return if self.selected == widget: self.select(None) if self.designer and source != self.designer: self.designer.delete(widget) for feature in self.features: feature.on_widget_delete(widget) self.tool_manager.on_widget_delete(widget) def cut(self, widget=None, source=None): if not self.designer: return widget = self.selected if widget is None else widget if not widget: return if self.selected == widget: self.select(None) self._clipboard = self.designer.as_node(widget) if source != self.designer: self.designer.delete(widget, True) for feature in self.features: feature.on_widget_delete(widget, True) self.tool_manager.on_widget_delete(widget) def duplicate(self): if self.designer and self.selected: self.designer.paste(self.designer.as_node(self.selected)) def on_restore(self, widget): for feature in self.features: feature.on_widget_restore(widget) def on_feature_change(self, new, old): self.features.insert(self.features.index(old), new) self.features.remove(old) def on_session_clear(self, source): for feature in self.features: if feature != source: feature.on_session_clear() self.tool_manager.on_session_clear() def restore_tabs(self): # ignore all tab status changes as we restore tabs self._ignore_tab_status = True first_context = None has_select = False for context_dat in self.pref.get("studio::prev_contexts"): context = self.create_context( context_dat["class"], *context_dat["args"], select=context_dat["selected"], **context_dat["kwargs"] ) has_select = has_select or context_dat["selected"] first_context = context if first_context is None else first_context context.deserialize(context_dat["data"]) if not first_context: self._show_empty("Open a design file") elif not has_select: first_context.select() self._ignore_tab_status = False def save_tab_status(self): if self._ignore_tab_status: return status = [] for tab in self.tab_view._tab_order: context = self.tab_view._tabs[tab] if isinstance(context, BaseContext) and context.can_persist(): data = context.serialize() data["selected"] = self.context == context status.append(data) self.pref.set("studio::prev_contexts", status) def check_unsaved_changes(self, check_contexts=None): check_contexts = self.contexts if check_contexts is None else check_contexts unsaved = [ i for i in check_contexts if isinstance(i, DesignContext) and i.designer.has_changed() ] if len(unsaved) > 1: contexts = MultiSaveDialog.ask_save(self, self, check_contexts) if contexts is None: return False for context in contexts: if context.designer.save() is None: return False elif unsaved: return unsaved[0].designer.on_app_close() elif unsaved is None: return False return True def preview(self): if self.designer.root_obj is None: # If there is no root object show a warning MessageDialog.show_warning( parent=self, title='Empty design', message='There is nothing to preview. Please add a root widget') return # close previous preview if any self.close_preview() window = self.current_preview = Toplevel(self) window.wm_transient(self) window.build = AppBuilder(window, node=self.designer.to_tree()) name = self.designer.design_path if self.designer.design_path is not None else "Untitled" window.build._app.title(os.path.basename(name)) def close_preview(self): if self.current_preview: self.current_preview.destroy() def reload(self): if self.designer: self.designer.reload() def _force_exit_prompt(self): return MessageDialog.builder( {"text": "Force exit", "value": True, "focus": True}, {"text": "Return to app", "value": False}, wait=True, title="Exit Failure", message="An internal failure is preventing the app from exiting. Force exit?", parent=self, icon=MessageDialog.ICON_ERROR ) def _on_close(self): """ Return ``True`` if exit successful otherwise ``False`` """ if self._is_shutting_down: # block multiple close attempts return self._is_shutting_down = True try: self._save_position() # pass the on window close event to the features for feature in self.features: # if any feature returns false abort shut down feature.save_window_pos() if not feature.on_app_close(): self._is_shutting_down = False return False if not self.tool_manager.on_app_close() or not self.check_unsaved_changes(): self._is_shutting_down = False return False self.quit() return True except Exception: self._exit_failures += 1 if self._exit_failures >= 2: force = self._force_exit_prompt() if force: # exit by all means necessary sys.exit(1) self._is_shutting_down = False return False def get_help(self): # Entry point for studio help functionality webbrowser.open("https://formation-studio.readthedocs.io/en/latest/") def settings(self): open_preferences(self) def _coming_soon(self): MessageDialog.show_info( parent=self, title="Coming soon", message="We are working hard to bring this feature to you. Hang in there.", icon="clock" ) def _check_updates(self): Updater.check(self) def _register_actions(self): CTRL, ALT, SHIFT = KeyMap.CONTROL, KeyMap.ALT, KeyMap.SHIFT routine = actions.Routine # These actions are best bound separately to avoid interference with text entry widgets actions.add( routine(self.cut, 'STUDIO_CUT', 'Cut selected widget', 'studio', CTRL + CharKey('x')), routine(self.copy, 'STUDIO_COPY', 'Copy selected widget', 'studio', CTRL + CharKey('c')), routine(self.paste, 'STUDIO_PASTE', 'Paste selected widget', 'studio', CTRL + CharKey('v')), routine(self.delete, 'STUDIO_DELETE', 'Delete selected widget', 'studio', KeyMap.DELETE), routine(self.duplicate, 'STUDIO_DUPLICATE', 'Duplicate selected widget', 'studio', CTRL + CharKey('d')), ) self.shortcuts.add_routines( routine(self.undo, 'STUDIO_UNDO', 'Undo last action', 'studio', CTRL + CharKey('Z')), routine(self.redo, 'STUDIO_REDO', 'Redo action', 'studio', CTRL + CharKey('Y')), # ----------------------------- routine(self.open_new, 'STUDIO_NEW', 'Open new design', 'studio', CTRL + CharKey('n')), routine(self.open_file, 'STUDIO_OPEN', 'Open design from file', 'studio', CTRL + CharKey('o')), routine(self.save, 'STUDIO_SAVE', 'Save current design', 'studio', CTRL + CharKey('s')), routine(self.save_as, 'STUDIO_SAVE_AS', 'Save current design under a new file', 'studio', CTRL + SHIFT + CharKey('s')), routine(self.save_all, 'STUDIO_SAVE_ALL', 'Save all open designs', 'studio', CTRL + ALT + CharKey('s')), routine(self.get_help, 'STUDIO_HELP', 'Show studio help', 'studio', KeyMap.F(12)), routine(self.settings, 'STUDIO_SETTINGS', 'Open studio settings', 'studio', ALT + CharKey('s')), routine(restart, 'STUDIO_RESTART', 'Restart application', 'studio', BlankKey), routine(self._on_close, 'STUDIO_EXIT', 'Exit application', 'studio', CTRL + CharKey('q')), # ------------------------------ routine(self.show_all_windows, 'FEATURE_SHOW_ALL', 'Show all feature windows', 'studio', ALT + CharKey('a')), routine(self.close_all, 'FEATURE_CLOSE_ALL', 'Close all feature windows', 'studio', ALT + CharKey('x')), routine(lambda: self.close_all_on_side('right'), 'FEATURE_CLOSE_RIGHT', 'Close feature windows to the right', 'studio', ALT + CharKey('R')), routine(lambda: self.close_all_on_side('left'), 'FEATURE_CLOSE_LEFT', 'Close feature windows to the left', 'studio', ALT + CharKey('L')), routine(self.features_as_docked, 'FEATURE_DOCK_ALL', 'Dock all feature windows', 'studio', ALT + CharKey('d')), routine(self.features_as_windows, 'FEATURE_UNDOCK_ALL', 'Undock all feature windows', 'studio', ALT + CharKey('u')), routine(self.save_window_positions, 'FEATURE_SAVE_POS', 'Save window positions', 'studio', ALT + SHIFT + CharKey('s')), # ----------------------------- routine(self.close_context, 'CONTEXT_CLOSE', 'Close tab', 'studio', CTRL + CharKey('T')), routine(self.close_all_contexts, 'CONTEXT_CLOSE_ALL', 'Close all tabs', 'studio', CTRL + ALT + CharKey('T')), routine(self.close_other_contexts, 'CONTEXT_CLOSE_OTHER', 'Close other tabs', 'studio', BlankKey), routine(self.close_other_contexts_right, 'CONTEXT_CLOSE_OTHER_RIGHT', 'Close all tabs on the right', 'studio', BlankKey), # ----------------------------- routine(self.preview, 'STUDIO_PREVIEW', 'Show preview', 'studio', KeyMap.F(5)), routine(self.close_preview, 'STUDIO_PREVIEW_CLOSE', 'Close any preview', 'studio', ALT + KeyMap.F(5)), routine(self.reload, 'STUDIO_RELOAD', 'Reload current design', 'studio', CTRL + CharKey('R')) )
def __init__(self, master=None, **cnf): super().__init__(master, **cnf) # Load icon asynchronously to prevent issues which have been known to occur when loading it synchronously icon_image = load_tk_image(self.ICON_PATH) self.load_styles(self.THEME_PATH) self.iconphoto(True, icon_image) self.pref = pref self._restore_position() self.title('Formation Studio') self.protocol('WM_DELETE_WINDOW', self._on_close) self.shortcuts = ShortcutManager(self, pref) self.shortcuts.bind_all() self._register_actions() self._toolbar = Frame(self, **self.style.surface, height=30) self._toolbar.pack(side="top", fill="x") self._toolbar.pack_propagate(0) self._statusbar = Frame(self, **self.style.surface, height=20) self._statusbar.pack(side="bottom", fill="x") self._statusbar.pack_propagate(0) body = Frame(self, **self.style.surface) body.pack(fill="both", expand=True, side="top") self._right_bar = SideBar(body) self._right_bar.pack(side="right", fill="y") self._left_bar = SideBar(body) self._left_bar.pack(side="left", fill="y") self._pane = PanedWindow(body, **self.style.pane_horizontal) self._pane.pack(side="left", fill="both", expand=True) self._left = FeaturePane(self._pane, **self.style.pane_vertical) self._center = PanedWindow(self._pane, **self.style.pane_vertical) self._right = FeaturePane(self._pane, **self.style.pane_vertical) self._bin = [] self._clipboard = None self.current_preview = None self._pane.add(self._left, minsize=320, sticky='nswe', width=320) self._pane.add(self._center, minsize=400, width=16000, sticky='nswe') self._pane.add(self._right, minsize=320, sticky='nswe', width=320) self._panes = { "left": (self._left, self._left_bar), "right": (self._right, self._right_bar), "center": (self._center, None) } icon = get_icon_image self.actions = ( ("Delete", icon("delete", 20, 20), lambda e: self.delete(), "Delete selected widget"), ("Undo", icon("undo", 20, 20), lambda e: self.undo(), "Undo action"), ("Redo", icon("redo", 20, 20), lambda e: self.redo(), "Redo action"), ("Cut", icon("cut", 20, 20), lambda e: self.cut(), "Cut selected widget"), ("separator",), ("Fullscreen", icon("image_editor", 20, 20), lambda e: self.close_all(), "Design mode"), ("Separate", icon("separate", 20, 20), lambda e: self.features_as_windows(), "Open features in window mode"), ("Dock", icon("flip_horizontal", 15, 15), lambda e: self.features_as_docked(), "Dock all features"), ("separator",), ("New", icon("add", 20, 20), lambda e: self.open_new(), "New design"), ("Save", icon("save", 20, 20), lambda e: self.save(), "Save design"), ("Preview", icon("play", 20, 20), lambda e: self.preview(), "Preview design"), ) self.init_toolbar() self.selected = None # set the image option to blank if there is no image for the menu option self.blank_img = blank_img = icon("blank", 14, 14) self.tool_manager = ToolManager(self) # -------------------------------------------- menu definition ------------------------------------------------ self.menu_template = (EnableIf( lambda: self.selected, ("separator",), ("command", "copy", icon("copy", 14, 14), actions.get('STUDIO_COPY'), {}), ("command", "duplicate", icon("copy", 14, 14), actions.get('STUDIO_DUPLICATE'), {}), EnableIf( lambda: self._clipboard is not None, ("command", "paste", icon("clipboard", 14, 14), actions.get('STUDIO_PASTE'), {}) ), ("command", "cut", icon("cut", 14, 14), actions.get('STUDIO_CUT'), {}), ("separator",), ("command", "delete", icon("delete", 14, 14), actions.get('STUDIO_DELETE'), {}), ),) self.menu_bar = MenuUtils.make_dynamic( (( ("cascade", "formation", None, None, {"menu": ( ("command", "Restart", None, actions.get('STUDIO_RESTART'), {}), ("separator", ), ("command", "About Formation", icon("formation", 14, 14), lambda: about_window(self), {}), ), "name": "apple"}), ) if platform_is(MAC) else ()) + ( ("cascade", "File", None, None, {"menu": ( ("command", "New", icon("add", 14, 14), actions.get('STUDIO_NEW'), {}), ("command", "Open", icon("folder", 14, 14), actions.get('STUDIO_OPEN'), {}), ("cascade", "Recent", icon("clock", 14, 14), None, {"menu": self._create_recent_menu()}), ("separator",), EnableIf( lambda: self.designer, ("command", "Save", icon("save", 14, 14), actions.get('STUDIO_SAVE'), {}), ("command", "Save As", icon("blank", 14, 14), actions.get('STUDIO_SAVE_AS'), {}) ), EnableIf( # more than one design contexts open lambda: len([i for i in self.contexts if isinstance(i, DesignContext)]) > 1, ("command", "Save All", icon("blank", 14, 14), actions.get('STUDIO_SAVE_ALL'), {}) ), ("separator",), ("command", "Settings", icon("settings", 14, 14), actions.get('STUDIO_SETTINGS'), {}), ("command", "Restart", icon("blank", 14, 14), actions.get('STUDIO_RESTART'), {}), ("command", "Exit", icon("close", 14, 14), actions.get('STUDIO_EXIT'), {}), )}), ("cascade", "Edit", None, None, {"menu": ( EnableIf(lambda: self.context and self.context.has_undo(), ("command", "undo", icon("undo", 14, 14), actions.get('STUDIO_UNDO'), {})), EnableIf(lambda: self.context and self.context.has_redo(), ("command", "redo", icon("redo", 14, 14), actions.get('STUDIO_REDO'), {})), *self.menu_template, )}), ("cascade", "Code", None, None, {"menu": ( EnableIf( lambda: self.designer and self.designer.root_obj, ("command", "Preview design", icon("play", 14, 14), actions.get('STUDIO_PREVIEW'), {}), ("command", "close preview", icon("close", 14, 14), actions.get('STUDIO_PREVIEW_CLOSE'), {}), ("separator", ), EnableIf( lambda: self.designer and self.designer.design_path, ("command", "Reload design file", icon("rotate_clockwise", 14, 14), actions.get('STUDIO_RELOAD'), {}), ), ) )}), ("cascade", "View", None, None, {"menu": ( ("command", "show all panes", blank_img, actions.get('FEATURE_SHOW_ALL'), {}), ("command", "close all panes", icon("close", 14, 14), actions.get('FEATURE_CLOSE_ALL'), {}), ("command", "close all panes on the right", blank_img, actions.get('FEATURE_CLOSE_RIGHT'), {}), ("command", "close all panes on the left", blank_img, actions.get('FEATURE_CLOSE_LEFT'), {}), ("separator",), ("command", "Undock all windows", blank_img, actions.get('FEATURE_UNDOCK_ALL'), {}), ("command", "Dock all windows", blank_img, actions.get('FEATURE_DOCK_ALL'), {}), ("separator",), LoadLater(self.get_features_as_menu), ("separator",), EnableIf( lambda: self.context, ("command", "close tab", icon("close", 14, 14), actions.get('CONTEXT_CLOSE'), {}), ("command", "close all tabs", blank_img, actions.get('CONTEXT_CLOSE_ALL'), {}), EnableIf( lambda: self.context and len(self.tab_view.tabs()) > 1, ("command", "close other tabs", blank_img, actions.get('CONTEXT_CLOSE_OTHER'), {}) ), EnableIf( lambda: self.context and self.context._contexts_right(), ("command", "close all tabs on the right", blank_img, actions.get('CONTEXT_CLOSE_OTHER_RIGHT'), {}) ) ), ("separator",), ("command", "Save window positions", blank_img, actions.get('FEATURE_SAVE_POS'), {}) )}), ("cascade", "Tools", None, None, {"menu": (LoadLater(self.tool_manager.get_tools_as_menu), )}), ("cascade", "Help", None, None, {"menu": ( ("command", "Help", icon('dialog_info', 14, 14), actions.get('STUDIO_HELP'), {}), ("command", "Check for updates", icon("cloud", 14, 14), self._check_updates, {}), ("separator",), ("command", "About Formation", icon("formation", 14, 14), lambda: about_window(self), {}), )}) ), self, self.style, False) self.config(menu=self.menu_bar) if platform_is(MAC): self.createcommand("tk::mac::ShowPreferences", lambda: actions.get('STUDIO_SETTINGS').invoke()) self.createcommand("tk::mac::ShowHelp", lambda: actions.get('STUDIO_HELP').invoke()) self.createcommand("tk::mac::Quit", lambda: actions.get('STUDIO_EXIT').invoke()) self.features = [] self.context = None self.contexts = [] self.tab_view = TabView(self._center) self.tab_view.malleable(True) self.tab_view.bind("<<TabSelectionChanged>>", self.on_context_switch) self.tab_view.bind("<<TabClosed>>", self.on_context_close) self.tab_view.bind("<<TabAdded>>", self.on_context_add) self.tab_view.bind("<<TabOrderChanged>>", lambda _: self.save_tab_status()) self._center.add(self.tab_view, sticky='nswe') self._tab_view_empty = Label( self.tab_view, **self.style.text_passive, compound='top', image=get_icon_image("paint", 60, 60) ) self._tab_view_empty.config(**self.style.bright) # install features for feature in FEATURES: self.install(feature) # common feature references self.style_pane = self.get_feature(StylePane) # initialize tools with everything ready self.tool_manager.initialize() self._ignore_tab_status = False self._startup() self._exit_failures = 0 self._is_shutting_down = False
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()