Exemple #1
0
 def __init__(self, master):
     super().__init__(master)
     self._restoration_data = {}
     self._highlighter = WidgetHighlighter(self.container.parent)
     self._edge_indicator = EdgeIndicator(self.container.parent)
     self._temp = {}
Exemple #2
0
class GridLayoutStrategy(BaseLayoutStrategy):
    name = 'GridLayout'
    icon = "grid"
    manager = "grid"
    EXPAND = 0x1
    CONTRACT = 0X2

    DEFINITION = {
        **BaseLayoutStrategy.DEFINITION,
        **COMMON_DEFINITION,
        "sticky": {
            "display_name": "sticky",
            "type": "anchor",
            "multiple": True,
            "name": "sticky",
            "default": ''
        },
        "row": {
            "display_name": "row",
            "type": "number",
            "name": "row",
            "default": None,
        },
        "column": {
            "display_name": "column",
            "type": "number",
            "name": "column",
            "default": None,
        },
        "columnspan": {
            "display_name": "column span",
            "type": "number",
            "name": "columnspan",
            "default": 1
        },
        "rowspan": {
            "display_name": "row span",
            "type": "number",
            "name": "rowspan",
            "default": 1
        },
    }

    def __init__(self, master):
        super().__init__(master)
        self._restoration_data = {}
        self._highlighter = WidgetHighlighter(self.container.parent)
        self._edge_indicator = EdgeIndicator(self.container.parent)
        self._temp = {}

    def get_restore(self, widget):
        return widget.grid_info()

    def restore_widget(self, widget, data=None):
        restoration_data = self._restoration_data.get(
            widget) if data is None else data
        self.children.append(widget)
        widget.level = self.level + 1
        widget.layout = self.container
        widget.grid(**restoration_data)

    def react_to(self, bounds):
        bounds = geometry.relative_bounds(bounds, self.container)
        col, row = self.container.grid_location(bounds[0], bounds[1])
        widget = self.container.grid_slaves(row, col)
        if len(widget):
            self._highlighter.highlight(widget[0])

    def _redraw_widget(self, widget):
        widget.grid(**self._grid_info(widget))

    def _redraw(self, row, column, row_shift, column_shift):
        for child in self.container.grid_slaves():
            info = child.grid_info()
            if info['column'] >= column:
                child.grid_configure(column=info["column"] + column_shift)
        for child in self.container.grid_slaves(None, column):
            info = child.grid_info()
            if info["row"] >= row:
                child.grid_configure(row=info["row"] + row_shift)

    def _adjust_rows(self, from_row=0):
        rows = self.container.grid_size()[1]
        for row in range(from_row, rows):
            if not len(self.container.grid_slaves(row)):
                for child in self.container.grid_slaves(row + 1):
                    info = child.grid_info()
                    child.grid_configure(row=info["row"] - 1)

    def _adjust_columns(self, from_col):
        cols = self.container.grid_size()[1]
        for col in range(from_col, cols):
            if not len(self.container.grid_slaves(None, col)):
                for child in self.container.grid_slaves(None, col + 1):
                    info = child.grid_info()
                    child.grid_configure(column=info["column"] - 1)

    def remove_widget(self, widget):
        super().remove_widget(widget)
        info = widget.grid_info()
        self.clear_indicators()
        if not info:
            return
        row, col, row_span, col_span = info["row"], info["column"], info[
            "rowspan"], info["columnspan"]
        widget.grid_forget()
        if not len(self.container.grid_slaves(row)):
            self._adjust_rows(from_row=row)
        if not len(self.container.grid_slaves(None, col)):
            self._adjust_columns(from_col=col)

    def _grid_info(self, widget):
        info = widget.grid_info()
        if info:
            return info
        else:
            info = self._temp.get(widget, {})
            info.update({"in_": self.container})
            return info

    def config_widget(self, widget, **kw):
        for prop in ("width", "height"):
            if prop in kw:
                widget.configure(**{prop: kw[prop]})
                kw.pop(prop)
        widget.grid_configure(**kw)

    def widget_released(self, widget):
        self._redraw_widget(widget)
        self._temp = None
        self.clear_indicators()

    def resize_widget(self, widget, bounds):
        if not self._temp:
            self._temp = self._grid_info(widget)
        self._move(widget, bounds)

    def _move(self, widget, bounds):
        super()._move(widget, bounds)
        self._location_analysis(bounds)

    def add_widget(self, widget, bounds=None, **kwargs):
        super().remove_widget(widget)
        super().add_widget(widget, bounds, **kwargs)
        if bounds is not None:
            row, col, row_shift, column_shift = self._location_analysis(bounds)
            self._redraw(max(0, row), max(0, col), row_shift, column_shift)
            kwargs.update({
                'in_': self.container,
                'row': max(0, row),
                'column': max(0, col)
            })
            widget.grid(**kwargs)
        else:
            widget.grid(in_=self.container)
            self.config_widget(widget, **kwargs)
        self.children.append(widget)
        self.clear_indicators()

    def _widget_at(self, row, column):
        return self.container.grid_slaves(column, row)

    def _location_analysis(self, bounds):
        self.clear_indicators()
        self._edge_indicator.update_idletasks()
        bounds = geometry.relative_bounds(bounds, self.container)
        x, y = bounds[0], bounds[1]
        col, row = self.container.grid_location(x, y)
        x, y = geometry.upscale_bounds(bounds, self.container)[:2]
        slaves = self.container.grid_slaves(max(0, row), max(0, col))
        if len(slaves) == 0:
            self.container.update_idletasks()
            bbox = self.container.grid_bbox(col, row)
            bounds = *bbox[:2], bbox[0] + bbox[2], bbox[1] + bbox[3]
            # Make bounds relative to designer
            bounds = geometry.upscale_bounds(bounds, self.container)
        else:
            bounds = geometry.bounds(slaves[0])
        y_offset, x_offset = 10, 10  # 0.15*(bounds[3] - bounds[1]), 0.15*(bounds[2] - bounds[0])
        # If the position is empty no need to alter the row or column
        resize = 1 if len(slaves) else 0
        if y - bounds[1] < y_offset:
            self._edge_indicator.top(bounds)
            return row, col, resize, 0
        elif bounds[3] - y < y_offset:
            self._edge_indicator.bottom(bounds)
            return row + resize, col, resize, 0
        elif x - bounds[0] < x_offset:
            self._edge_indicator.left(bounds)
            return row, col, 0, resize
        elif bounds[2] - x < x_offset:
            self._edge_indicator.right(bounds)
            return row, col + resize, 0, resize
        else:
            self._highlighter.highlight_bounds(bounds)
            return row, col, 0, 0

    def clear_indicators(self):
        self._highlighter.clear()
        self._edge_indicator.clear()

    def apply(self, prop, value, widget):
        if prop in ("width", "height"):
            widget.configure(**{prop: value})
        else:
            widget.grid_configure(**{prop: value})

    def react_to_pos(self, x, y):
        self._location_analysis((*geometry.resolve_position(
            (x, y), self.container.parent), 0, 0))

    def info(self, widget):
        info = widget.grid_info() or {}
        keys = widget.keys()
        for prop in ("width", "height"):
            if prop in keys:
                info.update({prop: widget[prop]})
        return info

    def get_def(self, widget):
        # Use copy since we are going to modify definition
        definition = dict(self.DEFINITION)
        keys = widget.keys()
        for prop in ("width", "height"):
            if prop not in keys:
                definition.pop(prop)
        return definition

    def copy_layout(self, widget, from_):
        info = from_.grid_info()
        info["in_"] = self.container
        widget.grid(**info)
        self.children.append(widget)
        super().add_widget(widget, (0, 0, 0, 0))

    def clear_all(self):
        # remove the children but still maintain them in the children list
        for child in self.children:
            child.grid_forget()
