コード例 #1
0
ファイル: editors.py プロジェクト: ObaraEmmanuel/Formation
class StyleItem(Frame):

    def __init__(self, parent, style_definition, on_change=None):
        super().__init__(parent.body)
        self.pref = get_active_pref(self)
        self.definition = style_definition
        self.name = style_definition.get("name")
        self.config(**self.style.surface)
        display = get_display_name(style_definition, self.pref)
        self._label = Label(self, **parent.style.text_passive, text=display,
                            anchor="w")
        self._label.grid(row=0, column=0, sticky='ew')
        self._editor = get_editor(self, style_definition)
        self._editor.grid(row=0, column=1, sticky='ew')
        self.grid_columnconfigure(1, weight=1, uniform=1)
        self.grid_columnconfigure(0, weight=1, uniform=1)
        self._on_change = on_change
        self._editor.set(style_definition.get("value"))
        self._editor.on_change(self._change)

    def _change(self, value):
        if self._on_change:
            self._on_change(self.name, value)

    def set_label(self, name):
        self._label.configure(text=name)

    def on_change(self, callback, *args, **kwargs):
        self._on_change = lambda name, val: callback(name, val, *args, **kwargs)

    def hide(self):
        self.grid_propagate(False)
        self.configure(height=0, width=0)

    def show(self):
        self.grid_propagate(True)

    def set(self, value):
        self._editor.set(value)

    def set_silently(self, value):
        # disable ability to trigger on change before setting value
        prev_callback = self._on_change
        self._on_change = None
        self.set(value)
        self._on_change = prev_callback
コード例 #2
0
class ComponentPane(BaseFeature):
    CLASSES = {
        "native": {
            "widgets": native.widgets
        },
        "legacy": {
            "widgets": legacy.widgets
        },
    }
    name = "Components"
    _var_init = False
    _defaults = {**BaseFeature._defaults, "widget_set": "native"}

    def __init__(self, master, studio=None, **cnf):
        if not self._var_init:
            self._init_var(studio)
        super().__init__(master, studio, **cnf)

        f = Frame(self, **self.style.dark)
        f.pack(side="top", fill="both", expand=True, pady=4)
        f.pack_propagate(0)

        self._widget_set = Spinner(self._header, width=150)
        self._widget_set.config(**self.style.no_highlight)
        self._widget_set.set_values(list(self.CLASSES.keys()))
        self._widget_set.pack(side="left")
        self._widget_set.on_change(self.collect_groups)
        self._select_pane = ScrolledFrame(f, width=150)
        self._select_pane.place(x=0, y=0, relwidth=0.4, relheight=1)

        self._search_btn = Button(self._header,
                                  image=get_icon_image("search", 15, 15),
                                  width=25,
                                  height=25,
                                  **self.style.dark_button)
        self._search_btn.pack(side="right")
        self._search_btn.on_click(self.start_search)
        self._search_selector = Label(self._select_pane.body,
                                      **self.style.dark_text,
                                      text="search",
                                      anchor="w")
        self._search_selector.configure(**self.style.dark_on_hover)

        self._widget_pane = ScrolledFrame(f, width=150, bg="orange")
        self._select_pane.body.config(**self.style.dark)
        self._widget_pane.place(relx=0.4, y=0, relwidth=0.6, relheight=1)

        self._pool = {}
        self._selectors = []
        self._selected = None
        self._component_cache = None
        self.collect_groups(self.get_pref("widget_set"))

    def _init_var(self, master=None):
        self._var_init = True
        for widget_set in self.CLASSES:
            self.CLASSES[widget_set]["var"] = BooleanVar(master, False)

    def _widget_sets_as_menu(self):
        return [
            (
                "checkbutton",  # Type checkbutton
                i.capitalize(),  # Label as title case
                None,  # Image
                partial(self.collect_groups, i),  # The callback
                {
                    "variable": self.CLASSES[i]["var"]
                }  # Additional config including the variable associated
            ) for i in self.CLASSES
        ]

    @property
    def selectors(self):
        return self._selectors

    def create_menu(self):
        return (
            ("command", "Search", get_icon_image("search", 14,
                                                 14), self.start_search, {}),
            ("cascade", "Widget set", None, None, {
                "menu": (*self._widget_sets_as_menu(), )
            }),
        )

    def collect_groups(self, widget_set):
        for other_set in [i for i in self.CLASSES if i != widget_set]:
            self.CLASSES[other_set]["var"].set(False)
        self.CLASSES[widget_set]["var"].set(True)
        self._widget_set.set(widget_set)
        self._select_pane.clear_children()
        self._pool = {}
        components = self.CLASSES.get(widget_set)["widgets"]
        for component in components:
            group = component.group.name
            if group in self._pool:
                self._pool[group].append(
                    Component(self._widget_pane.body, component))
            else:
                self._pool[group] = [
                    Component(self._widget_pane.body, component)
                ]
        self.render_groups()
        # component pool has changed so invalidate the cache
        self._component_cache = None
        self.set_pref("widget_set", widget_set)

    def get_components(self):
        if self._component_cache:
            return self._component_cache
        else:
            # flatten component pool and store to cache
            self._component_cache = [j for i in self._pool.values() for j in i]
            return self._component_cache

    def select(self, selector):
        if self._selected is not None:
            self._selected.deselect()
        selector.select()
        self._selected = selector
        self._widget_pane.clear_children()
        for component in self._pool[selector.name]:
            component.pack(side="top", pady=2, fill="x")

    def render_groups(self):
        self._selectors = []
        for group in self._pool:
            self.add_selector(Selector(self._select_pane.body, text=group))
        if len(self._selectors):
            self.select(self._selectors[0])

    def add_selector(self, selector):
        self._selectors.append(selector)
        selector.bind("<Button-1>", lambda *_: self.select(selector))
        selector.pack(side="top", pady=2, fill="x")

    def hide_selectors(self):
        for selector in self._selectors:
            selector.pack_forget()

    def show_selectors(self):
        for selector in self._selectors:
            selector.pack(side="top", pady=2, fill="x")

    def start_search(self, *_):
        super().start_search()
        self._widget_pane.scroll_to_start()
        if self._selected is not None:
            self._selected.deselect()
        self.hide_selectors()
        self._search_selector.pack(side="top", pady=2, fill="x")
        self._widget_pane.clear_children()
        # Display all components by running an empty query
        self.on_search_query("")

    def on_search_clear(self):
        super().on_search_clear()
        if len(self._selectors):
            self.select(self._selectors[0])
        self._search_selector.pack_forget()
        self.show_selectors()

    def on_search_query(self, query):
        for component in self.get_components():
            if query.lower() in component.component.display_name.lower():
                component.pack(side="top", pady=2, fill="x")
            else:
                component.pack_forget()
