From 81c4a1d31d4f8adb9f7e5a18661e1a18ba8632c7 Mon Sep 17 00:00:00 2001 From: Arda Aydin Date: Sun, 18 Oct 2026 14:21:56 +0300 Subject: [PATCH] Wiresheet: right-drag rectangle selects boxes, Ctrl adds to selection Co-Authored-By: Claude Opus 5.5 --- app_state.py | 2 +- link_drag.py | 89 ++++++++++++++++++++++++++++++++++++++++++++++------ main.py | 2 +- 3 files changed, 82 insertions(+), 11 deletions(-) diff --git a/app_state.py b/app_state.py index af0e186..7cb2f76 100644 --- a/app_state.py +++ b/app_state.py @@ -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.176" +VERSION = "0.0.0.177" PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties") DEFAULT_GRID_SIZE = 10 DEFAULT_ADD_TIMESTAMP = True diff --git a/link_drag.py b/link_drag.py index 7a04aa3..4886a93 100644 --- a/link_drag.py +++ b/link_drag.py @@ -11,7 +11,9 @@ either end. While dragging, the cursor points along the flow of the link over a would take it, and turns into a no-entry sign over one that would not, so a bad drop is visible before the button is released. -A right press that never moves still opens the component's context menu. A left press +A right press that never moves still opens the component's context menu. A right drag +that starts off a slot row draws a selection rectangle instead, and selects the boxes +wholly inside it on release; with Ctrl held they join the current selection. A left press anywhere on a box selects it, but only a press on its title bar moves it; on a slot row it draws a link instead, as in Workbench. """ @@ -33,11 +35,15 @@ HOVER_COLOR = "#1f4e79" DROP_OK_COLOR = "#2e9d3a" DROP_BAD_COLOR = "#d62828" RUBBER_COLOR = "#2f6fba" +MARQUEE_COLOR = "#2f6fba" +CTRL_MASK = 0x0004 _cursors = {} # role -> Tk cursor spec, built on first use _press = None # pending right press: {"hit", "x", "y", "event"} _drag = None # live link drag: {"hit", "anchor"} +_marquee = None # right press off a slot: {"x", "y", "cx", "cy", "state", "active"} _menu_callback = None # opens the ordinary right-click menu for an event +_select_boxes = None # makes a list of comp tags the box selection _link_dialog = None # opens the Link dialog: (source, target, src_slot, tgt_slot) @@ -301,14 +307,16 @@ def _anchor(hit): def clear(): """Drops any hover ring, rubber band and drag state; called on every repaint.""" - global _press, _drag + global _press, _drag, _marquee canvas = app_state.canvas if canvas is None: return canvas.delete("slot_hover") canvas.delete("link_rubber") + canvas.delete("marquee") _press = None _drag = None + _marquee = None _set_cursor(None) @@ -426,19 +434,79 @@ def _finish(event): return "link" -# Right button: on a slot it drags a link, and a press that never moves is the menu. +# Right button: on a slot it drags a link. Anywhere else, dragging pulls out a selection +# rectangle that selects every box wholly inside it on release (Ctrl adds them to the +# current selection). A press that never moves is the menu. + +def _marquee_motion(event): + """Draws the selection rectangle once the pointer has moved far enough.""" + global _marquee + if _marquee is None: + return + if not _marquee["active"]: + if abs(event.x - _marquee["x"]) < DRAG_THRESHOLD and \ + abs(event.y - _marquee["y"]) < DRAG_THRESHOLD: + return + _marquee["active"] = True + app_state.canvas.delete("slot_hover") + canvas = app_state.canvas + canvas.delete("marquee") + canvas.create_rectangle(_marquee["cx"], _marquee["cy"], + canvas.canvasx(event.x), canvas.canvasy(event.y), + outline=MARQUEE_COLOR, width=1, dash=(4, 2), + tags=("marquee",)) + canvas.tag_raise("marquee") + + +def _marquee_finish(event): + """Ends a rectangle drag. Returns True if it selected, False if it was a click.""" + global _marquee + marquee, _marquee = _marquee, None + canvas = app_state.canvas + canvas.delete("marquee") + if marquee is None or not marquee["active"]: + return False + cx, cy = canvas.canvasx(event.x), canvas.canvasy(event.y) + left, right = sorted((marquee["cx"], cx)) + top, bottom = sorted((marquee["cy"], cy)) + inside = [] + for comp_tag in app_state.canvas_comp_map: + box = wiresheet.get_box_bounds(comp_tag) + if box and left <= box[0] and top <= box[1] and box[2] <= right and box[3] <= bottom: + inside.append(comp_tag) + adding = (marquee["state"] | event.state) & CTRL_MASK + wiresheet.deselect_link() # links and boxes are never selected together + if adding: + current = [t for t in app_state.selected_canvas_tags if t not in inside] + _select_boxes(current + inside) + else: + _select_boxes(inside) + count = len(app_state.selected_canvas_tags) + app_state.set_status(f"{count} component{'s' if count != 1 else ''} selected.") + return True + def on_right_press(event): + global _marquee + _marquee = None if _start_press(event) is None: - _menu_callback(event) + canvas = app_state.canvas + _marquee = {"x": event.x, "y": event.y, "cx": canvas.canvasx(event.x), + "cy": canvas.canvasy(event.y), "state": event.state, "active": False} def on_right_motion(event): - _drag_motion(event) + if _marquee is not None: + _marquee_motion(event) + else: + _drag_motion(event) def on_right_release(event): - if _finish(event) == "click": + if _marquee is not None: + if not _marquee_finish(event): + _menu_callback(event) + elif _finish(event) == "click": _menu_callback(event) @@ -474,16 +542,19 @@ def on_left_release(event): _box_handlers = {} -def install(canvas, menu_callback, box_press, box_motion, box_release, link_dialog): +def install(canvas, menu_callback, box_press, box_motion, box_release, link_dialog, + select_boxes): """Binds hover, link dragging on both buttons, and the box handlers it wraps. menu_callback opens the context menu; box_* are the sheet's own left-button select-and-move handlers, which still run whenever a press is not on a slot row; - link_dialog opens the Link dialog when a link is dropped on a title bar. + link_dialog opens the Link dialog when a link is dropped on a title bar; + select_boxes makes a list of comp tags the selection after a rectangle drag. """ - global _menu_callback, _link_dialog + global _menu_callback, _link_dialog, _select_boxes _menu_callback = menu_callback _link_dialog = link_dialog + _select_boxes = select_boxes _box_handlers.update(press=box_press, motion=box_motion, release=box_release) canvas.bind("", on_motion) canvas.bind("", on_leave, add="+") diff --git a/main.py b/main.py index 0f88f63..9f5feb7 100644 --- a/main.py +++ b/main.py @@ -3761,7 +3761,7 @@ canvas.bind("", lambda e: wiresheet.draw_grid()) # selects and moves boxes and a plain right click is the menu, as before. link_drag.install(canvas, on_canvas_right_click, on_canvas_click, on_canvas_drag, on_canvas_release, - link_dialog=open_link_dialog) + link_dialog=open_link_dialog, select_boxes=select_canvas_boxes) canvas.bind("", on_canvas_right_click) canvas.bind("", on_canvas_double_click)