Rectangle selection moves to the left button on bare sheet (Ctrl adds); right-click is the menu again. Dragging a selected box by its title bar moves the whole selection as one undoable step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
585 lines
22 KiB
Python
585 lines
22 KiB
Python
"""Linking on the wiresheet with the mouse: slot hover, drag-out rubber band, drop.
|
|
|
|
Hovering a slot row highlights the half of it the pointer is in and swaps the cursor for
|
|
a horizontal arrow: the right half of a row is where a link leaves a slot, so it points
|
|
right, and the left half is where one arrives, so it points left. Pressing either
|
|
button on a slot and dragging pulls a rubber-band line out of that side; letting go on
|
|
another slot makes the link, and letting go on a component's title bar opens the Link
|
|
dialog for the two components with the dragged slot already picked. Started on the right half, the slot under the pointer is
|
|
the target; started on the left half, it is the source -- so a link can be drawn from
|
|
either end. While dragging, the cursor points along the flow of the link over a slot that
|
|
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 drag
|
|
that starts on bare sheet draws a selection rectangle, 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.
|
|
"""
|
|
|
|
import os
|
|
import struct
|
|
import tempfile
|
|
import tkinter as tk
|
|
|
|
import app_state
|
|
import sax_file
|
|
import wiresheet
|
|
from sax_dom import get_component_path
|
|
|
|
NO_ENTRY_PNG = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
"assets", "cursors", "no_entry.png")
|
|
DRAG_THRESHOLD = 4 # pixels the pointer must travel before a press is a drag
|
|
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 # left press on bare sheet: {"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)
|
|
|
|
|
|
# --------------------------------------------------------------------------- cursors
|
|
|
|
def _write_cur(path, pixels, hotspot):
|
|
"""Writes a 32x32 32-bit Windows .cur from rows of (r, g, b, a) tuples."""
|
|
size = 32
|
|
xor = bytearray()
|
|
for row in reversed(pixels): # DIBs are stored bottom-up
|
|
for r, g, b, a in row:
|
|
xor += bytes((b, g, r, a))
|
|
mask = bytearray()
|
|
for row in reversed(pixels):
|
|
bits = 0
|
|
for x, (_, _, _, a) in enumerate(row):
|
|
if a == 0:
|
|
bits |= 1 << (31 - x)
|
|
mask += struct.pack(">I", bits)
|
|
header = struct.pack("<IiiHHIIiiII", 40, size, size * 2, 1, 32, 0,
|
|
len(xor) + len(mask), 0, 0, 0, 0)
|
|
image = header + bytes(xor) + bytes(mask)
|
|
icondir = struct.pack("<HHH", 0, 2, 1)
|
|
entry = struct.pack("<BBBBHHII", size, size, 0, 0, hotspot[0], hotspot[1],
|
|
len(image), 6 + 16)
|
|
with open(path, "wb") as f:
|
|
f.write(icondir + entry + image)
|
|
|
|
|
|
def _inside_polygon(px, py, poly):
|
|
inside = False
|
|
j = len(poly) - 1
|
|
for i in range(len(poly)):
|
|
xi, yi = poly[i]
|
|
xj, yj = poly[j]
|
|
if (yi > py) != (yj > py) and px < (xj - xi) * (py - yi) / (yj - yi) + xi:
|
|
inside = not inside
|
|
j = i
|
|
return inside
|
|
|
|
|
|
def _arrow_pixels(pointing_right):
|
|
"""A hollow block arrow: white body, black rim -- the Workbench link cursor."""
|
|
poly = [(9, 14), (16, 14), (16, 10), (23, 16), (16, 22), (16, 18), (9, 18)]
|
|
if not pointing_right:
|
|
poly = [(32 - x, y) for x, y in poly]
|
|
inside = [[_inside_polygon(x + 0.5, y + 0.5, poly) for x in range(32)]
|
|
for y in range(32)]
|
|
rows = []
|
|
for y in range(32):
|
|
row = []
|
|
for x in range(32):
|
|
if not inside[y][x]:
|
|
row.append((0, 0, 0, 0))
|
|
continue
|
|
rim = any(not (0 <= x + dx < 32 and 0 <= y + dy < 32 and inside[y + dy][x + dx])
|
|
for dx in (-1, 0, 1) for dy in (-1, 0, 1))
|
|
row.append((0, 0, 0, 255) if rim else (255, 255, 255, 255))
|
|
rows.append(row)
|
|
return rows
|
|
|
|
|
|
def _no_entry_pixels():
|
|
"""assets/cursors/no_entry.png, centred on a 32x32 cursor; None if unreadable.
|
|
|
|
Read with PIL when it is installed, which keeps the image's soft edges; otherwise
|
|
with Tk's own PNG reader, which only knows whether a pixel is transparent or not.
|
|
"""
|
|
if not os.path.exists(NO_ENTRY_PNG):
|
|
return None
|
|
try:
|
|
from PIL import Image
|
|
image = Image.open(NO_ENTRY_PNG).convert("RGBA")
|
|
width, height = image.size
|
|
|
|
def pixel(x, y):
|
|
return image.getpixel((x, y))
|
|
except ImportError:
|
|
image = tk.PhotoImage(file=NO_ENTRY_PNG)
|
|
width, height = image.width(), image.height()
|
|
|
|
def pixel(x, y):
|
|
if image.transparency_get(x, y):
|
|
return (0, 0, 0, 0)
|
|
return tuple(image.get(x, y)) + (255,)
|
|
if width > 32 or height > 32:
|
|
return None
|
|
left, top = 16 - width // 2, 16 - height // 2
|
|
rows = [[(0, 0, 0, 0)] * 32 for _ in range(32)]
|
|
for y in range(height):
|
|
for x in range(width):
|
|
rows[top + y][left + x] = pixel(x, y)
|
|
return rows
|
|
|
|
|
|
def _cursor(role):
|
|
"""The Tk cursor for 'right', 'left' or 'no', drawn once and cached."""
|
|
if role in _cursors:
|
|
return _cursors[role]
|
|
fallback = {"right": "sb_right_arrow", "left": "sb_left_arrow", "no": "X_cursor"}[role]
|
|
spec = fallback
|
|
if os.name == "nt":
|
|
try:
|
|
folder = os.path.join(tempfile.gettempdir(), "sedona_editor_cursors")
|
|
os.makedirs(folder, exist_ok=True)
|
|
path = os.path.join(folder, f"link_{role}.cur")
|
|
if role == "right":
|
|
_write_cur(path, _arrow_pixels(True), (23, 16))
|
|
elif role == "left":
|
|
_write_cur(path, _arrow_pixels(False), (9, 16))
|
|
else:
|
|
pixels = _no_entry_pixels()
|
|
if pixels is None:
|
|
raise OSError("no_entry.png missing or unreadable")
|
|
_write_cur(path, pixels, (16, 16))
|
|
candidate = "{@" + path.replace("\\", "/") + "}"
|
|
app_state.canvas.configure(cursor=candidate) # proves Tk can load it
|
|
spec = candidate
|
|
except (OSError, tk.TclError):
|
|
spec = "no" if role == "no" else fallback
|
|
_cursors[role] = spec
|
|
return spec
|
|
|
|
|
|
def _set_cursor(role):
|
|
canvas = app_state.canvas
|
|
try:
|
|
canvas.configure(cursor=_cursor(role) if role else "")
|
|
except tk.TclError:
|
|
canvas.configure(cursor="")
|
|
|
|
|
|
# --------------------------------------------------------------------------- hit test
|
|
|
|
def slot_at(cx, cy):
|
|
"""The slot row under a canvas point, as a dict, or None.
|
|
|
|
Picks the topmost box under the point, so a box lying over another wins, and says
|
|
which half of the row the point is in.
|
|
"""
|
|
canvas = app_state.canvas
|
|
for item in reversed(canvas.find_overlapping(cx, cy, cx, cy)):
|
|
tags = canvas.gettags(item)
|
|
if "component" not in tags:
|
|
continue
|
|
comp_tag = next((t for t in tags if t.startswith("comp_group_")), None)
|
|
if comp_tag is None:
|
|
continue
|
|
box = wiresheet.get_box_bounds(comp_tag)
|
|
if box is None:
|
|
return None
|
|
zoom = app_state.zoom
|
|
half_row = wiresheet.SLOT_ROW_HEIGHT * zoom / 2
|
|
for slot_name, offset in app_state.component_slot_offsets.get(comp_tag, {}).items():
|
|
row_y = box[1] + offset * zoom
|
|
if row_y - half_row <= cy < row_y + half_row:
|
|
return {
|
|
"comp_tag": comp_tag,
|
|
"comp": app_state.canvas_comp_map.get(comp_tag),
|
|
"slot": slot_name,
|
|
"side": "right" if cx >= (box[0] + box[2]) / 2 else "left",
|
|
"box": box,
|
|
"row_y": row_y,
|
|
"half_row": half_row,
|
|
}
|
|
return None # over a header or footer, not a slot
|
|
return None
|
|
|
|
|
|
def title_at(cx, cy):
|
|
"""The comp tag whose title bar is topmost under a canvas point, or None."""
|
|
canvas = app_state.canvas
|
|
for item in reversed(canvas.find_overlapping(cx, cy, cx, cy)):
|
|
tags = canvas.gettags(item)
|
|
if "component" not in tags:
|
|
continue
|
|
comp_tag = next((t for t in tags if t.startswith("comp_group_")), None)
|
|
if comp_tag is None:
|
|
continue
|
|
box = wiresheet.get_box_bounds(comp_tag)
|
|
if box and box[1] <= cy <= box[1] + wiresheet.HEADER_HEIGHT * app_state.zoom:
|
|
return comp_tag
|
|
return None
|
|
return None
|
|
|
|
|
|
def _slot_info(comp, slot_name):
|
|
for slot in app_state.final_components_registry.get(comp.get("type", ""), []):
|
|
if slot["name"] == slot_name:
|
|
return slot
|
|
return None
|
|
|
|
|
|
def _check_link(source, target):
|
|
"""Returns (ok, reason) for a link from one hit slot to another."""
|
|
if source["comp"] is None or target["comp"] is None:
|
|
return False, "No component there."
|
|
if source["comp"] is target["comp"] and source["slot"] == target["slot"]:
|
|
return False, "A slot cannot be linked to itself."
|
|
src = _slot_info(source["comp"], source["slot"])
|
|
tgt = _slot_info(target["comp"], target["slot"])
|
|
if src is None or tgt is None:
|
|
return False, "Slot not found in the kit manifest."
|
|
for slot in (src, tgt):
|
|
if "a" in slot["flags"] or slot["type"] == "void":
|
|
return False, f"'{slot['name']}' is an action and cannot be linked."
|
|
if tgt["readonly"]:
|
|
return False, f"'{tgt['name']}' is read-only and cannot be a link target."
|
|
if src["type"] != tgt["type"]:
|
|
return False, (f"Type mismatch: {source['slot']} is {src['type']}, "
|
|
f"{target['slot']} is {tgt['type']}.")
|
|
from_ref, to_ref = _refs(source, target)
|
|
links = app_state.xml_root_element.find("links")
|
|
if links is not None:
|
|
for link in links.findall("link"):
|
|
if link.get("from") == from_ref and link.get("to") == to_ref:
|
|
return False, "That link already exists."
|
|
return True, f"Link {from_ref} → {to_ref}"
|
|
|
|
|
|
def _refs(source, target):
|
|
root = app_state.xml_root_element
|
|
return (f"{get_component_path(source['comp'], root)}.{source['slot']}",
|
|
f"{get_component_path(target['comp'], root)}.{target['slot']}")
|
|
|
|
|
|
# --------------------------------------------------------------------------- drawing
|
|
|
|
def _highlight(hit, color):
|
|
"""Rings the half of a slot row the pointer is over, with a bar on its outer edge."""
|
|
canvas = app_state.canvas
|
|
canvas.delete("slot_hover")
|
|
if hit is None:
|
|
return
|
|
left, _, right, _ = hit["box"]
|
|
mid = (left + right) / 2
|
|
top, bottom = hit["row_y"] - hit["half_row"], hit["row_y"] + hit["half_row"]
|
|
x1, x2 = (mid, right - 1) if hit["side"] == "right" else (left + 1, mid)
|
|
canvas.create_rectangle(x1, top, x2, bottom, outline=color, width=2,
|
|
tags=("slot_hover",))
|
|
edge = right - 2 if hit["side"] == "right" else left + 2
|
|
canvas.create_line(edge, top, edge, bottom, fill=color, width=4, tags=("slot_hover",))
|
|
canvas.tag_raise("slot_hover")
|
|
|
|
|
|
def _highlight_title(comp_tag, color):
|
|
"""Rings a box's title bar: the drop that opens the Link dialog for that box."""
|
|
canvas = app_state.canvas
|
|
canvas.delete("slot_hover")
|
|
left, top, right, _ = wiresheet.get_box_bounds(comp_tag)
|
|
canvas.create_rectangle(left + 1, top + 1, right - 1,
|
|
top + wiresheet.HEADER_HEIGHT * app_state.zoom,
|
|
outline=color, width=2, tags=("slot_hover",))
|
|
canvas.tag_raise("slot_hover")
|
|
|
|
|
|
def _anchor(hit):
|
|
left, _, right, _ = hit["box"]
|
|
return (right if hit["side"] == "right" else left), hit["row_y"]
|
|
|
|
|
|
def clear():
|
|
"""Drops any hover ring, rubber band and drag state; called on every repaint."""
|
|
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)
|
|
|
|
|
|
# --------------------------------------------------------------------------- events
|
|
|
|
def on_motion(event):
|
|
"""Plain pointer movement: ring the slot half under the pointer."""
|
|
if _drag is not None or app_state.drag_data.get("tag"):
|
|
return
|
|
canvas = app_state.canvas
|
|
hit = slot_at(canvas.canvasx(event.x), canvas.canvasy(event.y))
|
|
_highlight(hit, HOVER_COLOR)
|
|
_set_cursor(hit["side"] if hit else None)
|
|
|
|
|
|
def on_leave(_event):
|
|
if _drag is None:
|
|
app_state.canvas.delete("slot_hover")
|
|
_set_cursor(None)
|
|
|
|
|
|
def _start_press(event):
|
|
"""Records a press on a slot row as a link drag in waiting; None if not on a slot."""
|
|
global _press
|
|
canvas = app_state.canvas
|
|
hit = slot_at(canvas.canvasx(event.x), canvas.canvasy(event.y))
|
|
if hit is None or hit["comp"] is None:
|
|
_press = None
|
|
return None
|
|
_press = {"hit": hit, "x": event.x, "y": event.y}
|
|
return _press
|
|
|
|
|
|
def _drag_motion(event):
|
|
"""Pulls the rubber band to the pointer and judges the slot under it, if any."""
|
|
global _drag
|
|
canvas = app_state.canvas
|
|
if _drag is None:
|
|
if _press is None:
|
|
return
|
|
if abs(event.x - _press["x"]) < DRAG_THRESHOLD and abs(event.y - _press["y"]) < DRAG_THRESHOLD:
|
|
return
|
|
_drag = {"hit": _press["hit"], "anchor": _anchor(_press["hit"])}
|
|
app_state.set_status("Drop on a slot to link it; release elsewhere to cancel.")
|
|
|
|
cx, cy = canvas.canvasx(event.x), canvas.canvasy(event.y)
|
|
start = _drag["hit"]
|
|
ax, ay = _drag["anchor"]
|
|
canvas.delete("link_rubber")
|
|
canvas.create_line(ax, ay, cx, cy, fill=RUBBER_COLOR, width=2, dash=(4, 2),
|
|
tags=("link_rubber",))
|
|
canvas.tag_raise("link_rubber")
|
|
|
|
outgoing = start["side"] == "right"
|
|
hit = slot_at(cx, cy)
|
|
if hit is None:
|
|
title_tag = title_at(cx, cy)
|
|
if title_tag is not None:
|
|
# Its own title too: a component may link one of its slots to another.
|
|
_highlight_title(title_tag, DROP_OK_COLOR)
|
|
_set_cursor("right" if outgoing else "left")
|
|
app_state.set_status("Drop to pick the other slot in the Link dialog.")
|
|
return
|
|
if hit is None or (hit["comp_tag"] == start["comp_tag"] and hit["slot"] == start["slot"]):
|
|
canvas.delete("slot_hover")
|
|
_set_cursor(None)
|
|
return
|
|
ok, reason = _check_link(start, hit) if outgoing else _check_link(hit, start)
|
|
# Ring the side of the drop slot the link would attach to, not the side under the
|
|
# pointer: a link always arrives on the left of a row and leaves from its right.
|
|
shown = dict(hit, side="left" if outgoing else "right")
|
|
_highlight(shown, DROP_OK_COLOR if ok else DROP_BAD_COLOR)
|
|
_set_cursor(("right" if outgoing else "left") if ok else "no")
|
|
app_state.set_status(reason, warn=not ok)
|
|
|
|
|
|
def _finish(event):
|
|
"""Ends a press. Returns 'link' after a drag (made or refused), 'click' after a
|
|
press that never moved, None if no press on a slot was pending."""
|
|
global _press, _drag
|
|
canvas = app_state.canvas
|
|
press, drag = _press, _drag
|
|
_press = _drag = None
|
|
canvas.delete("link_rubber")
|
|
canvas.delete("slot_hover")
|
|
_set_cursor(None)
|
|
|
|
if drag is None:
|
|
return "click" if press is not None else None
|
|
|
|
cx, cy = canvas.canvasx(event.x), canvas.canvasy(event.y)
|
|
hit = slot_at(cx, cy)
|
|
start = drag["hit"]
|
|
title_tag = title_at(cx, cy) if hit is None else None
|
|
if title_tag is not None:
|
|
other = app_state.canvas_comp_map.get(title_tag)
|
|
if other is None:
|
|
app_state.set_status("Link cancelled.")
|
|
elif start["side"] == "right":
|
|
_link_dialog(start["comp"], other, start["slot"], None)
|
|
else:
|
|
_link_dialog(other, start["comp"], None, start["slot"])
|
|
return "link"
|
|
if hit is None or (hit["comp_tag"] == start["comp_tag"] and hit["slot"] == start["slot"]):
|
|
app_state.set_status("Link cancelled.")
|
|
return "link"
|
|
source, target = (start, hit) if start["side"] == "right" else (hit, start)
|
|
ok, reason = _check_link(source, target)
|
|
if not ok:
|
|
app_state.set_status(f"Cannot link: {reason}", warn=True)
|
|
return "link"
|
|
from_ref, to_ref = _refs(source, target)
|
|
sax_file.add_link(from_ref, to_ref)
|
|
app_state.set_status(f"Linked {from_ref} → {to_ref}")
|
|
return "link"
|
|
|
|
|
|
# Right button: on a slot it drags a link, and a press that never moves is the menu.
|
|
|
|
def on_right_press(event):
|
|
if _start_press(event) is None:
|
|
_menu_callback(event)
|
|
|
|
|
|
def on_right_motion(event):
|
|
_drag_motion(event)
|
|
|
|
|
|
def on_right_release(event):
|
|
if _finish(event) == "click":
|
|
_menu_callback(event)
|
|
|
|
|
|
# Left button on bare sheet: dragging pulls out a selection rectangle that selects every
|
|
# box wholly inside it on release (Ctrl adds them to the current selection).
|
|
|
|
def _on_bare_sheet(cx, cy):
|
|
"""True if no box or link lies under a canvas point."""
|
|
canvas = app_state.canvas
|
|
for item in canvas.find_overlapping(cx - 3, cy - 3, cx + 3, cy + 3):
|
|
tags = canvas.gettags(item)
|
|
if "component" in tags or "link" in tags:
|
|
return False
|
|
return True
|
|
|
|
|
|
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
|
|
|
|
|
|
# Left button: anywhere on a box selects it. Only the title bar moves it; a slot row
|
|
# drags out a link, and the rest of the box does nothing more than select.
|
|
|
|
def _on_title_bar(comp_tag, cy):
|
|
box = wiresheet.get_box_bounds(comp_tag)
|
|
return box is not None and box[1] <= cy <= box[1] + wiresheet.HEADER_HEIGHT * app_state.zoom
|
|
|
|
|
|
def on_left_press(event):
|
|
global _marquee
|
|
_marquee = None
|
|
canvas = app_state.canvas
|
|
cx, cy = canvas.canvasx(event.x), canvas.canvasy(event.y)
|
|
if _on_bare_sheet(cx, cy):
|
|
app_state.drag_data["tag"] = None
|
|
if not event.state & CTRL_MASK:
|
|
# A plain click on bare sheet still clears the selection, drag or not.
|
|
wiresheet.deselect_link()
|
|
_select_boxes([])
|
|
_marquee = {"x": event.x, "y": event.y, "cx": cx, "cy": cy,
|
|
"state": event.state, "active": False}
|
|
return
|
|
_box_handlers["press"](event) # selection, and arms a box move
|
|
if _start_press(event) is not None:
|
|
app_state.canvas.delete("slot_hover")
|
|
comp_tag = app_state.drag_data.get("tag")
|
|
if comp_tag and not _on_title_bar(comp_tag, app_state.canvas.canvasy(event.y)):
|
|
app_state.drag_data["tag"] = None # selected, but not picked up
|
|
|
|
|
|
def on_left_motion(event):
|
|
if _marquee is not None:
|
|
_marquee_motion(event)
|
|
elif _press is not None or _drag is not None:
|
|
_drag_motion(event)
|
|
else:
|
|
_box_handlers["motion"](event)
|
|
|
|
|
|
def on_left_release(event):
|
|
if _marquee is not None:
|
|
_marquee_finish(event)
|
|
elif _finish(event) in (None, "click"):
|
|
_box_handlers["release"](event)
|
|
|
|
|
|
_box_handlers = {}
|
|
|
|
|
|
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;
|
|
select_boxes makes a list of comp tags the selection after a rectangle drag.
|
|
"""
|
|
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="+")
|
|
canvas.bind("<ButtonPress-1>", on_left_press)
|
|
canvas.bind("<B1-Motion>", on_left_motion)
|
|
canvas.bind("<ButtonRelease-1>", on_left_release)
|
|
canvas.bind("<ButtonPress-3>", on_right_press)
|
|
canvas.bind("<B3-Motion>", on_right_motion)
|
|
canvas.bind("<ButtonRelease-3>", on_right_release)
|