コード例 #3
0
ファイル: design.py プロジェクト: ObaraEmmanuel/Formation
class Designer(DesignPad, Container):
    MOVE = 0x2
    RESIZE = 0x3
    WIDGET_INIT_PADDING = 20
    WIDGET_INIT_HEIGHT = 25
    name = "Designer"
    pane = None
    _coord_indicator = None

    def __init__(self, master, studio):
        super().__init__(master)
        self.id = None
        self.context = master
        self.studio = studio
        self.name_generator = NameGenerator(self.studio.pref)
        self.setup_widget()
        self.parent = self
        self.config(**self.style.bright, takefocus=True)
        self.objects = []
        self.root_obj = None
        self.layout_strategy = DesignLayoutStrategy(self)
        self.highlight = HighLight(self)
        self.highlight.on_resize(self._on_size_changed)
        self.highlight.on_move(self._on_move)
        self.highlight.on_release(self._on_release)
        self.highlight.on_start(self._on_start)
        self._update_throttling()
        self.studio.pref.add_listener("designer::frame_skip",
                                      self._update_throttling)
        self.current_obj = None
        self.current_container = None
        self.current_action = None
        self._displace_active = False
        self._last_displace = time.time()
        self._frame.bind("<Button-1>", lambda _: self.focus_set(), '+')
        self._frame.bind("<Button-1>", self.set_pos, '+')
        self._frame.bind('<Motion>', self.on_motion, '+')
        self._frame.bind('<KeyRelease>', self._stop_displace, '+')
        self._padding = 30
        self.design_path = None
        self.builder = DesignBuilder(self)
        self._shortcut_mgr = KeyMap(self._frame)
        self._set_shortcuts()
        self._last_click_pos = None

        self._empty = Label(
            self,
            image=get_tk_image("paint", 30, 30),
            compound="top",
            text=" ",
            **self.style.text_passive,
        )
        self._empty.config(**self.style.bright)
        self._show_empty(True)

        # create the dynamic menu
        self._context_menu = MenuUtils.make_dynamic(
            self.studio.menu_template +
            (LoadLater(self.studio.tool_manager.get_tool_menu), ) +
            (LoadLater(lambda: self.current_obj.create_menu()
                       if self.current_obj else ()), ), self.studio,
            self.style)
        design_menu = (EnableIf(
            lambda: self.studio._clipboard is not None,
            ("command", "paste", icon("clipboard", 14, 14),
             lambda: self.paste(self.studio._clipboard, paste_to=self), {})), )
        self.set_up_context(design_menu)
        self._empty.set_up_context(design_menu)
        if Designer._coord_indicator is None:
            Designer._coord_indicator = self.studio.install_status_widget(
                CoordinateIndicator)
        self._text_editor = Text(self, wrap='none')
        self._text_editor.on_change(self._text_change)
        self._text_editor.bind("<FocusOut>", self._text_hide)
        self._base_font = FontStyle()

    def _get_designer(self):
        return self

    def focus_set(self):
        self._frame.focus_force()

    def set_pos(self, event):
        # store the click position for effective widget pasting
        self._last_click_pos = event.x_root, event.y_root

    def _update_throttling(self, *_):
        self.highlight.set_skip_max(
            self.studio.pref.get("designer::frame_skip"))

    def _show_empty(self, flag, **kw):
        if flag:
            kw['image'] = kw.get('image', get_tk_image('paint', 30, 30))
            kw['text'] = kw.get('text',
                                "Drag or paste a container here to start")
            self._empty.configure(**kw)
            self._empty.place(relwidth=1, relheight=1)
        else:
            self._empty.place_forget()

    def _set_shortcuts(self):
        shortcut_mgr = self._shortcut_mgr
        shortcut_mgr.bind()
        shortcut_mgr.add_routines(
            actions.get('STUDIO_COPY'),
            actions.get('STUDIO_CUT'),
            actions.get('STUDIO_DELETE'),
            actions.get('STUDIO_PASTE'),
            actions.get('STUDIO_DUPLICATE'),
        )
        # allow control of widget position using arrow keys
        shortcut_mgr.add_shortcut(
            (lambda: self.displace('right'), KeyMap.RIGHT),
            (lambda: self.displace('left'), KeyMap.LEFT),
            (lambda: self.displace('up'), KeyMap.UP),
            (lambda: self.displace('down'), KeyMap.DOWN),
        )

    def _open_default(self):
        self.update_idletasks()
        from studio.lib import legacy
        width = max(self.width - self._padding * 2, 300)
        height = max(self.height - self._padding * 2, 300)
        self.add(legacy.Frame,
                 self._padding,
                 self._padding,
                 width=width,
                 height=height)
        self.builder.generate()
        self.design_path = None

    @property
    def _ids(self):
        return [i.id for i in self.objects]

    def has_changed(self):
        # check if design has changed since last save or loading so we can prompt user to save changes
        builder = self.builder
        if self.root_obj:
            builder = DesignBuilder(self)
            builder.generate()
        return builder != self.builder

    def open_new(self):
        # open a blank design
        self.open_file(None)

    def to_tree(self):
        """ Generate node form of current design state without needing to save"""
        builder = DesignBuilder(self)
        builder.generate()
        return builder.root

    def save_prompt(self):
        return MessageDialog.builder(
            {
                "text": "Save",
                "value": True,
                "focus": True
            }, {
                "text": "Don't save",
                "value": False
            }, {
                "text": "Cancel",
                "value": None
            },
            wait=True,
            title="Save design",
            message=
            f"Design file \"{self.context.name}\" has unsaved changes. Do you want to save them?",
            parent=self.studio,
            icon=MessageDialog.ICON_INFO)

    def open_file(self, path=None):
        if self.has_changed():
            save = self.save_prompt()
            if save:
                # user opted to save
                saved_to = self.save()
                if saved_to is None:
                    # User did not complete saving and opted to cancel
                    return
            elif save is None:
                # user made no choice or basically selected cancel
                return
        if path:
            self.builder = DesignBuilder(self)
            progress = MessageDialog.show_progress(
                mode=MessageDialog.INDETERMINATE,
                message='Loading design file to studio...',
                parent=self.studio)
            self._load_design(path, progress)
        else:
            # if no path is supplied the default behaviour is to open a blank design
            self._open_default()

    def clear(self):
        # Warning: this method deletes elements irreversibly
        # remove the current root objects and their descendants
        self.studio.select(None)
        # create a copy since self.objects will mostly change during iteration
        # remove root and dangling objects
        for widget in self.objects:
            widget.destroy()
        self.objects.clear()
        self.root_obj = None

    def _verify_version(self):
        if self.builder.metadata.get("version"):
            _, major, __ = __version__.split(".")
            if major < self.builder.metadata["version"].get("major", 0):
                MessageDialog.show_warning(
                    parent=self.studio,
                    message=
                    ("Design was made using a higher version of the studio. \n"
                     "Some features may not be supported on this version. \n"
                     "Update to a new version of Formation for proper handling. \n"
                     "Note that saving may irreversibly strip off any unsupported features"
                     ))

    @as_thread
    def _load_design(self, path, progress=None):
        # Loading designs is elaborate so better do it on its own thread
        # Capture any errors that occur while loading
        # This helps the user single out syntax errors and other value errors
        try:
            self.design_path = path
            self.root_obj = self.builder.load(path, self)
            self.context.on_load_complete()
        except Exception as e:
            self.clear()
            self.studio.on_session_clear(self)
            accelerator = actions.get_routine("STUDIO_RELOAD").accelerator
            text = f"{str(e)}\nPress {accelerator} to reload" if accelerator else f"{str(e)} \n reload design"
            self._show_empty(True,
                             text=text,
                             image=get_tk_image("dialog_error", 50, 50))
            # MessageDialog.show_error(parent=self.studio, title='Error loading design', message=str(e))
        finally:
            if progress:
                progress.destroy()
            self._verify_version()

    def reload(self, *_):
        if not self.design_path or self.studio.context != self.context:
            return
        if self.has_changed():
            okay = MessageDialog.ask_okay_cancel(
                title="Confirm reload",
                message="All changes made will be lost",
                parent=self.studio)
            if not okay:
                # user made no choice or basically selected cancel
                return
        self.clear()
        self.studio.on_session_clear(self)
        self.open_file(self.design_path)

    def save(self, new_path=False):
        if not self.design_path or new_path:
            path = filedialog.asksaveasfilename(parent=self,
                                                filetypes=get_file_types(),
                                                defaultextension='.xml')
            if not path:
                return None
            self.design_path = path
        self.builder.write(self.design_path)
        return self.design_path

    def as_node(self, widget):
        builder = self.builder
        if builder is None:
            builder = DesignBuilder(self)
        return builder.to_tree(widget)

    def paste(self, node, silently=False, paste_to=None):
        if paste_to is None:
            paste_to = self.current_obj
        if paste_to is None:
            return
        layout = paste_to if isinstance(paste_to,
                                        Container) else paste_to.layout
        width = int(node["layout"]["width"] or 0)
        height = int(node["layout"]["height"] or 0)
        x, y = self._last_click_pos or (self.winfo_rootx() + 50,
                                        self.winfo_rooty() + 50)
        self._last_click_pos = x + 5, y + 5  # slightly displace click position so multiple pastes are still visible
        bounds = geometry.resolve_bounds((x, y, x + width, y + height), self)
        obj = self.builder.load_section(node, layout, bounds)
        restore_point = layout.get_restore(obj)
        # Create an undo redo point if add is not silent
        if not silently:
            self.studio.new_action(
                Action(
                    # Delete silently to prevent adding the event to the undo/redo stack
                    lambda _: self.delete(obj, True),
                    lambda _: self.restore(obj, restore_point, obj.layout)))
        return obj

    def _get_unique(self, obj_class):
        """
        Generate a unique id for widget belonging to a given class
        """
        return self.name_generator.generate(obj_class, self._ids)

    def on_motion(self, event):
        self.highlight.resize(event)
        geometry.make_event_relative(event, self)
        self._coord_indicator.set_coord(self._frame.canvasx(event.x),
                                        self._frame.canvasy(event.y))

    def _attach(self, obj):
        # bind events for context menu and object selection
        # all widget additions call this method so clear empty message
        self._show_empty(False)
        MenuUtils.bind_all_context(obj,
                                   lambda e: self.show_menu(e, obj),
                                   add='+')
        obj.bind_all(
            '<Shift-ButtonPress-1>',
            lambda e: self.highlight.set_function(self.highlight.move, e),
            add='+')
        obj.bind_all('<Motion>', self.on_motion, '+')
        obj.bind_all('<ButtonRelease>', self.highlight.clear_resize, '+')
        if "text" in obj.keys():
            obj.bind_all("<Double-Button-1>",
                         lambda _: self._show_text_editor(obj))
        self.objects.append(obj)
        if self.root_obj is None:
            self.root_obj = obj
        obj.bind_all("<Button-1>",
                     lambda e: self._handle_select(obj, e),
                     add='+')
        # bind shortcuts
        self._shortcut_mgr.bind_widget(obj)

    def show_menu(self, event, obj=None):
        # select object generating the context menu event first
        if obj is not None:
            self.select(obj)
        MenuUtils.popup(event, self._context_menu)

    def _handle_select(self, obj, event):
        # store the click position for effective widget pasting
        self._last_click_pos = event.x_root, event.y_root
        self.select(obj)

    def load(self,
             obj_class,
             name,
             container,
             attributes,
             layout,
             bounds=None):
        obj = obj_class(self, name)
        obj.configure(**attributes)
        self._attach(obj)
        if bounds is not None:
            container.add_widget(obj, bounds)
        else:
            container.add_widget(obj, **layout)
        if container == self:
            container = None
        self.studio.add(obj, container)
        return obj

    def _show_root_widget_warning(self):
        MessageDialog.show_warning(
            title='Invalid root widget',
            parent=self.studio,
            message='Only containers are allowed as root widgets')

    def add(self, obj_class: PseudoWidget.__class__, x, y, **kwargs):
        if obj_class.group != Groups.container and self.root_obj is None:
            # We only need a container as the root widget
            self._show_root_widget_warning()
            return
        silent = kwargs.get("silently", False)
        name = self._get_unique(obj_class)
        obj = obj_class(self, name)
        if hasattr(obj, 'initial_dimensions'):
            width, height = obj.initial_dimensions
        else:
            width = kwargs.get(
                "width",
                self._base_font.measure(name) + self.WIDGET_INIT_PADDING)
            height = kwargs.get("height", self.WIDGET_INIT_HEIGHT)
        obj.layout = kwargs.get("intended_layout", None)
        self._attach(obj)  # apply extra bindings required
        layout = kwargs.get("layout")
        # If the object has a layout which actually the layout at the point of creation prepare and pass it
        # to the layout
        if isinstance(layout, Container):
            bounds = (x, y, x + width, y + height)
            bounds = geometry.resolve_bounds(bounds, self)
            layout.add_widget(obj, bounds)
            self.studio.add(obj, layout)
            restore_point = layout.get_restore(obj)
            # Create an undo redo point if add is not silent
            if not silent:
                self.studio.new_action(
                    Action(
                        # Delete silently to prevent adding the event to the undo/redo stack
                        lambda _: self.delete(obj, True),
                        lambda _: self.restore(obj, restore_point, obj.layout))
                )
        elif obj.layout is None:
            # This only happens when adding the main layout. We dont need to add this action to the undo/redo stack
            # This main layout is attached directly to the designer
            obj.layout = self
            self.layout_strategy.add_widget(obj,
                                            x=x,
                                            y=y,
                                            width=width,
                                            height=height)
            self.studio.add(obj, None)

        return obj

    def select_layout(self, layout: Container):
        pass

    def restore(self, widget, restore_point, container):
        container.restore_widget(widget, restore_point)
        self.studio.on_restore(widget)
        self._replace_all(widget)

    def _replace_all(self, widget):
        # Recursively add widget and all its children to objects
        self.objects.append(widget)
        if self.root_obj is None:
            self.root_obj = widget
        if isinstance(widget, Container):
            for child in widget._children:
                self._replace_all(child)

    def delete(self, widget, silently=False):
        if not widget:
            return
        if not silently:
            restore_point = widget.layout.get_restore(widget)
            self.studio.new_action(
                Action(
                    lambda _: self.restore(widget, restore_point, widget.layout
                                           ),
                    lambda _: self.studio.delete(widget, True)))
        else:
            self.studio.delete(widget, self)
        widget.layout.remove_widget(widget)
        if widget == self.root_obj:
            # try finding another toplevel widget that can be a root obj otherwise leave it as none
            self.root_obj = None
            for w in self.layout_strategy.children:
                if isinstance(w, Container) or w.group == Groups.container:
                    self.root_obj = w
                    break
        self._uproot_widget(widget)
        if not self.objects:
            self._show_empty(True)

    def _uproot_widget(self, widget):
        # Recursively remove widgets and all its children
        if widget in self.objects:
            self.objects.remove(widget)
        if isinstance(widget, Container):
            for child in widget._children:
                self._uproot_widget(child)

    def set_active_container(self, container):
        if self.current_container is not None:
            self.current_container.clear_highlight()
        self.current_container = container

    def compute_overlap(self, bound1, bound2):
        return geometry.compute_overlap(bound1, bound2)

    def layout_at(self, bounds):
        for container in sorted(filter(
                lambda x: isinstance(x, Container) and x != self.current_obj,
                self.objects),
                                key=lambda x: len(self.objects) - x.level):
            if isinstance(
                    self.current_obj,
                    Container) and self.current_obj.level < container.level:
                continue
            if self.compute_overlap(geometry.bounds(container), bounds):
                return container
        return None

    def parse_bounds(self, bounds):
        return {
            "x": bounds[0],
            "y": bounds[1],
            "width": bounds[2] - bounds[0],
            "height": bounds[3] - bounds[1]
        }

    def position(self, widget, bounds):
        self.place_child(widget, **self.parse_bounds(bounds))

    def select(self, obj, explicit=False):
        if obj is None:
            self.clear_obj_highlight()
            self.studio.select(None, self)
            self.highlight.clear()
            return
        self.focus_set()
        if self.current_obj == obj:
            return
        self.clear_obj_highlight()
        self.current_obj = obj
        self.draw_highlight(obj)
        if not explicit:
            # The event is originating from the designer
            self.studio.select(obj, self)

    def draw_highlight(self, obj):
        self.highlight.surround(obj)

    def _stop_displace(self, _):
        if self._displace_active:
            # this ensures event is added to undo redo stack
            self._on_release(geometry.bounds(self.current_obj))
            # mark the latest action as designer displace
            latest = self.studio.last_action()
            if latest is not None:
                latest.key = "designer_displace"
            self._displace_active = False

    def displace(self, side):
        if not self.current_obj:
            return
        if time.time() - self._last_displace < .5:
            self.studio.pop_last_action("designer_displace")

        self._on_start()
        self._displace_active = True
        self._last_displace = time.time()
        bounds = geometry.bounds(self.current_obj)
        x1, y1, x2, y2 = bounds
        if side == 'right':
            bounds = x1 + 1, y1, x2 + 1, y2
        elif side == 'left':
            bounds = x1 - 1, y1, x2 - 1, y2
        elif side == 'up':
            bounds = x1, y1 - 1, x2, y2 - 1
        elif side == 'down':
            bounds = x1, y1 + 1, x2, y2 + 1
        self._on_move(bounds)

    def clear_obj_highlight(self):
        if self.highlight is not None:
            self.highlight.clear()
            self.current_obj = None
            if self.current_container is not None:
                self.current_container.clear_highlight()
                self.current_container = None

    def _on_start(self):
        obj = self.current_obj
        if obj is not None:
            obj.layout.change_start(obj)

    def _on_release(self, bound):
        obj = self.current_obj
        container = self.current_container
        if obj is None:
            return
        if container is not None and container != obj:
            container.clear_highlight()
            if self.current_action == self.MOVE:
                container.add_widget(obj, bound)
                # If the enclosed widget was initially the root object, make the container the new root object
                if obj == self.root_obj and obj != self:
                    self.root_obj = self.current_container
            else:
                obj.layout.widget_released(obj)
            self.studio.widget_layout_changed(obj)
            self.current_action = None
            self.create_restore(obj)
        elif obj.layout == self and self.current_action == self.MOVE:
            self.create_restore(obj)
        elif self.current_action == self.RESIZE:
            obj.layout.widget_released(obj)
            self.current_action = None
            self.create_restore(obj)

    def create_restore(self, widget):
        prev_restore_point = widget.recent_layout_info
        cur_restore_point = widget.layout.get_restore(widget)
        if prev_restore_point == cur_restore_point:
            return
        prev_container = prev_restore_point["container"]
        container = widget.layout

        def undo(_):
            container.remove_widget(widget)
            prev_container.restore_widget(widget, prev_restore_point)

        def redo(_):
            prev_container.remove_widget(widget)
            container.restore_widget(widget, cur_restore_point)

        self.studio.new_action(Action(undo, redo))

    def _on_move(self, new_bound):
        obj = self.current_obj
        current_container = self.current_container
        if obj is None:
            return
        self.current_action = self.MOVE
        container: Container = self.layout_at(new_bound)
        if container is not None and obj != container:
            if container != current_container:
                if current_container is not None:
                    current_container.clear_highlight()
                container.show_highlight()
                self.current_container = container
            container.move_widget(obj, new_bound)
        else:
            if current_container is not None:
                current_container.clear_highlight()
                self.current_container = self
            obj.level = 0
            obj.layout = self
            self.move_widget(obj, new_bound)
        if obj.layout.layout_strategy.realtime_support:
            self.studio.widget_layout_changed(obj)

    def _on_size_changed(self, new_bound):
        obj = self.current_obj
        if obj is None:
            return
        self.current_action = self.RESIZE
        if isinstance(obj.layout, Container):
            obj.layout.resize_widget(obj, new_bound)

        if obj.layout.layout_strategy.realtime_support:
            self.studio.widget_layout_changed(obj)

    def _text_change(self):
        self.studio.style_pane.apply_style("text", self._text_editor.get_all(),
                                           self.current_obj)

    def _show_text_editor(self, widget):
        assert widget == self.current_obj
        self._text_editor.lift(widget)
        cnf = self._collect_text_config(widget)
        self._text_editor.config(**cnf)
        self._text_editor.place(in_=widget, relwidth=1, relheight=1, x=0, y=0)
        self._text_editor.clear()
        self._text_editor.focus_set()
        self._text_editor.insert("1.0", widget["text"])

    def _collect_text_config(self, widget):
        s = ttk.Style()
        config = dict(background="#ffffff",
                      foreground="#000000",
                      font="TkDefaultFont")
        keys = widget.keys()
        for opt in config:
            if opt in keys:
                config[opt] = (widget[opt] or config[opt])
            else:
                config[opt] = (s.lookup(widget.winfo_class(), opt)
                               or config[opt])
        config["insertbackground"] = config["foreground"]
        return config

    def _text_hide(self, *_):
        self._text_editor.place_forget()

    def on_select(self, widget):
        self.select(widget)

    def on_widget_change(self, old_widget, new_widget=None):
        pass

    def on_widget_add(self, widget, parent):
        pass

    def show_highlight(self, *_):
        pass

    def save_window_pos(self):
        pass

    def on_app_close(self):
        if self.has_changed():
            save = self.save_prompt()
            if save:
                save_to = self.save()
                if save_to is None:
                    return False
            elif save is None:
                return False
        return True