Exemple #3
0
class MalleableTree(TreeView):
    """
    Sub class of TreeView that allows rearrangement of Nodes which useful in repositioning components in the
    various studio features. For any tree view that allows rearrangement, subclass MalleableTree.
    """
    drag_components = []  # All objects that were selected when dragging began
    drag_active = False  # Flag showing whether we are currently dragging stuff
    drag_popup = None  # The almost transparent window that shows what is being dragged
    drag_highlight = None  # The widget that currently contains the rectangular highlight
    drag_select = None  # The node where all events go when button is released ending drag
    drag_display_limit = 3  # The maximum number of items the drag popup can display
    drag_instance = None  # The current tree that is performing a drag

    class Node(TreeView.Node):
        PADDING = 0

        def __init__(self, master=None, **config):
            # Master is always a TreeView object unless you tamper with the add_as_node method
            super().__init__(master, **config)
            # If set tp False the node accepts children and vice versa
            self._is_terminal = config.get("terminal", True)
            self.strip.bind_all("<Motion>", self.begin_drag)
            # use add='+' to avoid overriding the default event which selects nodes
            self.strip.bind_all("<ButtonRelease-1>", self.end_drag, add='+')
            self.strip.config(**self.style.dark_highlight)  # The highlight on a normal day
            self._on_structure_change = None
            self.editable = False
            self.configuration = config

        def on_structure_change(self, callback, *args, **kwargs):
            self._on_structure_change = lambda: callback(*args, **kwargs)

        def _change_structure(self):
            if self._on_structure_change:
                self._on_structure_change()
            self.tree._structure_changed()

        # noinspection PyProtectedMember
        def begin_drag(self, event):
            if not self.editable:
                return
            # If cursor is moved while holding the left button down for the first time we begin drag
            if event.state & EventMask.MOUSE_BUTTON_1 and not MalleableTree.drag_active and \
                    self.tree.selected_count():
                MalleableTree.drag_popup = DragWindow(self.window).set_position(event.x_root, event.y_root + 20)
                MalleableTree.drag_components = self.tree._selected
                MalleableTree.drag_instance = self.tree
                count = 0
                for component in MalleableTree.drag_components:
                    # Display all items upto the drag_display_limit
                    if count == MalleableTree.drag_display_limit:
                        overflow = len(MalleableTree.drag_components) - count
                        # Display the overflow information
                        Label(MalleableTree.drag_popup,
                              text=f"and {overflow} other{'' if overflow == 1 else 's'}...", anchor='w',
                              **self.style.dark_text).pack(side="top", fill="x")
                        break
                    Label(MalleableTree.drag_popup,
                          text=component.name, anchor='w',
                          **self.style.dark_text).pack(side="top", fill="x")
                    count += 1
                MalleableTree.drag_active = True
            elif MalleableTree.drag_active:
                widget = self.winfo_containing(event.x_root, event.y_root)
                # The widget can be a child to Node but not necessarily a node but we need a node so
                # Resolve the node that is immediately under the cursor position by iteratively getting widget's parent
                # For the sake of performance not more than 4 iterations
                limit = 4
                while not isinstance(widget, self.__class__):
                    if widget is None:
                        # This happens when someone hovers outside the current top level window
                        break
                    widget = self.nametowidget(widget.winfo_parent())
                    limit -= 1
                    if not limit:
                        break
                tree = self.event_first(event, self.tree, MalleableTree)

                if isinstance(widget, MalleableTree.Node):
                    # We can only react if we have resolved the widget to a Node object
                    widget.react(event)
                    # Store the currently reacting widget so we can apply actions to it on ButtonRelease/ drag_end
                    MalleableTree.drag_select = widget
                elif isinstance(tree, self.tree.__class__):
                    # if the tree found is compatible to the current tree i.e belongs to same class or is subclass of
                    # disallow incompatible trees from interacting as this may cause errors
                    tree.react(event)
                    MalleableTree.drag_select = tree
                else:
                    # No viable node found on resolution so clear all highlights and indicators
                    if MalleableTree.drag_select:
                        MalleableTree.drag_select.clear_indicators()
                    MalleableTree.drag_select = None

                MalleableTree.drag_popup.set_position(event.x_root, event.y_root + 20)

        def end_drag(self, event):
            # Dragging is complete so we make the necessary insertions and repositions
            node = MalleableTree.drag_select
            if MalleableTree.drag_active:
                if MalleableTree.drag_select is not None:
                    action = node.react(event)
                    if action == 0:
                        node.insert_before(*MalleableTree.drag_components)
                    elif action == 1:
                        node.insert(None, *MalleableTree.drag_components)
                    elif action == 2:
                        node.insert_after(*MalleableTree.drag_components)
                    # else there is no viable action to take.
                    if action in (0, 1, 2):
                        # These actions means tree structure changed
                        self._change_structure()
                # Reset all drag related attributes
                MalleableTree.drag_select.clear_indicators()
                MalleableTree.drag_popup.destroy()  # remove the drag popup window
                MalleableTree.drag_popup = None
                MalleableTree.drag_active = False
                MalleableTree.drag_components = []
                MalleableTree.drag_instance = None
                self.clear_indicators()
                MalleableTree.drag_highlight = None

        def highlight(self):
            MalleableTree.drag_highlight = self
            self.strip.config(**self.style.bright_highlight)

        def react(self, event) -> int:
            # Checks, based on the cursor position whether we can insert before, into or after the node
            # Returns 0, 1 or 2 respectively
            # It is mostly with respect to the nodes head element known as the strip except for --- case * --- below
            self.clear_indicators()
            # The cursor is at the top edge of the node so we can attempt to insert before it
            if event.y_root < self.strip.winfo_rooty() + 5:
                self.tree.edge_indicator.top(upscale_bounds(bounds(self.strip), self))
                return 0
            # The cursor is at the center of the node so we can attempt a direct insert into the node
            elif self.strip.winfo_rooty() + 5 < event.y_root < self.strip.winfo_rooty() + self.strip.height - 5:
                if not self._is_terminal:
                    # If node is terminal then id does not support children and consequently insertion
                    self.highlight()
                    return 1
            # The cursor is at the bottom edge of the node so we attempt to insert immediately after the node
            elif self._expanded:  # --- Case * ---
                # If the node is expanded we would want to edge indicate at the very bottom after its last child
                if event.y_root > self.winfo_rooty() + self.height - 5:
                    self.tree.edge_indicator.bottom(bounds(self))
                    return 2
            else:
                self.tree.edge_indicator.bottom(upscale_bounds(bounds(self.strip), self))
                return 2

        def clear_highlight(self):
            # Remove the rectangular highlight around the node
            self.strip.configure(**self.style.dark_highlight)

        def clear_indicators(self):
            # Remove any remaining node highlights and edge indicators
            if MalleableTree.drag_highlight is not None:
                MalleableTree.drag_highlight.clear_highlight()
            self.tree.edge_indicator.clear()

        @property
        def is_terminal(self):
            return self._is_terminal

        @is_terminal.setter
        def is_terminal(self, value):
            self._is_terminal = value

        def insert(self, index=None, *nodes):
            # if dragging to new tree copy to new location
            # only do this during drags, i.e drag_active is True
            if MalleableTree.drag_active and MalleableTree.drag_instance != self.tree:
                # clone to new parent tree
                # the node will still be retained in the former tree
                nodes = [node.clone(self.tree) for node in nodes]
                self.clear_indicators()
            super().insert(index, *nodes)
            return nodes

        def clone(self, parent):
            #  Generic cloning that replicates node using config provided on creation
            #  Override to define attributes that may have changed
            node = self.__class__(parent, **self.configuration)
            node.parent_node = self.parent_node
            for sub_node in self.nodes:
                sub_node_clone = sub_node.clone(parent)
                node.insert(None, sub_node_clone)
            return node

    def __init__(self, master, **config):
        super().__init__(master, **config)
        self._on_structure_change = None
        self.is_terminal = False
        self.edge_indicator = EdgeIndicator(self)  # A line that shows where an insertion can occur

    def on_structure_change(self, callback, *args, **kwargs):
        self._on_structure_change = lambda: callback(*args, **kwargs)

    def _structure_changed(self):
        if self._on_structure_change:
            self._on_structure_change()

    def insert(self, index=None, *nodes):
        # if dragging to new tree clone nodes to new location
        # only do this during drags, i.e drag_active is True
        if MalleableTree.drag_active and MalleableTree.drag_instance != self:
            # clone to new parent tree
            # the node will still be retained in the former tree
            nodes = [node.clone(self) for node in nodes]
            self.edge_indicator.clear()
        super().insert(index, *nodes)
        # Return the nodes just in case they have been cloned and new references are required
        return nodes

    def react(self, *_):
        self.clear_indicators()
        self.highlight()
        # always perform a direct insert hence return 1
        return 1

    def highlight(self):
        MalleableTree.drag_highlight = self
        self.config(**self.style.bright_highlight)

    def clear_highlight(self):
        # Remove the rectangular highlight around the node
        self.configure(**self.style.dark_highlight)

    def clear_indicators(self):
        # Remove any remaining node highlights and edge indicators
        if MalleableTree.drag_highlight is not None:
            MalleableTree.drag_highlight.clear_highlight()
            self.edge_indicator.clear()
Exemple #4
0
 def __init__(self, master, **config):
     super().__init__(master, **config)
     self._on_structure_change = None
     self.is_terminal = False
     self.edge_indicator = EdgeIndicator(self)  # A line that shows where an insertion can occur
Exemple #5
0
 def initialize_tree(self):
     super(MalleableTree, self).initialize_tree()
     self._on_structure_change = None
     self.is_terminal = False
     self.edge_indicator = EdgeIndicator(
         self.get_body())  # A line that shows where an insertion can occur