Zoom to Fit, with a toolbar icon beside the zoom controls

Fits the whole painted sheet in the visible canvas and scrolls it into view,
from the View menu, Ctrl/Cmd+9, or the four-corners button after the zoom
percentage. The ladder gains 25% and 33% at the bottom, so a wide sheet has
somewhere to fit into.

Fit is the one scale that is not on the ladder -- a sheet fits at whatever
scale it fits at -- so zoom_step now takes the first level above or below the
scale in force rather than the neighbour of the nearest rung.

The sheet is measured from item coordinates rather than canvas.bbox(), which
is inflated by the outline stroke: a stroke is the same two pixels at every
zoom, so a bbox divided back to 1:1 reports a slightly smaller sheet the
further in it is zoomed, and fitting an already fitted sheet crept from 288%
to 291% instead of standing still. Anything painted left of or above the
canvas origin is measured from the origin, since no amount of scrolling brings
it into view.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 23:14:46 +03:00
co-authored by Claude Opus 5
parent 781138e3dc
commit ad8396ef7b
4 changed files with 125 additions and 8 deletions
+34
View File
@@ -176,6 +176,34 @@ def do_zoom_reset(event=None):
return "break"
def do_zoom_fit(event=None):
"""Zoom so the whole sheet is visible, and scroll it into view.
The fitted scale is not one of the ladder's, so the percentage on the toolbar is
where it is read off; stepping in or out from there rejoins the ladder.
"""
canvas = app_state.canvas
level = wiresheet.fit_zoom_level()
bounds = wiresheet.sheet_content_bounds()
if level is None or bounds is None:
app_state.set_status("Nothing to fit — open a sheet with components first.")
return "break"
applied = wiresheet.set_zoom(level)
update_zoom_label()
# Bring the sheet's own top-left corner to the top-left of the view, since a fitted
# sheet is measured from there rather than from the canvas origin.
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, (bounds[0] * applied - wiresheet.FIT_PADDING) / span_x))
canvas.yview_moveto(max(0.0, (bounds[1] * applied - wiresheet.FIT_PADDING) / span_y))
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:
@@ -1251,6 +1279,8 @@ SHORTCUTS = [
("KP_Subtract", do_zoom_out),
("0", do_zoom_reset),
("KP_0", do_zoom_reset),
("9", do_zoom_fit),
("KP_9", do_zoom_fit),
]
for key, handler in SHORTCUTS:
window.bind_all(f"<Command-{key}>", handler)
@@ -1313,6 +1343,7 @@ 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_command(label="Zoom to Fit", command=do_zoom_fit, accelerator=f"{accel_mod}+9")
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)
@@ -1357,6 +1388,9 @@ 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)
# U+26F6, the four-corners frame every other editor marks "fit to window" with.
ttk.Button(toolbar, text="⛶", width=3, command=do_zoom_fit).pack(side=tk.LEFT, padx=(3, 0))
paned_window = ttk.PanedWindow(window, orient=tk.HORIZONTAL)
paned_window.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)