コード例 #4
0
class VariablePane(BaseFeature):
    name = "Variablepane"
    icon = "text"

    _defaults = {**BaseFeature._defaults, "side": "right"}

    _definitions = {
        "name": {
            "name": "name",
            "type": "text",
        }
    }

    _empty_message = "No variables added"

    def __init__(self, master, studio=None, **cnf):
        super().__init__(master, studio, **cnf)
        f = Frame(self, **self.style.surface)
        f.pack(side="top", fill="both", expand=True, pady=4)
        f.pack_propagate(0)

        self._variable_pane = ScrolledFrame(f, width=150)
        self._variable_pane.place(x=0, y=0, relwidth=0.4, relheight=1)

        self._detail_pane = ScrolledFrame(f, width=150)
        self._detail_pane.place(relx=0.4,
                                y=0,
                                relwidth=0.6,
                                relheight=1,
                                x=15,
                                width=-20)

        Label(self._detail_pane.body,
              **self.style.text_passive,
              text="Type",
              anchor="w").pack(side="top", fill="x")
        self.var_type_lbl = Label(self._detail_pane.body,
                                  **self.style.text,
                                  anchor="w")
        self.var_type_lbl.pack(side="top", fill="x")
        Label(self._detail_pane.body,
              **self.style.text_passive,
              text="Name",
              anchor="w").pack(side="top", fill="x")
        self.var_name = editors.get_editor(self._detail_pane.body,
                                           self._definitions["name"])
        self.var_name.pack(side="top", fill="x")
        Label(self._detail_pane.body,
              **self.style.text_passive,
              text="Value",
              anchor="w").pack(fill="x", side="top")
        self._editors = {}
        self._editor = None

        self._search_btn = Button(self._header,
                                  image=get_icon_image("search", 15, 15),
                                  width=25,
                                  height=25,
                                  **self.style.button)
        self._search_btn.pack(side="right")
        self._search_btn.on_click(self.start_search)
        self._search_query = None

        self._add = MenuButton(self._header, **self.style.button)
        self._add.configure(image=get_icon_image("add", 15, 15))
        self._add.pack(side="right")
        self._delete_btn = Button(self._header,
                                  image=get_icon_image("delete", 15, 15),
                                  width=25,
                                  height=25,
                                  **self.style.button)
        self._delete_btn.pack(side="right")
        self._delete_btn.on_click(self._delete)
        self._var_types_menu = self.make_menu(self._get_add_menu(),
                                              self._add,
                                              title="Add variable")
        self._var_types_menu.configure(tearoff=True)
        self._add.config(menu=self._var_types_menu)
        self._selected = None
        self._links = {}
        self._overlay = Label(f,
                              **self.style.text_passive,
                              text=self._empty_message,
                              compound="top")
        self._overlay.configure(image=get_icon_image("add", 25, 25))
        self._show_overlay(True)

    def start_search(self, *_):
        if self.variables:
            super().start_search()
            self._variable_pane.scroll_to_start()

    def on_search_query(self, query):
        matches = []
        self._variable_pane.clear_children()
        for item in self.variables:
            if query in item.name:
                self._show(item)
                matches.append(item)

        if not matches:
            self._show_overlay(True,
                               text="No matches found",
                               image=get_icon_image("search", 25, 25))
        else:
            self.select(matches[0])
            self._show_overlay(False)
        self._search_query = query

    def on_search_clear(self):
        self.on_search_query("")
        self._search_query = None
        # remove overlay if we have variables otherwise show it
        self._show_overlay(not self.variables)
        super().on_search_clear()

    def _get_add_menu(self):
        _types = VariableItem._types
        return [(tk.COMMAND, _types[i].get("name"),
                 get_icon_image(_types[i].get("icon"), 14,
                                14), functools.partial(self.menu_add_var,
                                                       i), {}) for i in _types]

    def create_menu(self):
        return (
            ("cascade", "Add", get_icon_image("add", 14, 14), None, {
                "menu": self._get_add_menu()
            }),
            ("command", "Delete", get_icon_image("delete", 14,
                                                 14), self._delete, {}),
            ("command", "Search", get_icon_image("search", 14,
                                                 14), self.start_search, {}),
        )

    def _show_overlay(self, flag=True, **kwargs):
        if flag:
            kwargs["text"] = kwargs.get("text", self._empty_message)
            kwargs["image"] = kwargs.get("image",
                                         get_icon_image("add", 25, 25))
            self._overlay.lift()
            self._overlay.configure(**kwargs)
            self._overlay.place(x=0, y=0, relwidth=1, relheight=1)
        else:
            self._overlay.place_forget()

    def menu_add_var(self, var_type, **kw):
        item = self.add_var(var_type, **kw)
        self.select(item)

    def add_var(self, var_type, **kw):
        var = var_type(self.studio)
        item_count = len(
            list(filter(lambda x: x.var_type == var_type, self.variables))) + 1
        name = kw.get('name', f"{var_type.__name__}_{item_count}")
        value = kw.get('value')
        item = VariableItem(self._variable_pane.body, var, name)
        item.bind("<Button-1>", lambda e: self.select(item))
        if value is not None:
            item.set(value)

        self._show(item)
        self._show_overlay(False)
        if self._search_query is not None:
            # reapply search if any
            self.on_search_query(self._search_query)
        elif not self.variables:
            self.select(item)
        VariableManager.add(item)
        return item

    def delete_var(self, var):
        self._hide(var)
        VariableManager.remove(var)

    def _delete(self, *_):
        if self._selected:
            self.delete_var(self._selected)
        if self.variables:
            self.select(self.variables[0])
        else:
            self._show_overlay(True)

    def clear_variables(self):
        # the list is likely to change during iteration, create local copy
        variables = list(self.variables)
        for var in variables:
            self.delete_var(var)
        self._show_overlay(True)

    @property
    def variables(self):
        return VariableManager.variables()

    def select(self, item):
        if item == self._selected:
            return
        item.select()
        if self._selected:
            self._selected.deselect()
        self._selected = item
        self._detail_for(item)

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

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

    def _get_editor(self, variable):
        editor_type = variable.definition["type"]
        if not self._editors.get(editor_type):
            # we do not have that type of editor yet, create it
            self._editors[editor_type] = editors.get_editor(
                self._detail_pane.body, variable.definition)
        return self._editors[editor_type]

    def refresh(self):
        # redraw variables for current context
        self._variable_pane.body.clear_children()
        has_selection = False
        if not self.variables:
            self._show_overlay(True)
        else:
            self._show_overlay(False)
        for item in self.variables:
            self._show(item)
            if not has_selection:
                self.select(item)
                has_selection = True
        # reapply search query if any
        if self._search_query is not None:
            self.on_search_query(self._search_query)

    def _detail_for(self, variable):
        _editor = self._get_editor(variable)
        if self._editor != _editor:
            # we need to change current editor completely
            if self._editor:
                self._editor.pack_forget()
            self._editor = _editor
        self._editor.set(variable.value)
        self._editor.pack(side="top", fill="x")
        self._editor.on_change(variable.set)

        self.var_name.set(variable.name)
        self.var_name.on_change(variable.set_name)
        self.var_type_lbl["text"] = variable.var_type_name

    def on_session_clear(self):
        self.clear_variables()

    def on_context_switch(self):
        VariableManager.set_context(self.studio.context)
        self.refresh()
