Wiresheet: right-drag rectangle selects boxes, Ctrl adds to selection

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 14:21:56 +03:00
co-authored by Claude Opus 5.5
parent 0c69f18046
commit 81c4a1d31d
3 changed files with 82 additions and 11 deletions
+1 -1
View File
@@ -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
+80 -9
View File
@@ -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("<Motion>", on_motion)
canvas.bind("<Leave>", on_leave, add="+")
+1 -1
View File
@@ -3761,7 +3761,7 @@ canvas.bind("<Configure>", 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("<ButtonPress-2>", on_canvas_right_click)
canvas.bind("<Double-1>", on_canvas_double_click)