Wiresheet zoom, from the View menu, the toolbar and Ctrl+wheel
A ladder of scales from 50% to 300%, stepped by Zoom In / Zoom Out / Zoom 100%, by the two buttons beside the navigation arrows, and by Ctrl and the wheel, which holds the point under the pointer still. A menu or keyboard zoom holds the middle of the view instead, so it closes in on what is already being looked at. Painting and routing stay at 1:1, because that is where every metric is tuned: box header and row heights, wire stubs, lane gaps, standoffs, knob radii. Zoom scales the finished picture, so a zoomed sheet keeps the proportions of a 1:1 one and the router reaches the same decisions at every scale. Text comes along because the three box fonts are resized once the boxes have been measured with them. redraw_links replaces the direct draw_links calls a drag or a drop makes: the router reads box positions off the canvas, so the boxes go back to 1:1 for the routing pass and the result is scaled again. Canvas coordinates carry the zoom, which is why the drag, release and drop handlers now snap against meta_scale * zoom -- a box dragged the same distance across the screen lands on the same meta step at 50%, 100% and 200%. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,7 @@ _history_file_path = None # the file sheet_history was recorded against
|
||||
# function that touches them checks first.
|
||||
btn_back = None
|
||||
btn_forward = None
|
||||
zoom_label = None
|
||||
menu_view = None
|
||||
show_left_column_var = None
|
||||
|
||||
@@ -119,6 +120,71 @@ def do_tidy_layout(event=None):
|
||||
return "break"
|
||||
|
||||
|
||||
# --- Zoom ---
|
||||
|
||||
def update_zoom_label():
|
||||
"""Puts the painted scale on the toolbar as a percentage."""
|
||||
if zoom_label is not None:
|
||||
zoom_label.config(text=f"{round(app_state.zoom * 100)}%")
|
||||
|
||||
|
||||
def zoom_to(level, anchor=None):
|
||||
"""Repaints the sheet at this zoom, holding one point of it still on screen.
|
||||
|
||||
``anchor`` is a canvas-relative (x, y) to keep under the same pixel -- the pointer,
|
||||
for a wheel zoom -- and defaults to the middle of the visible area, so a menu or
|
||||
keyboard zoom closes in on what is already being looked at rather than on the
|
||||
top-left corner.
|
||||
"""
|
||||
canvas = app_state.canvas
|
||||
before = app_state.zoom
|
||||
|
||||
if anchor is None:
|
||||
anchor = (canvas.winfo_width() / 2, canvas.winfo_height() / 2)
|
||||
|
||||
# Where the held point sits on the unscaled sheet.
|
||||
sheet_x = canvas.canvasx(anchor[0]) / before
|
||||
sheet_y = canvas.canvasy(anchor[1]) / before
|
||||
|
||||
after = wiresheet.set_zoom(level)
|
||||
update_zoom_label()
|
||||
if after == before:
|
||||
return
|
||||
|
||||
# Scroll the repainted sheet back under the held point. The scrollregion starts at
|
||||
# the origin, so its far corner is the span the view fractions are measured against.
|
||||
region = str(canvas.cget("scrollregion")).split()
|
||||
if len(region) == 4:
|
||||
span_x = float(region[2]) or 1.0
|
||||
span_y = float(region[3]) or 1.0
|
||||
canvas.xview_moveto(max(0.0, (sheet_x * after - anchor[0]) / span_x))
|
||||
canvas.yview_moveto(max(0.0, (sheet_y * after - anchor[1]) / span_y))
|
||||
|
||||
|
||||
def do_zoom_in(event=None):
|
||||
zoom_to(wiresheet.zoom_step(1))
|
||||
return "break"
|
||||
|
||||
|
||||
def do_zoom_out(event=None):
|
||||
zoom_to(wiresheet.zoom_step(-1))
|
||||
return "break"
|
||||
|
||||
|
||||
def do_zoom_reset(event=None):
|
||||
zoom_to(1.0)
|
||||
return "break"
|
||||
|
||||
|
||||
def on_canvas_wheel_zoom(event):
|
||||
"""Ctrl + wheel over the sheet, the gesture every other editor uses for zoom."""
|
||||
if not event.delta:
|
||||
return "break"
|
||||
step = 1 if event.delta > 0 else -1
|
||||
zoom_to(wiresheet.zoom_step(step), anchor=(event.x, event.y))
|
||||
return "break"
|
||||
|
||||
|
||||
# --- Event handlers ---
|
||||
|
||||
def on_tree_select(event):
|
||||
@@ -251,7 +317,7 @@ def on_canvas_click(event):
|
||||
|
||||
# Bring the clicked box clear of any neighbour it happens to sit under.
|
||||
canvas.tag_raise(group_tag)
|
||||
wiresheet.draw_links()
|
||||
wiresheet.redraw_links()
|
||||
|
||||
|
||||
def do_link_mark():
|
||||
@@ -520,13 +586,14 @@ def on_canvas_drag(event):
|
||||
canvas.move(group_tag, cx - app_state.drag_data["x"], cy - app_state.drag_data["y"])
|
||||
app_state.drag_data["x"] = cx
|
||||
app_state.drag_data["y"] = cy
|
||||
wiresheet.draw_links()
|
||||
wiresheet.redraw_links()
|
||||
return
|
||||
|
||||
# Snap to whole meta steps, which is the grid on an ordinary sheet and a multiple of
|
||||
# it on one too wide for meta's range -- a position between steps cannot be stored.
|
||||
step = app_state.meta_scale
|
||||
margin = wiresheet.layout_margin(step)
|
||||
# Canvas coordinates carry the sheet's zoom, so the step and the margin do too.
|
||||
step = app_state.meta_scale * app_state.zoom
|
||||
margin = wiresheet.layout_margin(app_state.meta_scale) * app_state.zoom
|
||||
|
||||
# Follow the pointer, but land the box on a step every step of the way, and keep it
|
||||
# out of the margin strip along the top and left canvas edges.
|
||||
@@ -542,7 +609,7 @@ def on_canvas_drag(event):
|
||||
canvas.move(group_tag, shift_x, shift_y)
|
||||
# Wires are not part of the dragged group, so they are re-routed against the
|
||||
# new positions.
|
||||
wiresheet.draw_links()
|
||||
wiresheet.redraw_links()
|
||||
|
||||
|
||||
def on_canvas_release(event):
|
||||
@@ -550,14 +617,14 @@ def on_canvas_release(event):
|
||||
return
|
||||
|
||||
canvas = app_state.canvas
|
||||
step = app_state.meta_scale
|
||||
step = app_state.meta_scale * app_state.zoom
|
||||
group_tag = app_state.drag_data["tag"]
|
||||
origin = wiresheet.get_box_origin(group_tag)
|
||||
|
||||
if origin is not None:
|
||||
current_x, current_y = origin
|
||||
|
||||
margin = wiresheet.layout_margin(step)
|
||||
margin = wiresheet.layout_margin(app_state.meta_scale) * app_state.zoom
|
||||
snapped_x = max(margin, round(current_x / step) * step)
|
||||
snapped_y = max(margin, round(current_y / step) * step)
|
||||
|
||||
@@ -589,7 +656,7 @@ def on_canvas_release(event):
|
||||
app_state.mark_dirty()
|
||||
|
||||
app_state.drag_data["tag"] = None
|
||||
wiresheet.draw_links()
|
||||
wiresheet.redraw_links()
|
||||
|
||||
|
||||
def fit_tree_column(tree):
|
||||
@@ -1174,6 +1241,16 @@ SHORTCUTS = [
|
||||
("z", do_undo),
|
||||
("y", do_redo),
|
||||
("b", toggle_left_column),
|
||||
# Zoom, in every spelling the key can arrive as: with and without Shift, and on the
|
||||
# numeric keypad.
|
||||
("plus", do_zoom_in),
|
||||
("equal", do_zoom_in),
|
||||
("KP_Add", do_zoom_in),
|
||||
("minus", do_zoom_out),
|
||||
("underscore", do_zoom_out),
|
||||
("KP_Subtract", do_zoom_out),
|
||||
("0", do_zoom_reset),
|
||||
("KP_0", do_zoom_reset),
|
||||
]
|
||||
for key, handler in SHORTCUTS:
|
||||
window.bind_all(f"<Command-{key}>", handler)
|
||||
@@ -1233,6 +1310,10 @@ menu_view.add_command(
|
||||
label=MENU_FORWARD_LABEL, command=go_forward, accelerator="Alt+Right", state="disabled"
|
||||
)
|
||||
menu_view.add_separator()
|
||||
menu_view.add_command(label="Zoom In", command=do_zoom_in, accelerator=f"{accel_mod}++")
|
||||
menu_view.add_command(label="Zoom Out", command=do_zoom_out, accelerator=f"{accel_mod}+-")
|
||||
menu_view.add_command(label="Zoom 100%", command=do_zoom_reset, accelerator=f"{accel_mod}+0")
|
||||
menu_view.add_separator()
|
||||
menu_view.add_command(label="Tidy Layout", command=do_tidy_layout, accelerator="Ctrl+W")
|
||||
menu_bar.add_cascade(label="View", menu=menu_view)
|
||||
|
||||
@@ -1269,6 +1350,13 @@ btn_back.pack(side=tk.LEFT)
|
||||
btn_forward = ttk.Button(toolbar, text="▶", width=3, command=go_forward, state="disabled")
|
||||
btn_forward.pack(side=tk.LEFT, padx=(3, 0))
|
||||
|
||||
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=8, pady=2)
|
||||
|
||||
ttk.Button(toolbar, text="−", width=3, command=do_zoom_out).pack(side=tk.LEFT)
|
||||
zoom_label = ttk.Label(toolbar, text="100%", width=6, anchor="center")
|
||||
zoom_label.pack(side=tk.LEFT, padx=1)
|
||||
ttk.Button(toolbar, text="+", width=3, command=do_zoom_in).pack(side=tk.LEFT)
|
||||
|
||||
paned_window = ttk.PanedWindow(window, orient=tk.HORIZONTAL)
|
||||
paned_window.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||
|
||||
@@ -1461,8 +1549,8 @@ def on_palette_release(event):
|
||||
drop_x = canvas.canvasx(event.x_root - canvas.winfo_rootx())
|
||||
drop_y = canvas.canvasy(event.y_root - canvas.winfo_rooty())
|
||||
|
||||
step = app_state.meta_scale
|
||||
min_step = wiresheet.layout_margin(step) // step
|
||||
step = app_state.meta_scale * app_state.zoom
|
||||
min_step = wiresheet.layout_margin(app_state.meta_scale) // app_state.meta_scale
|
||||
grid_x = max(min_step, min(wiresheet.META_COORD_MAX, round(drop_x / step)))
|
||||
grid_y = max(min_step, min(wiresheet.META_COORD_MAX, round(drop_y / step)))
|
||||
|
||||
@@ -1511,6 +1599,11 @@ canvas.bind("<Configure>", lambda e: wiresheet.draw_grid())
|
||||
canvas.bind("<ButtonPress-3>", on_canvas_right_click)
|
||||
canvas.bind("<ButtonPress-2>", on_canvas_right_click)
|
||||
|
||||
# Wheel zoom. Windows and macOS report a delta; X11 sends button 4 and 5 instead.
|
||||
canvas.bind("<Control-MouseWheel>", on_canvas_wheel_zoom)
|
||||
canvas.bind("<Control-Button-4>", do_zoom_in)
|
||||
canvas.bind("<Control-Button-5>", do_zoom_out)
|
||||
|
||||
paned_window.add(frame_editor, weight=3)
|
||||
|
||||
undo.set_refresh_callback(refresh_all_views)
|
||||
|
||||
Reference in New Issue
Block a user