コード例 #5
0
ファイル: components.py プロジェクト: ObaraEmmanuel/Formation
class ComponentPane(BaseFeature):
    CLASSES = {
        "native": {"widgets": native.widgets},
        "legacy": {"widgets": legacy.widgets},
    }
    name = "Components"
    _var_init = False
    _defaults = {
        **BaseFeature._defaults,
        "widget_set": "native"
    }
    _custom_pref_path = "studio::custom_widget_paths"

    def __init__(self, master, studio=None, **cnf):
        if not self._var_init:
            self._init_var(studio)
        super().__init__(master, studio, **cnf)

        f = Frame(self, **self.style.surface)
        f.pack(side="top", fill="both", expand=True, pady=4)
        f.pack_propagate(0)

        self._widget_set = Spinner(self._header, width=150)
        self._widget_set.config(**self.style.no_highlight)
        self._widget_set.set_values(list(self.CLASSES.keys()))
        self._widget_set.pack(side="left")
        self._widget_set.on_change(self.collect_groups)
        self._select_pane = ScrolledFrame(f, width=150)
        self._select_pane.place(x=0, y=0, relwidth=0.4, relheight=1)

        self._search_btn = Button(self._header, image=get_icon_image("search", 15, 15), width=25, height=25,
                                  **self.style.button)
        self._search_btn.pack(side="right")
        self._search_btn.on_click(self.start_search)
        self._search_selector = Label(self._select_pane.body, **self.style.text, text="search", anchor="w")
        self._search_selector.configure(**self.style.hover)

        self._widget_pane = ScrolledFrame(f, width=150)
        self._select_pane.body.config(**self.style.surface)
        self._widget_pane.place(relx=0.4, y=0, relwidth=0.6, relheight=1)

        self._pool = {}
        self._selectors = []
        self._selected = None
        self._component_cache = None
        self._extern_groups = []
        self._widget = None
        self.collect_groups(self.get_pref("widget_set"))
        # add custom widgets config to settings
        templates.update(_widget_pref_template)
        self._custom_group = None
        self._custom_widgets = []
        Preferences.acquire().add_listener(self._custom_pref_path, self._init_custom)
        self._reload_custom()

    @property
    def custom_widgets(self):
        return self._custom_widgets

    def auto_find_load_custom(self, *modules):
        # locate and load all custom widgets in modules
        # module can be a module or a path to module file
        self._custom_widgets = []
        errors = {}
        for module in modules:
            if isinstance(module, str):
                try:
                    module = import_path(module)
                except Exception as e:
                    errors[module] = e
                    continue
            for attr in dir(module):
                if type(getattr(module, attr)) == WidgetMeta:
                    self._custom_widgets.append(getattr(module, attr))
        if errors:
            error_msg = "\n\n".join(
                [f"{path}\n{error}" for path, error in errors.items()]
            )
            MessageDialog.show_error(
                parent=self.window,
                message=f"Error loading widgets \n\n{error_msg}"
            )

        return self._custom_widgets

    def _init_custom(self, paths):
        # reload custom widget modules
        try:
            widgets = self.auto_find_load_custom(*paths)
        except Exception as e:

            return

        if not widgets:
            if self._custom_group is not None:
                self.unregister_group(self._custom_group)
                self._custom_group = None
            return

        if self._custom_group is None:
            self._custom_group = self.register_group(
                "Custom",
                widgets,
                ComponentGroup,
            )
        else:
            self._custom_group.update_components(widgets)
            # this will force group to be re-rendered
            self.select(self._custom_group.selector)

    def _reload_custom(self):
        self._init_custom(Preferences.acquire().get(self._custom_pref_path))

    def _init_var(self, master=None):
        self._var_init = True
        for widget_set in self.CLASSES:
            self.CLASSES[widget_set]["var"] = BooleanVar(master, False)

    def _widget_sets_as_menu(self):
        return [
            ("checkbutton",  # Type checkbutton
             i.capitalize(),  # Label as title case
             None,  # Image
             partial(self.collect_groups, i),  # The callback
             {"variable": self.CLASSES[i]["var"]}  # Additional config including the variable associated
             ) for i in self.CLASSES
        ]

    @property
    def selectors(self):
        return self._selectors

    def create_menu(self):
        return (
            (
                "command", "Reload custom widgets",
                get_icon_image("rotate_clockwise", 14, 14), self._reload_custom, {}
            ),
            (
                "command", "Search",
                get_icon_image("search", 14, 14), self.start_search, {}
            ),
            ("cascade", "Widget set", get_icon_image("blank", 14, 14), None, {"menu": (
                *self._widget_sets_as_menu(),
            )}),
        )

    def collect_groups(self, widget_set):
        for other_set in [i for i in self.CLASSES if i != widget_set]:
            self.CLASSES[other_set]["var"].set(False)
        self.CLASSES[widget_set]["var"].set(True)
        self._widget_set.set(widget_set)
        self._select_pane.clear_children()
        self._pool = {}
        components = self.CLASSES.get(widget_set)["widgets"]
        for component in components:
            group = component.group.name
            if group in self._pool:
                self._pool[group].append(Component(self._widget_pane.body, component))
            else:
                self._pool[group] = [Component(self._widget_pane.body, component)]
        self.render_groups()
        # component pool has changed so invalidate the cache
        self._component_cache = None
        self.set_pref("widget_set", widget_set)

    def get_components(self):
        if self._component_cache:
            # cache hit
            return self._component_cache
        # flatten component pool and store to cache
        self._component_cache = [j for i in self._pool.values() for j in i]
        self._component_cache.extend(
            [item for g in self._extern_groups for item in g.components]
        )
        return self._component_cache

    def select(self, selector):
        if self._selected is not None:
            self._selected.deselect()
        selector.select()
        self._selected = selector
        self._widget_pane.clear_children()

        if isinstance(selector.group, ComponentGroup):
            components = selector.group.components
        else:
            components = self._pool[selector.name]

        for component in components:
            component.pack(side="top", pady=2, fill="x")

    def _auto_select(self):
        # automatically pick a selector when no groups have
        # been explicitly selected and the pane is in limbo
        if self._selectors:
            self.select(self._selectors[0])
        else:
            self._widget_pane.clear_children()
            self._selected = None

    def render_groups(self):
        self._selectors = []
        for group in self._pool:
            self.add_selector(Selector(self._select_pane.body, text=group))
        self._auto_select()
        self.render_extern_groups()

    def render_extern_groups(self):
        for group in self._extern_groups:
            if group.supports(self._widget):
                self.add_selector(group.selector)
            else:
                self.remove_selector(group.selector)
                if self._selected == group.selector:
                    self._auto_select()

    def add_selector(self, selector):
        if selector in self._selectors:
            return
        self._selectors.append(selector)
        selector.bind("<Button-1>", lambda *_: self.select(selector))
        selector.pack(side="top", pady=2, fill="x")

    def remove_selector(self, selector):
        if selector in self._selectors:
            self._selectors.remove(selector)
        selector.pack_forget()

    def hide_selectors(self):
        for selector in self._selectors:
            selector.pack_forget()

    def show_selectors(self):
        for selector in self._selectors:
            selector.pack(side="top", pady=2, fill="x")

    def register_group(self, name, items, group_class, evaluator=None, component_class=None):
        group = group_class(self._widget_pane.body, name, items, evaluator, component_class)
        self._extern_groups.append(group)
        # link up selector and group
        group.selector = Selector(self._select_pane.body, text=group.name)
        group.selector.group = group
        self.render_extern_groups()
        return group

    def unregister_group(self, group):
        if group in self._extern_groups:
            self.remove_selector(group.selector)
            self._extern_groups.remove(group)
            self._auto_select()

    def on_select(self, widget):
        self._widget = widget
        self.render_extern_groups()

    def start_search(self, *_):
        super().start_search()
        self._widget_pane.scroll_to_start()
        if self._selected is not None:
            self._selected.deselect()
        self.hide_selectors()
        self._search_selector.pack(side="top", pady=2, fill="x")
        self._widget_pane.clear_children()
        # Display all components by running an empty query
        self.on_search_query("")

    def on_search_clear(self):
        super().on_search_clear()
        if self._selectors:
            self.select(self._selectors[0])
        self._search_selector.pack_forget()
        self.show_selectors()

    def on_search_query(self, query):
        for component in self.get_components():
            if query.lower() in component.component.display_name.lower():
                component.pack(side="top", pady=2, fill="x")
            else:
                component.pack_forget()
