Fix live-device slot ID resolution, add print and tighten wire routing
_slot_name_by_id() now resolves SOX link slot IDs against the inheritance-flattened final_components_registry instead of the per-type slot_registry whose local IDs were wrong for types with intermediate parents (e.g. RegFloat→Register→Component). This produced incorrect link endpoints, inflated linked-slot visibility, and different box sizes between file and live modes. Also: tighten routing constants for denser wiring, add wiresheet PNG print/export (Ctrl+P), sort link routing jobs by gap distance, remove debug route logging, and fix live tree-expand re-selection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -633,12 +633,8 @@ And two things worth knowing about the manifests themselves rather than the code
|
|||||||
|
|
||||||
## Known gaps
|
## Known gaps
|
||||||
|
|
||||||
- The yellow square in each box header is drawn but wired to nothing; it is the natural
|
- The yellow square in each box header is a type-icon placeholder that is drawn but not yet
|
||||||
place for a per-box collapse/expand toggle.
|
functional; no icon set has been wired to it.
|
||||||
- Undo covers box moves, adds and deletes. Nothing else pushes a command, so a change made
|
|
||||||
outside those three is not reversible.
|
|
||||||
- `on_canvas_release` snaps using the rounded-rectangle polygon's first coordinate,
|
|
||||||
which is `x1 + radius` rather than `x1`, so snapping is offset by the corner radius.
|
|
||||||
- Slot values are edited from the Properties view only; a wiresheet box row is still
|
- Slot values are edited from the Properties view only; a wiresheet box row is still
|
||||||
display-only.
|
display-only.
|
||||||
- The Link View stacks each side in one column, so a component with a dozen peers makes a
|
- The Link View stacks each side in one column, so a component with a dozen peers makes a
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
|||||||
import configparser
|
import configparser
|
||||||
import os
|
import os
|
||||||
|
|
||||||
VERSION = "0.0.0.092"
|
VERSION = "0.0.0.093"
|
||||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||||
DEFAULT_GRID_SIZE = 10
|
DEFAULT_GRID_SIZE = 10
|
||||||
DEFAULT_ADD_TIMESTAMP = True
|
DEFAULT_ADD_TIMESTAMP = True
|
||||||
|
|||||||
@@ -339,6 +339,17 @@ class DeviceSession:
|
|||||||
"to": f"{to_path}.{to_slot_name}"})
|
"to": f"{to_path}.{to_slot_name}"})
|
||||||
|
|
||||||
def _slot_name_by_id(self, kit_id, type_id_val, slot_id):
|
def _slot_name_by_id(self, kit_id, type_id_val, slot_id):
|
||||||
|
# SOX links use absolute slot IDs across the full inheritance chain.
|
||||||
|
# final_components_registry has correctly resolved absolute IDs;
|
||||||
|
# the per-type slot_registry only stores local (per-type) IDs
|
||||||
|
# which are wrong for types with intermediate parents.
|
||||||
|
type_name = self.sox_client.type_name_map.get((kit_id, type_id_val))
|
||||||
|
if type_name and type_name in app_state.final_components_registry:
|
||||||
|
for s in app_state.final_components_registry[type_name]:
|
||||||
|
if s["slot_id"] == slot_id:
|
||||||
|
return s["name"]
|
||||||
|
# Fallback to slot_registry (local IDs — wrong for inherited types,
|
||||||
|
# but keeps things working when the schema registry is not yet loaded)
|
||||||
slots = self.sox_client.slot_registry.get((kit_id, type_id_val))
|
slots = self.sox_client.slot_registry.get((kit_id, type_id_val))
|
||||||
if slots:
|
if slots:
|
||||||
for s in slots:
|
for s in slots:
|
||||||
|
|||||||
@@ -635,7 +635,7 @@ def on_tree_select(event):
|
|||||||
tree.after_idle(_clear_restoring_flag)
|
tree.after_idle(_clear_restoring_flag)
|
||||||
app_state.current_sheet_parent = element
|
app_state.current_sheet_parent = element
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
import traceback; traceback.print_exc()
|
||||||
|
|
||||||
if element is not None:
|
if element is not None:
|
||||||
components_to_render = [child for child in element if child.tag == "comp"]
|
components_to_render = [child for child in element if child.tag == "comp"]
|
||||||
@@ -1351,6 +1351,8 @@ def attach_tree_scrollbars(container, tree):
|
|||||||
tree.bind("<Configure>", lambda event: fit_tree_column(tree), add="+")
|
tree.bind("<Configure>", lambda event: fit_tree_column(tree), add="+")
|
||||||
def _on_tree_open(event):
|
def _on_tree_open(event):
|
||||||
tree.after_idle(fit_tree_column, tree)
|
tree.after_idle(fit_tree_column, tree)
|
||||||
|
if _restoring_selection:
|
||||||
|
return
|
||||||
if app_state.live_mode:
|
if app_state.live_mode:
|
||||||
opened = tree.focus()
|
opened = tree.focus()
|
||||||
if opened:
|
if opened:
|
||||||
@@ -2091,14 +2093,20 @@ def _live_tree_expand(comp_id, tree_item_id):
|
|||||||
ds.build_links_elements(sax_root)
|
ds.build_links_elements(sax_root)
|
||||||
|
|
||||||
def _ui():
|
def _ui():
|
||||||
|
global _restoring_selection
|
||||||
app_state.xml_root_element = sax_root
|
app_state.xml_root_element = sax_root
|
||||||
sax_file.refresh_treeview_from_dom(sax_root)
|
sax_file.refresh_treeview_from_dom(sax_root)
|
||||||
# Re-open and select the node the user clicked
|
# Re-open and re-select the node without triggering a wiresheet
|
||||||
|
# re-render — the sheet is already showing the right content and
|
||||||
|
# a redundant close_wiresheet + open_wiresheet cycle can lose links.
|
||||||
for iid, elem in app_state.tree_element_map.items():
|
for iid, elem in app_state.tree_element_map.items():
|
||||||
if elem.get("id") == str(comp_id):
|
if elem.get("id") == str(comp_id):
|
||||||
|
app_state.current_sheet_parent = elem
|
||||||
|
_restoring_selection = True
|
||||||
tree.item(iid, open=True)
|
tree.item(iid, open=True)
|
||||||
tree.selection_set(iid)
|
tree.selection_set(iid)
|
||||||
tree.see(iid)
|
tree.see(iid)
|
||||||
|
tree.after_idle(_clear_restoring_flag)
|
||||||
break
|
break
|
||||||
app_state.set_status(f"Loaded {len(children)} children")
|
app_state.set_status(f"Loaded {len(children)} children")
|
||||||
|
|
||||||
@@ -2171,6 +2179,7 @@ SHORTCUTS = [
|
|||||||
("KP_0", do_zoom_reset),
|
("KP_0", do_zoom_reset),
|
||||||
("9", do_zoom_fit),
|
("9", do_zoom_fit),
|
||||||
("KP_9", do_zoom_fit),
|
("KP_9", do_zoom_fit),
|
||||||
|
("p", wiresheet.print_sheet),
|
||||||
]
|
]
|
||||||
for key, handler in SHORTCUTS:
|
for key, handler in SHORTCUTS:
|
||||||
window.bind_all(f"<Command-{key}>", handler)
|
window.bind_all(f"<Command-{key}>", handler)
|
||||||
@@ -2211,6 +2220,8 @@ menu_file.add_command(label="Open", command=sax_file.open_file)
|
|||||||
menu_file.add_command(label="Save", command=do_save, accelerator=f"{accel_mod}+S")
|
menu_file.add_command(label="Save", command=do_save, accelerator=f"{accel_mod}+S")
|
||||||
menu_file.add_command(label="Save As...", command=sax_file.save_file_as)
|
menu_file.add_command(label="Save As...", command=sax_file.save_file_as)
|
||||||
menu_file.add_separator()
|
menu_file.add_separator()
|
||||||
|
menu_file.add_command(label="Print...", command=wiresheet.print_sheet, accelerator=f"{accel_mod}+P")
|
||||||
|
menu_file.add_separator()
|
||||||
menu_file.add_command(label="Exit", command=window.quit)
|
menu_file.add_command(label="Exit", command=window.quit)
|
||||||
menu_bar.add_cascade(label="File", menu=menu_file)
|
menu_bar.add_cascade(label="File", menu=menu_file)
|
||||||
|
|
||||||
@@ -2308,6 +2319,12 @@ _btn_tidy = ttk.Button(toolbar, text="⊞", width=3, command=do_tidy_layout)
|
|||||||
_btn_tidy.pack(side=tk.LEFT, padx=(3, 0))
|
_btn_tidy.pack(side=tk.LEFT, padx=(3, 0))
|
||||||
_ToolTip(_btn_tidy, "Tidy Layout (Ctrl+W)")
|
_ToolTip(_btn_tidy, "Tidy Layout (Ctrl+W)")
|
||||||
|
|
||||||
|
ttk.Separator(toolbar, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=8, pady=2)
|
||||||
|
|
||||||
|
_btn_print = ttk.Button(toolbar, text="🖨", width=3, command=wiresheet.print_sheet)
|
||||||
|
_btn_print.pack(side=tk.LEFT)
|
||||||
|
_ToolTip(_btn_print, "Print Wiresheet (Ctrl+P)")
|
||||||
|
|
||||||
paned_window = ttk.PanedWindow(window, orient=tk.HORIZONTAL)
|
paned_window = ttk.PanedWindow(window, orient=tk.HORIZONTAL)
|
||||||
paned_window.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
paned_window.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
|||||||
+189
-9
@@ -1,6 +1,8 @@
|
|||||||
"""Canvas painting: the grid, component boxes, and the routed link wires."""
|
"""Canvas painting: the grid, component boxes, and the routed link wires."""
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
import tkinter.font as tkfont
|
import tkinter.font as tkfont
|
||||||
|
|
||||||
@@ -9,8 +11,9 @@ import schema
|
|||||||
import undo
|
import undo
|
||||||
from sax_dom import get_component_path, read_meta_property, update_meta_property
|
from sax_dom import get_component_path, read_meta_property, update_meta_property
|
||||||
|
|
||||||
LINK_STUB = 12 # straight run leaving a slot before the wire is allowed to turn
|
|
||||||
LINK_CLEAR = 8 # gap kept between a horizontal wire lane and any box above or below
|
LINK_STUB = 8 # straight run leaving a slot before the wire is allowed to turn
|
||||||
|
LINK_CLEAR = 5 # gap kept between a horizontal wire lane and any box above or below
|
||||||
LINK_EPS = 0.5 # tolerance so a wire touching its own box edge is not a collision
|
LINK_EPS = 0.5 # tolerance so a wire touching its own box edge is not a collision
|
||||||
|
|
||||||
# Vertical wire runs. A run hugs the box it leaves or enters instead of drifting down the
|
# Vertical wire runs. A run hugs the box it leaves or enters instead of drifting down the
|
||||||
@@ -18,9 +21,9 @@ LINK_EPS = 0.5 # tolerance so a wire touching its own box edge is not a col
|
|||||||
# the closest it is ever allowed to get, and two runs sharing one gap stand
|
# the closest it is ever allowed to get, and two runs sharing one gap stand
|
||||||
# WIRE_LANE_GAP apart. A gap therefore has to be WIRE_HUG either side plus one
|
# WIRE_LANE_GAP apart. A gap therefore has to be WIRE_HUG either side plus one
|
||||||
# WIRE_LANE_GAP per extra run, which is why TIDY_GAP below is exactly 2 * WIRE_HUG.
|
# WIRE_LANE_GAP per extra run, which is why TIDY_GAP below is exactly 2 * WIRE_HUG.
|
||||||
WIRE_HUG = 15
|
WIRE_HUG = 10
|
||||||
WIRE_MIN_CLEAR = 10
|
WIRE_MIN_CLEAR = 6
|
||||||
WIRE_LANE_GAP = 10
|
WIRE_LANE_GAP = 6
|
||||||
|
|
||||||
# No wire may pass under a component, and that outranks hugging: when the hug band is
|
# No wire may pass under a component, and that outranks hugging: when the hug band is
|
||||||
# already taken, a run may stand off this far instead so the wire can still be drawn
|
# already taken, a run may stand off this far instead so the wire can still be drawn
|
||||||
@@ -32,7 +35,7 @@ WIRE_STANDOFF_MAX = 60
|
|||||||
# a free horizontal lane is nudged until it clears any other wire's by this much. The
|
# a free horizontal lane is nudged until it clears any other wire's by this much. The
|
||||||
# short stubs joining a slot cannot move -- they have to meet their pin -- so a fan-out
|
# short stubs joining a slot cannot move -- they have to meet their pin -- so a fan-out
|
||||||
# from one output pin still shares the run right beside the pin.
|
# from one output pin still shares the run right beside the pin.
|
||||||
WIRE_ROW_GAP = 5
|
WIRE_ROW_GAP = 3
|
||||||
LINK_COLOR = "#2f6fba"
|
LINK_COLOR = "#2f6fba"
|
||||||
KNOB_OFFSHEET_COLOR = "#9aa0a6" # a link whose far end is on another wiresheet
|
KNOB_OFFSHEET_COLOR = "#9aa0a6" # a link whose far end is on another wiresheet
|
||||||
KNOB_RADIUS = 4
|
KNOB_RADIUS = 4
|
||||||
@@ -55,7 +58,7 @@ META_COORD_MAX = 255 # meta packs x and y into one byte each
|
|||||||
# agree on it without anything having to be recorded in the file.
|
# agree on it without anything having to be recorded in the file.
|
||||||
META_SCALE_MAX = 12
|
META_SCALE_MAX = 12
|
||||||
|
|
||||||
TIDY_GAP = 30 # minimum clear space kept between two boxes
|
TIDY_GAP = 20 # minimum clear space kept between two boxes
|
||||||
TIDY_MARGIN = 30 # minimum clear space kept from the top and left canvas edges
|
TIDY_MARGIN = 30 # minimum clear space kept from the top and left canvas edges
|
||||||
|
|
||||||
# The scales the sheet can be painted at. A ladder rather than a free multiplier, so
|
# The scales the sheet can be painted at. A ladder rather than a free multiplier, so
|
||||||
@@ -455,7 +458,8 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
they are refused when they would hide under another wire: the wire is better
|
they are refused when they would hide under another wire: the wire is better
|
||||||
off detouring, which puts it on a lane it can actually move.
|
off detouring, which puts it on a lane it can actually move.
|
||||||
"""
|
"""
|
||||||
for cx in channel_candidates(src_box, dst_box, boxes, low, high):
|
candidates = channel_candidates(src_box, dst_box, boxes, low, high)
|
||||||
|
for cx in candidates:
|
||||||
lane = free_lane(cx, run_lo, run_hi, boxes, claimed, low, high, max_steps)
|
lane = free_lane(cx, run_lo, run_hi, boxes, claimed, low, high, max_steps)
|
||||||
if lane is None:
|
if lane is None:
|
||||||
continue
|
continue
|
||||||
@@ -541,8 +545,13 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
# First pass: only the straight route, whose horizontal runs are pinned to their
|
# First pass: only the straight route, whose horizontal runs are pinned to their
|
||||||
# slots and cannot be moved later. Letting these claim their rows first stops a
|
# slots and cannot be moved later. Letting these claim their rows first stops a
|
||||||
# detouring wire from parking its movable lane on top of one of them.
|
# detouring wire from parking its movable lane on top of one of them.
|
||||||
|
attempt_names = ("straight(1)",)
|
||||||
attempts = (lambda: straight_through(1),)
|
attempts = (lambda: straight_through(1),)
|
||||||
else:
|
else:
|
||||||
|
attempt_names = (
|
||||||
|
"straight(1)", "around(hug)", "straight(8)", "around(standoff)",
|
||||||
|
"straight(1,stack)", "straight(8,stack)", "around(standoff,stack)",
|
||||||
|
)
|
||||||
attempts = (
|
attempts = (
|
||||||
lambda: straight_through(1), # hugging, own lane
|
lambda: straight_through(1), # hugging, own lane
|
||||||
lambda: around_boxes(WIRE_HUG), # hugging, routed around
|
lambda: around_boxes(WIRE_HUG), # hugging, routed around
|
||||||
@@ -555,7 +564,7 @@ def route_link_path(src_box, src_y, dst_box, dst_y, boxes, taken=None,
|
|||||||
lambda: straight_through(8, allow_stack=True),
|
lambda: straight_through(8, allow_stack=True),
|
||||||
lambda: around_boxes(WIRE_STANDOFF_MAX, allow_stack=True),
|
lambda: around_boxes(WIRE_STANDOFF_MAX, allow_stack=True),
|
||||||
)
|
)
|
||||||
for attempt in attempts:
|
for name, attempt in zip(attempt_names, attempts):
|
||||||
candidate, runs = attempt()
|
candidate, runs = attempt()
|
||||||
if candidate is not None:
|
if candidate is not None:
|
||||||
if taken is not None:
|
if taken is not None:
|
||||||
@@ -703,6 +712,10 @@ def draw_links():
|
|||||||
app_state.canvas_link_map[wire_tag] = link
|
app_state.canvas_link_map[wire_tag] = link
|
||||||
want_knob(knob_x, slot_y, "off_sheet", wire_tag)
|
want_knob(knob_x, slot_y, "off_sheet", wire_tag)
|
||||||
|
|
||||||
|
# Sort by horizontal gap distance so shorter, simpler links claim lanes first.
|
||||||
|
# Ties are broken by vertical span, keeping the cheapest wires at the front.
|
||||||
|
jobs.sort(key=lambda j: (abs(j[3][0] - j[1][2]), abs(j[4] - j[2])))
|
||||||
|
|
||||||
# Route in two passes. A wire that can run straight has its horizontal segments
|
# Route in two passes. A wire that can run straight has its horizontal segments
|
||||||
# pinned to its two slots, so it gets first claim on those rows; wires that have to
|
# pinned to its two slots, so it gets first claim on those rows; wires that have to
|
||||||
# detour then pick lanes that keep clear of them. Routing in one pass lets a detour
|
# detour then pick lanes that keep clear of them. Routing in one pass lets a detour
|
||||||
@@ -1319,3 +1332,170 @@ def tidy_layout():
|
|||||||
render_current_sheet()
|
render_current_sheet()
|
||||||
|
|
||||||
return len(changes), clamped
|
return len(changes), clamped
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Print (PNG export via screen capture)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_PRINT_PAD = 20 # breathing room around content in the saved image
|
||||||
|
|
||||||
|
|
||||||
|
def print_sheet(event=None):
|
||||||
|
"""Saves the wiresheet as a PNG image sized to its content and opens it.
|
||||||
|
|
||||||
|
The canvas is temporarily scrolled and resized so that every component and wire
|
||||||
|
is visible, then captured with PIL.ImageGrab. The grid is hidden during the
|
||||||
|
capture. The image size adapts automatically: a sparse sheet produces a small
|
||||||
|
image, a crowded one a larger image. When the content at the current zoom is
|
||||||
|
larger than the screen, the zoom is temporarily reduced so the full sheet fits.
|
||||||
|
"""
|
||||||
|
canvas = app_state.canvas
|
||||||
|
if canvas is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
bounds = sheet_content_bounds()
|
||||||
|
if bounds is None:
|
||||||
|
app_state.set_status("Nothing to print.")
|
||||||
|
return
|
||||||
|
|
||||||
|
from PIL import ImageGrab
|
||||||
|
from tkinter import filedialog
|
||||||
|
|
||||||
|
# Build a default file name from the file name and current sheet path.
|
||||||
|
title_parts = []
|
||||||
|
if app_state.current_file_path:
|
||||||
|
title_parts.append(os.path.splitext(os.path.basename(app_state.current_file_path))[0])
|
||||||
|
if app_state.current_sheet_parent is not None:
|
||||||
|
path = get_component_path(app_state.current_sheet_parent,
|
||||||
|
app_state.xml_root_element)
|
||||||
|
if path:
|
||||||
|
title_parts.append(path.replace("/", "_"))
|
||||||
|
default_name = ("_".join(title_parts) if title_parts else "wiresheet") + ".png"
|
||||||
|
|
||||||
|
png_path = filedialog.asksaveasfilename(
|
||||||
|
parent=app_state.window,
|
||||||
|
title="Save Wiresheet as PNG",
|
||||||
|
defaultextension=".png",
|
||||||
|
initialfile=default_name,
|
||||||
|
filetypes=[("PNG Image", "*.png"), ("All Files", "*.*")],
|
||||||
|
)
|
||||||
|
if not png_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
pad = _PRINT_PAD
|
||||||
|
|
||||||
|
# Work out how large the content is at 1:1 and decide a print zoom that keeps
|
||||||
|
# the whole sheet within the available screen area.
|
||||||
|
content_w = bounds[2] - bounds[0] + 2 * pad
|
||||||
|
content_h = bounds[3] - bounds[1] + 2 * pad
|
||||||
|
|
||||||
|
screen_w = canvas.winfo_screenwidth() - 100
|
||||||
|
screen_h = canvas.winfo_screenheight() - 100
|
||||||
|
max_zoom = min(screen_w / content_w, screen_h / content_h, app_state.zoom)
|
||||||
|
# Don't zoom below the minimum ladder level.
|
||||||
|
print_zoom = max(max_zoom, ZOOM_LEVELS[0])
|
||||||
|
|
||||||
|
# The original zoom is restored after capture.
|
||||||
|
original_zoom = app_state.zoom
|
||||||
|
zoom_changed = abs(print_zoom - original_zoom) > 1e-9
|
||||||
|
|
||||||
|
# Content rectangle at the print zoom (canvas pixel coordinates).
|
||||||
|
x1 = max(0.0, bounds[0] * print_zoom - pad)
|
||||||
|
y1 = max(0.0, bounds[1] * print_zoom - pad)
|
||||||
|
x2 = bounds[2] * print_zoom + pad
|
||||||
|
y2 = bounds[3] * print_zoom + pad
|
||||||
|
capture_w = int(math.ceil(x2 - x1))
|
||||||
|
capture_h = int(math.ceil(y2 - y1))
|
||||||
|
|
||||||
|
# Remember the canvas's current geometry so it can be restored afterwards.
|
||||||
|
old_scrollregion = canvas.cget("scrollregion")
|
||||||
|
old_width = canvas.winfo_width()
|
||||||
|
old_height = canvas.winfo_height()
|
||||||
|
old_xview = canvas.xview()
|
||||||
|
old_yview = canvas.yview()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Re-render at the print zoom if needed.
|
||||||
|
if zoom_changed:
|
||||||
|
app_state.zoom = print_zoom
|
||||||
|
render_current_sheet()
|
||||||
|
|
||||||
|
# Hide the grid.
|
||||||
|
grid_items = canvas.find_withtag("grid")
|
||||||
|
for item in grid_items:
|
||||||
|
canvas.itemconfigure(item, state="hidden")
|
||||||
|
|
||||||
|
# Expand the canvas widget and scroll region so the full content area is
|
||||||
|
# rendered on-screen in one shot.
|
||||||
|
canvas.config(
|
||||||
|
width=capture_w,
|
||||||
|
height=capture_h,
|
||||||
|
scrollregion=(x1, y1, x2, y2),
|
||||||
|
)
|
||||||
|
canvas.xview_moveto(0.0)
|
||||||
|
canvas.yview_moveto(0.0)
|
||||||
|
|
||||||
|
# Force Tk to process the resize and repaint before the grab.
|
||||||
|
canvas.update_idletasks()
|
||||||
|
canvas.update()
|
||||||
|
|
||||||
|
# Grab the canvas area from the screen.
|
||||||
|
cx = canvas.winfo_rootx()
|
||||||
|
cy = canvas.winfo_rooty()
|
||||||
|
grab_w = min(capture_w, canvas.winfo_width())
|
||||||
|
grab_h = min(capture_h, canvas.winfo_height())
|
||||||
|
image = ImageGrab.grab(bbox=(cx, cy, cx + grab_w, cy + grab_h))
|
||||||
|
|
||||||
|
image.save(png_path)
|
||||||
|
|
||||||
|
# Restore canvas geometry.
|
||||||
|
canvas.config(
|
||||||
|
width=old_width,
|
||||||
|
height=old_height,
|
||||||
|
scrollregion=old_scrollregion,
|
||||||
|
)
|
||||||
|
canvas.xview_moveto(old_xview[0])
|
||||||
|
canvas.yview_moveto(old_yview[0])
|
||||||
|
|
||||||
|
# Restore the grid.
|
||||||
|
for item in grid_items:
|
||||||
|
canvas.itemconfigure(item, state="normal")
|
||||||
|
|
||||||
|
# Restore the original zoom if it was changed.
|
||||||
|
if zoom_changed:
|
||||||
|
app_state.zoom = original_zoom
|
||||||
|
render_current_sheet()
|
||||||
|
else:
|
||||||
|
canvas.update_idletasks()
|
||||||
|
|
||||||
|
# Open the saved image with the system viewer.
|
||||||
|
if sys.platform == "win32":
|
||||||
|
os.startfile(png_path)
|
||||||
|
else:
|
||||||
|
import subprocess
|
||||||
|
opener = "open" if sys.platform == "darwin" else "xdg-open"
|
||||||
|
subprocess.Popen([opener, png_path])
|
||||||
|
|
||||||
|
app_state.set_status(f"Wiresheet saved: {png_path}")
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
# Restore everything on error.
|
||||||
|
try:
|
||||||
|
canvas.config(
|
||||||
|
width=old_width,
|
||||||
|
height=old_height,
|
||||||
|
scrollregion=old_scrollregion,
|
||||||
|
)
|
||||||
|
canvas.xview_moveto(old_xview[0])
|
||||||
|
canvas.yview_moveto(old_yview[0])
|
||||||
|
for item in canvas.find_withtag("grid"):
|
||||||
|
canvas.itemconfigure(item, state="normal")
|
||||||
|
if zoom_changed:
|
||||||
|
app_state.zoom = original_zoom
|
||||||
|
render_current_sheet()
|
||||||
|
else:
|
||||||
|
canvas.update_idletasks()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
app_state.set_status(f"Print failed: {exc}", warn=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user