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 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")
|
||||
DEFAULT_GRID_SIZE = 10
|
||||
DEFAULT_ADD_TIMESTAMP = True
|
||||
@@ -54,6 +54,9 @@ CLEAN_TINT = "white"
|
||||
DIRTY_NAME_COLOUR = "#c00000"
|
||||
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 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
|
||||
|
||||
@@ -2655,8 +2655,12 @@ def _start_cov_polling():
|
||||
_cov_poll_id = app_state.window.after(100, poll)
|
||||
|
||||
|
||||
def _live_tree_expand(comp_id, tree_item_id):
|
||||
"""Called when user expands a tree node in live mode to lazy-load children."""
|
||||
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.
|
||||
|
||||
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
|
||||
if ds is None or not ds.is_connected:
|
||||
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-render — the sheet is already showing the right content and
|
||||
# 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():
|
||||
if elem.get("id") == str(comp_id):
|
||||
app_state.current_sheet_parent = elem
|
||||
_restoring_selection = True
|
||||
tree.item(iid, open=True)
|
||||
tree.selection_set(iid)
|
||||
tree.see(iid)
|
||||
tree.after_idle(_clear_restoring_flag)
|
||||
if select:
|
||||
app_state.current_sheet_parent = elem
|
||||
_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
|
||||
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()
|
||||
|
||||
|
||||
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 ---
|
||||
window = tk.Tk()
|
||||
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.
|
||||
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
|
||||
attach_tree_scrollbars(frame_tree, tree)
|
||||
tree.bind("<<TreeviewSelect>>", on_tree_select)
|
||||
@@ -3039,10 +3069,15 @@ def refresh_palette_kits():
|
||||
# --- Palette drag and drop ---
|
||||
# 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
|
||||
# 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
|
||||
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():
|
||||
@@ -3051,8 +3086,54 @@ def _destroy_drag_ghost():
|
||||
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():
|
||||
_destroy_drag_ghost()
|
||||
_mark_tree_drop_row(None)
|
||||
_cancel_hover_expand()
|
||||
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:
|
||||
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):
|
||||
left = canvas.winfo_rootx()
|
||||
top = canvas.winfo_rooty()
|
||||
return (left <= x_root < left + canvas.winfo_width()
|
||||
and top <= y_root < top + canvas.winfo_height())
|
||||
return _pointer_over_widget(canvas, x_root, y_root)
|
||||
|
||||
|
||||
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):
|
||||
@@ -3117,6 +3231,11 @@ def on_palette_release(event):
|
||||
if not was_dragging or not kit_name or not type_name:
|
||||
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):
|
||||
return
|
||||
|
||||
|
||||
+33
-3
@@ -16,6 +16,7 @@ from sax_dom import (
|
||||
find_component_by_path,
|
||||
find_parent_element,
|
||||
get_component_path,
|
||||
read_meta_property,
|
||||
restore_children,
|
||||
snapshot_children,
|
||||
update_meta_property,
|
||||
@@ -936,9 +937,38 @@ def build_config_values(full_type, overrides=None, device_values=None):
|
||||
return config_values
|
||||
|
||||
|
||||
def add_component(kit_name, type_name, grid_x, grid_y):
|
||||
"""Places a new component of kit::type at a grid position. Returns the element."""
|
||||
parent = current_sheet_container()
|
||||
def grid_position_below_children(parent):
|
||||
"""A spot for a component added without a drop point: under the parent's lowest child.
|
||||
|
||||
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:
|
||||
messagebox.showwarning("Add Component", "Open a SAX file first.")
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user