コード例 #6
0
class VariablePane(BaseFeature):
    name = "Variablepane"
    icon = "text"

    _defaults = {**BaseFeature._defaults, "side": "right"}

    _definitions = {
        "name": {
            "name": "name",
            "type": "text",
        }
    }

    def __init__(self, master, studio=None, **cnf):
        super().__init__(master, studio, **cnf)
        f = Frame(self, **self.style.dark)
        f.pack(side="top", fill="both", expand=True, pady=4)
        f.pack_propagate(0)

        self._variable_pane = ScrolledFrame(f, width=150)
        self._variable_pane.place(x=0, y=0, relwidth=0.4, relheight=1)

        self._detail_pane = ScrolledFrame(f, width=150)
        self._detail_pane.place(relx=0.4,
                                y=0,
                                relwidth=0.6,
                                relheight=1,
                                x=15,
                                width=-20)

        self._search_btn = Button(self._header,
                                  image=get_icon_image("search", 15, 15),
                                  width=25,
                                  height=25,
                                  **self.style.dark_button)
        self._search_btn.pack(side="right")
        self._search_btn.on_click(self.start_search)
        self._add = MenuButton(self._header, **self.style.dark_button)
        self._add.configure(image=get_icon_image("add", 15, 15))
        self._add.pack(side="right")
        self._delete_btn = Button(self._header,
                                  image=get_icon_image("delete", 15, 15),
                                  width=25,
                                  height=25,
                                  **self.style.dark_button)
        self._delete_btn.pack(side="right")
        self._delete_btn.on_click(self._delete)
        self._var_types_menu = self.make_menu(self._get_add_menu(), self._add)
        self._add.config(menu=self._var_types_menu)
        self._selected = None
        self._links = {}
        self._overlay = Label(f,
                              **self.style.dark_text_passive,
                              text="Add variables",
                              compound="top")
        self._overlay.configure(image=get_icon_image("add", 25, 25))
        self._show_overlay(True)
        self._editors = []

    def start_search(self, *_):
        if len(self.variables):
            super().start_search()
            self._variable_pane.scroll_to_start()

    def on_search_query(self, query):
        matches = []
        self._variable_pane.clear_children()
        for item in self.variables:
            if query in item.name:
                self._show(item)
                matches.append(item)

        if not len(matches):
            self._show_overlay(True,
                               text="No matches found",
                               image=get_icon_image("search", 25, 25))
        else:
            self.select(matches[0])
            self._show_overlay(False)

    def on_search_clear(self):
        self.on_search_query("")
        self._show_overlay(False)
        super().on_search_clear()

    def _get_add_menu(self):
        _types = VariableItem._types
        return [(tk.COMMAND, _types[i].get("name"),
                 get_icon_image(_types[i].get("icon"), 14,
                                14), functools.partial(self.add_var, i), {})
                for i in _types]

    def create_menu(self):
        return (
            ("cascade", "Add", get_icon_image("add", 14, 14), None, {
                "menu": self._get_add_menu()
            }),
            ("command", "Delete", get_icon_image("delete", 14,
                                                 14), self._delete, {}),
            ("command", "Search", get_icon_image("search", 14,
                                                 14), self.start_search, {}),
        )

    def _show_overlay(self, flag=True, **kwargs):
        if flag:
            self._overlay.lift()
            self._overlay.configure(**kwargs)
            self._overlay.place(x=0, y=0, relwidth=1, relheight=1)
        else:
            self._overlay.place_forget()

    def add_var(self, var_type, **kw):
        var = var_type(self.studio)
        item_count = len(
            list(filter(lambda x: x.var_type == var_type, self.variables))) + 1
        name = kw.get('name', f"{var_type.__name__}_{item_count}")
        value = kw.get('value')
        item = VariableItem(self._variable_pane.body, var, name)
        item.bind("<Button-1>", lambda e: self.select(item))
        if value is not None:
            item.set(value)
        VariableManager.add(item)
        self._show(item)
        self._show_overlay(False)
        self.select(item)

    def delete_var(self, var):
        self._hide(var)
        VariableManager.remove(var)

    def _delete(self, *_):
        if self._selected:
            self.delete_var(self._selected)
        if len(self.variables):
            self.select(self.variables[0])
        else:
            self._show_overlay(True)

    def clear_variables(self):
        # the list is likely to change during iteration, create local copy
        variables = list(self.variables)
        for var in variables:
            self.delete_var(var)
        self._show_overlay(True)

    @property
    def variables(self):
        return VariableManager.variables

    def select(self, item):
        if item == self._selected:
            return
        item.select()
        if self._selected:
            self._selected.deselect()
        self._selected = item
        self._detail_for(item)

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

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

    def _detail_for(self, variable):
        self._detail_pane.clear_children()
        Label(self._detail_pane.body,
              **self.style.dark_text_passive,
              text="Type",
              anchor="w").pack(fill="x", side="top")
        Label(self._detail_pane.body,
              **self.style.dark_text,
              text=variable.var_type_name,
              anchor="w").pack(fill="x", side="top")
        Label(self._detail_pane.body,
              **self.style.dark_text_passive,
              text="Name",
              anchor="w").pack(fill="x", side="top")
        name = editors.get_editor(self._detail_pane.body,
                                  self._definitions["name"])
        name.pack(side="top", fill="x")
        name.set(variable.name)
        name.on_change(variable.set_name)
        Label(self._detail_pane.body,
              **self.style.dark_text_passive,
              text="Value",
              anchor="w").pack(fill="x", side="top")
        value = editors.get_editor(self._detail_pane.body, variable.definition)
        value.set(variable.value)
        value.pack(side="top", fill="x")
        value.on_change(variable.set)

    def on_session_clear(self):
        self.clear_variables()
