Palette: drag onto the tree to add inside a component; hover opens branches
A palette drag dropped on a tree row adds the component inside that row's component, placed under its lowest child. The row under the pointer is highlighted, and resting on a collapsed branch opens it, loading it from the device when live without moving the selection or the open sheet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "0.0.0.154"
|
VERSION = "0.0.0.156"
|
||||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
@@ -54,6 +54,9 @@ CLEAN_TINT = "white"
|
|||||||
DIRTY_NAME_COLOUR = "#c00000"
|
DIRTY_NAME_COLOUR = "#c00000"
|
||||||
DIRTY_NAME_TAG = "unsaved_file"
|
DIRTY_NAME_TAG = "unsaved_file"
|
||||||
|
|
||||||
|
# Marks the tree row a palette drag would drop into.
|
||||||
|
TREE_DROP_TAG = "palette_drop_target"
|
||||||
|
|
||||||
# A themed Treeview maps the selected state to SystemHighlightText, and that mapping beats
|
# A themed Treeview maps the selected state to SystemHighlightText, and that mapping beats
|
||||||
# a row tag's own foreground -- so the file name turned white-on-blue the moment its row
|
# a row tag's own foreground -- so the file name turned white-on-blue the moment its row
|
||||||
# was selected, which is exactly when the root sheet is the one being looked at. The
|
# was selected, which is exactly when the root sheet is the one being looked at. The
|
||||||
|
|||||||
@@ -2655,8 +2655,12 @@ def _start_cov_polling():
|
|||||||
_cov_poll_id = app_state.window.after(100, poll)
|
_cov_poll_id = app_state.window.after(100, poll)
|
||||||
|
|
||||||
|
|
||||||
def _live_tree_expand(comp_id, tree_item_id):
|
def _live_tree_expand(comp_id, tree_item_id, select=True, on_loaded=None):
|
||||||
"""Called when user expands a tree node in live mode to lazy-load children."""
|
"""Called when user expands a tree node in live mode to lazy-load children.
|
||||||
|
|
||||||
|
select=False loads without moving the selection or the open sheet, as a palette drag
|
||||||
|
hovering over a branch does. on_loaded gets the rebuilt row of the expanded node.
|
||||||
|
"""
|
||||||
ds = app_state.device_session
|
ds = app_state.device_session
|
||||||
if ds is None or not ds.is_connected:
|
if ds is None or not ds.is_connected:
|
||||||
return
|
return
|
||||||
@@ -2681,14 +2685,20 @@ def _live_tree_expand(comp_id, tree_item_id):
|
|||||||
# Re-open and re-select the node without triggering a wiresheet
|
# Re-open and re-select the node without triggering a wiresheet
|
||||||
# re-render — the sheet is already showing the right content and
|
# re-render — the sheet is already showing the right content and
|
||||||
# a redundant close_wiresheet + open_wiresheet cycle can lose links.
|
# a redundant close_wiresheet + open_wiresheet cycle can lose links.
|
||||||
|
if not select:
|
||||||
|
_rebind_open_sheet()
|
||||||
for iid, elem in app_state.tree_element_map.items():
|
for iid, elem in app_state.tree_element_map.items():
|
||||||
if elem.get("id") == str(comp_id):
|
if elem.get("id") == str(comp_id):
|
||||||
app_state.current_sheet_parent = elem
|
|
||||||
_restoring_selection = True
|
|
||||||
tree.item(iid, open=True)
|
tree.item(iid, open=True)
|
||||||
tree.selection_set(iid)
|
if select:
|
||||||
tree.see(iid)
|
app_state.current_sheet_parent = elem
|
||||||
tree.after_idle(_clear_restoring_flag)
|
_restoring_selection = True
|
||||||
|
tree.selection_set(iid)
|
||||||
|
tree.see(iid)
|
||||||
|
tree.after_idle(_clear_restoring_flag)
|
||||||
|
tree.after_idle(fit_tree_column, tree)
|
||||||
|
if on_loaded is not None:
|
||||||
|
on_loaded(iid)
|
||||||
break
|
break
|
||||||
app_state.set_status(f"Loaded {len(children)} children")
|
app_state.set_status(f"Loaded {len(children)} children")
|
||||||
|
|
||||||
@@ -2701,6 +2711,25 @@ def _live_tree_expand(comp_id, tree_item_id):
|
|||||||
threading.Thread(target=_bg, daemon=True).start()
|
threading.Thread(target=_bg, daemon=True).start()
|
||||||
|
|
||||||
|
|
||||||
|
def _rebind_open_sheet():
|
||||||
|
"""Points the open sheet at its element in a freshly built DOM and repaints it.
|
||||||
|
|
||||||
|
A lazy load replaces the whole DOM. The sheet on screen still holds elements of the
|
||||||
|
old one, and edits made through them would land in a document nobody saves.
|
||||||
|
"""
|
||||||
|
old_parent = app_state.current_sheet_parent
|
||||||
|
if old_parent is None:
|
||||||
|
wiresheet.render_current_sheet()
|
||||||
|
return
|
||||||
|
old_id = old_parent.get("id")
|
||||||
|
app_element = app_state.xml_root_element.find("app")
|
||||||
|
app_state.current_sheet_parent = next(
|
||||||
|
(elem for elem in app_state.xml_root_element.iter()
|
||||||
|
if elem.tag in ("app", "comp") and old_id is not None and elem.get("id") == old_id),
|
||||||
|
app_element)
|
||||||
|
wiresheet.render_current_sheet()
|
||||||
|
|
||||||
|
|
||||||
# --- UI Setup ---
|
# --- UI Setup ---
|
||||||
window = tk.Tk()
|
window = tk.Tk()
|
||||||
app_state.window = window
|
app_state.window = window
|
||||||
@@ -2933,6 +2962,7 @@ tree = ttk.Treeview(
|
|||||||
)
|
)
|
||||||
# Configured once, for the life of the tree. Only the row's tags change after this.
|
# Configured once, for the life of the tree. Only the row's tags change after this.
|
||||||
tree.tag_configure(app_state.DIRTY_NAME_TAG, foreground=app_state.DIRTY_NAME_COLOUR)
|
tree.tag_configure(app_state.DIRTY_NAME_TAG, foreground=app_state.DIRTY_NAME_COLOUR)
|
||||||
|
tree.tag_configure(app_state.TREE_DROP_TAG, background=app_state.NAV_SELECTION_BG)
|
||||||
app_state.tree = tree
|
app_state.tree = tree
|
||||||
attach_tree_scrollbars(frame_tree, tree)
|
attach_tree_scrollbars(frame_tree, tree)
|
||||||
tree.bind("<<TreeviewSelect>>", on_tree_select)
|
tree.bind("<<TreeviewSelect>>", on_tree_select)
|
||||||
@@ -3039,10 +3069,15 @@ def refresh_palette_kits():
|
|||||||
# --- Palette drag and drop ---
|
# --- Palette drag and drop ---
|
||||||
# Tk has no drag-and-drop between widgets, so the gesture is assembled by hand:
|
# Tk has no drag-and-drop between widgets, so the gesture is assembled by hand:
|
||||||
# remember what was pressed, start dragging past a small threshold, follow the pointer
|
# remember what was pressed, start dragging past a small threshold, follow the pointer
|
||||||
# with a borderless ghost window, and on release test the pointer against the canvas.
|
# with a borderless ghost window, and on release test the pointer against the canvas
|
||||||
|
# and the navigation tree. A drop on a tree row puts the component inside that row's
|
||||||
|
# component, which need not be the sheet that is open. Resting on a collapsed branch
|
||||||
|
# opens it (loading it from the device when live), so a drop can reach any depth.
|
||||||
DRAG_THRESHOLD = 6
|
DRAG_THRESHOLD = 6
|
||||||
|
HOVER_EXPAND_MS = 700
|
||||||
|
|
||||||
palette_drag = {"kit": None, "type": None, "origin": None, "active": False, "ghost": None}
|
palette_drag = {"kit": None, "type": None, "origin": None, "active": False, "ghost": None,
|
||||||
|
"tree_row": None, "expand_job": None}
|
||||||
|
|
||||||
|
|
||||||
def _destroy_drag_ghost():
|
def _destroy_drag_ghost():
|
||||||
@@ -3051,8 +3086,54 @@ def _destroy_drag_ghost():
|
|||||||
palette_drag["ghost"] = None
|
palette_drag["ghost"] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_tree_drop_row(row):
|
||||||
|
"""Highlights the tree row a drop would land in; None clears the highlight.
|
||||||
|
|
||||||
|
Done with a tag rather than the selection, since selecting a row opens its sheet.
|
||||||
|
"""
|
||||||
|
previous = palette_drag["tree_row"]
|
||||||
|
if previous == row:
|
||||||
|
return
|
||||||
|
if previous is not None and tree.exists(previous):
|
||||||
|
tags = [t for t in tree.item(previous, "tags") if t != app_state.TREE_DROP_TAG]
|
||||||
|
tree.item(previous, tags=tags)
|
||||||
|
if row is not None:
|
||||||
|
tree.item(row, tags=list(tree.item(row, "tags")) + [app_state.TREE_DROP_TAG])
|
||||||
|
palette_drag["tree_row"] = row
|
||||||
|
|
||||||
|
_cancel_hover_expand()
|
||||||
|
if row is not None and tree.get_children(row) and not tree.item(row, "open"):
|
||||||
|
palette_drag["expand_job"] = window.after(HOVER_EXPAND_MS, _hover_expand, row)
|
||||||
|
|
||||||
|
|
||||||
|
def _cancel_hover_expand():
|
||||||
|
if palette_drag["expand_job"] is not None:
|
||||||
|
window.after_cancel(palette_drag["expand_job"])
|
||||||
|
palette_drag["expand_job"] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _hover_expand(row):
|
||||||
|
"""Opens the branch a palette drag has rested on, loading it first on a device."""
|
||||||
|
palette_drag["expand_job"] = None
|
||||||
|
if not palette_drag["active"] or palette_drag["tree_row"] != row or not tree.exists(row):
|
||||||
|
return
|
||||||
|
|
||||||
|
tree.item(row, open=True) # in code, so no <<TreeviewOpen>>; loaded here instead
|
||||||
|
tree.after_idle(fit_tree_column, tree)
|
||||||
|
elem = app_state.tree_element_map.get(row)
|
||||||
|
comp_id = elem.get("id") if elem is not None else None
|
||||||
|
if app_state.live_mode and comp_id is not None:
|
||||||
|
# The load rebuilds the tree, so the highlight goes back on the new row for it.
|
||||||
|
def relight(new_row):
|
||||||
|
if palette_drag["active"] and palette_drag["tree_row"] == row:
|
||||||
|
_mark_tree_drop_row(new_row)
|
||||||
|
_live_tree_expand(int(comp_id), row, select=False, on_loaded=relight)
|
||||||
|
|
||||||
|
|
||||||
def _reset_palette_drag():
|
def _reset_palette_drag():
|
||||||
_destroy_drag_ghost()
|
_destroy_drag_ghost()
|
||||||
|
_mark_tree_drop_row(None)
|
||||||
|
_cancel_hover_expand()
|
||||||
palette_drag.update(kit=None, type=None, origin=None, active=False)
|
palette_drag.update(kit=None, type=None, origin=None, active=False)
|
||||||
|
|
||||||
|
|
||||||
@@ -3100,12 +3181,45 @@ def on_palette_motion(event):
|
|||||||
if palette_drag["ghost"] is not None:
|
if palette_drag["ghost"] is not None:
|
||||||
palette_drag["ghost"].geometry(f"+{event.x_root + 12}+{event.y_root + 10}")
|
palette_drag["ghost"].geometry(f"+{event.x_root + 12}+{event.y_root + 10}")
|
||||||
|
|
||||||
|
_mark_tree_drop_row(_tree_row_under_pointer(event.x_root, event.y_root))
|
||||||
|
|
||||||
|
|
||||||
|
def _pointer_over_widget(widget, x_root, y_root):
|
||||||
|
left = widget.winfo_rootx()
|
||||||
|
top = widget.winfo_rooty()
|
||||||
|
return (left <= x_root < left + widget.winfo_width()
|
||||||
|
and top <= y_root < top + widget.winfo_height())
|
||||||
|
|
||||||
|
|
||||||
def _pointer_over_canvas(x_root, y_root):
|
def _pointer_over_canvas(x_root, y_root):
|
||||||
left = canvas.winfo_rootx()
|
return _pointer_over_widget(canvas, x_root, y_root)
|
||||||
top = canvas.winfo_rooty()
|
|
||||||
return (left <= x_root < left + canvas.winfo_width()
|
|
||||||
and top <= y_root < top + canvas.winfo_height())
|
def _tree_row_under_pointer(x_root, y_root):
|
||||||
|
"""The navigation tree row under the pointer, or None off the tree or between rows."""
|
||||||
|
if app_state.xml_root_element is None or not _pointer_over_widget(tree, x_root, y_root):
|
||||||
|
return None
|
||||||
|
row = tree.identify_row(y_root - tree.winfo_rooty())
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
elem = app_state.tree_element_map.get(row)
|
||||||
|
if elem is not None and elem.get("type") == "":
|
||||||
|
return None # a "..." stand-in for a child not loaded from the device
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_on_tree_row(kit_name, type_name, row):
|
||||||
|
"""Adds kit::type inside the component a tree row stands for."""
|
||||||
|
parent = app_state.tree_element_map.get(row)
|
||||||
|
if parent is None and tree.parent(row) == "":
|
||||||
|
parent = app_state.xml_root_element.find("app") # the root row is the app
|
||||||
|
if parent is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Opened first, so the rebuild after the add keeps it open and the new row shows.
|
||||||
|
tree.item(row, open=True)
|
||||||
|
grid_x, grid_y = sax_file.grid_position_below_children(parent)
|
||||||
|
sax_file.add_component(kit_name, type_name, grid_x, grid_y, parent=parent)
|
||||||
|
|
||||||
|
|
||||||
def on_palette_release(event):
|
def on_palette_release(event):
|
||||||
@@ -3117,6 +3231,11 @@ def on_palette_release(event):
|
|||||||
if not was_dragging or not kit_name or not type_name:
|
if not was_dragging or not kit_name or not type_name:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
tree_row = _tree_row_under_pointer(event.x_root, event.y_root)
|
||||||
|
if tree_row is not None:
|
||||||
|
_drop_on_tree_row(kit_name, type_name, tree_row)
|
||||||
|
return
|
||||||
|
|
||||||
if not _pointer_over_canvas(event.x_root, event.y_root):
|
if not _pointer_over_canvas(event.x_root, event.y_root):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
+33
-3
@@ -16,6 +16,7 @@ from sax_dom import (
|
|||||||
find_component_by_path,
|
find_component_by_path,
|
||||||
find_parent_element,
|
find_parent_element,
|
||||||
get_component_path,
|
get_component_path,
|
||||||
|
read_meta_property,
|
||||||
restore_children,
|
restore_children,
|
||||||
snapshot_children,
|
snapshot_children,
|
||||||
update_meta_property,
|
update_meta_property,
|
||||||
@@ -936,9 +937,38 @@ def build_config_values(full_type, overrides=None, device_values=None):
|
|||||||
return config_values
|
return config_values
|
||||||
|
|
||||||
|
|
||||||
def add_component(kit_name, type_name, grid_x, grid_y):
|
def grid_position_below_children(parent):
|
||||||
"""Places a new component of kit::type at a grid position. Returns the element."""
|
"""A spot for a component added without a drop point: under the parent's lowest child.
|
||||||
parent = current_sheet_container()
|
|
||||||
|
Used when a component is placed from the tree, where there is no point on the sheet
|
||||||
|
to drop it at. Lining it up on the left margin below everything already there keeps
|
||||||
|
it clear of the existing boxes in the common case; Tidy sorts out the rest.
|
||||||
|
"""
|
||||||
|
min_step = wiresheet.layout_margin(app_state.meta_scale) // app_state.meta_scale
|
||||||
|
lowest = None
|
||||||
|
for child in parent:
|
||||||
|
if child.tag != "comp":
|
||||||
|
continue
|
||||||
|
meta_prop = child.find("prop[@name='meta']")
|
||||||
|
y = read_meta_property(meta_prop.get("val") if meta_prop is not None else None)[1]
|
||||||
|
lowest = y if lowest is None else max(lowest, y)
|
||||||
|
|
||||||
|
if lowest is None:
|
||||||
|
return min_step, min_step
|
||||||
|
return min_step, max(min_step, min(wiresheet.META_COORD_MAX, lowest + TREE_DROP_GAP))
|
||||||
|
|
||||||
|
|
||||||
|
# Meta steps between the lowest existing box and one added under it from the tree.
|
||||||
|
TREE_DROP_GAP = 8
|
||||||
|
|
||||||
|
|
||||||
|
def add_component(kit_name, type_name, grid_x, grid_y, parent=None):
|
||||||
|
"""Places a new component of kit::type at a grid position. Returns the element.
|
||||||
|
|
||||||
|
parent is the component it goes inside; None means the sheet that is open.
|
||||||
|
"""
|
||||||
|
if parent is None:
|
||||||
|
parent = current_sheet_container()
|
||||||
if parent is None:
|
if parent is None:
|
||||||
messagebox.showwarning("Add Component", "Open a SAX file first.")
|
messagebox.showwarning("Add Component", "Open a SAX file first.")
|
||||||
return None
|
return None
|
||||||
|
|||||||
Reference in New Issue
Block a user