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 = {}
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()
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()
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 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