コード例 #7
0
ファイル: updates.py プロジェクト: ObaraEmmanuel/Formation
class Updater(Frame):
    def __init__(self, master):
        super().__init__(master)
        self.config(**self.style.surface)
        self._progress = ProgressBar(self)
        self._progress.pack(side="top", fill="x", padx=20, pady=20)
        self._progress_text = Label(self, **self.style.text_small, anchor="w")
        self._progress_text.pack(side="top", fill="x", padx=20, pady=10)
        self._message = Label(
            self,
            **self.style.text,
            anchor="w",
            compound="left",
            wrap=400,
            justify="left",
            pady=5,
            padx=5,
        )
        self._action_btn = Button(self,
                                  text="Retry",
                                  **self.style.button_highlight,
                                  width=80,
                                  height=25)
        self.extra_info = Text(self,
                               width=40,
                               height=6,
                               state='disabled',
                               font='consolas 10')
        self.pack(fill="both", expand=True)
        self.check_for_update()

    def show_button(self, text, func):
        self._action_btn.config(text=text)
        self._action_btn.on_click(func)
        self._action_btn.pack(side="bottom", anchor="e", pady=5, padx=5)

    def show_progress(self, message):
        self.clear_children()
        self._progress_text.configure(text=message)
        self._progress.pack(side="top", fill="x", padx=20, pady=20)
        self._progress_text.pack(side="top", fill="x", padx=20, pady=10)

    def show_error(self, message, retry_func):
        self.show_error_plain(message)
        self.show_button("Retry", retry_func)

    def show_error_plain(self, message):
        self.show_message(message, MessageDialog.ICON_ERROR)

    def update_found(self, version):
        self.show_info(
            f"New version formation-studio {version} found. Do you want to install?"
        )
        self.show_button("Install", lambda _: self.install(version))

    def show_info(self, message):
        self.show_message(message, MessageDialog.ICON_INFO)

    def show_message(self, message, icon):
        self.clear_children()
        self._message.configure(text=message,
                                image=get_icon_image(icon, 50, 50))
        self._message.pack(side="top", padx=20, pady=10)

    def install(self, version):
        self.show_progress(f"Updating to formation-studio {version}")
        self.upgrade(version)

    @classmethod
    def check(cls, master):
        dialog = MessageDialog(master, cls)
        dialog.title("Formation updater")
        dialog.focus_set()
        return dialog

    @as_thread
    def check_for_update(self, *_):
        self._progress.mode(ProgressBar.INDETERMINATE)
        self.show_progress("Checking for updates ...")
        try:
            content = urlopen(
                "https://pypi.org/pypi/formation-studio/json").read()
            data = json.loads(content)
            ver = data["info"]["version"]
            if ver <= formation.__version__:
                self.show_info("You are using the latest version")
            else:
                self.update_found(ver)
        except URLError:
            self.show_error(
                "Failed to connect. Check your internet connection"
                " and try again.", self.check_for_update)

    @as_thread
    def upgrade(self, version):
        try:
            # run formation cli upgrade command
            proc_info = subprocess.run([sys.executable, "-m", "studio", "-u"],
                                       capture_output=True)
            if proc_info.returncode != 0 or proc_info.stderr:
                self.show_error_plain(
                    "Something went wrong. Failed to upgrade formation-studio"
                    f" to version {version} ,"
                    f" Exited with code: {proc_info.returncode}")
                if proc_info.stderr:
                    self.extra_info.config(state="normal")
                    self.extra_info.pack(side="top",
                                         fill="x",
                                         padx=20,
                                         pady=10)
                    self.extra_info.clear()
                    self.extra_info.set(str(proc_info.stderr))
                    self.extra_info.config(state="disabled")
            else:
                self.show_info(
                    "Upgrade successful. Restart to complete installation")
                self.show_button(
                    "Restart",
                    lambda _: get_routine("STUDIO_RESTART").invoke())
                return
        except Exception as e:
            self.show_error_plain(e)

        self.show_button("Retry", lambda _: self.install(version))