sedona_editor/main.py
arda.aydin@ontrol.com.tr 9c283c7eea First Commit
Sedona sax editor AI yardimi ile yaziliyor
2026-08-13 17:59:08 +03:00

760 lines
26 KiB
Python

"""Sedona SAX Tree View Viewer & Program Editor — UI wiring, dialogs and event handlers."""
import os
import sys
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import app_state
import palette
import sax_file
import undo
import wiresheet
from sax_dom import read_meta_property, update_meta_property
def refresh_all_views():
"""Rebuilds tree and canvas from the DOM. Registered as undo/redo's view refresh."""
sax_file.refresh_treeview_from_dom(app_state.xml_root_element)
wiresheet.render_current_sheet()
def do_undo(event=None):
label = undo.undo()
if label is None:
return "break"
app_state.window.title(
f"Sedona SAX Tree View Viewer & Program Editor [v{app_state.VERSION}] — undid {label}"
)
return "break"
def do_redo(event=None):
label = undo.redo()
if label is None:
return "break"
app_state.window.title(
f"Sedona SAX Tree View Viewer & Program Editor [v{app_state.VERSION}] — redid {label}"
)
return "break"
def do_save(event=None):
sax_file.execute_direct_overwrite_save()
return "break"
def do_tidy_layout(event=None):
"""Menu command: re-flow the open sheet so nothing overlaps."""
if not app_state.canvas_comp_map:
messagebox.showinfo("Tidy Layout", "Open a sheet with components first.")
return "break"
moved, clamped = wiresheet.tidy_layout()
if moved == 0:
messagebox.showinfo("Tidy Layout", "Nothing to move — no boxes overlap.")
elif clamped:
messagebox.showwarning(
"Tidy Layout",
f"Repositioned {moved} component(s).\n\n"
f"{clamped} hit the coordinate limit of {wiresheet.META_COORD_MAX} grid units "
"and were clamped, because meta stores x and y in one byte each. "
"Lower the grid size in Preferences to fit more in."
)
return "break"
# --- Event handlers ---
def on_tree_select(event):
"""Triggered when a branch node is clicked in the Navigation view."""
tree = app_state.tree
selected_items = tree.selection()
if not selected_items:
return
selected_id = selected_items[0]
element = app_state.tree_element_map.get(selected_id)
components_to_render = []
# Remembered so undo/redo and tidy can repaint this same sheet from the DOM.
app_state.current_sheet_parent = element
if element is not None:
components_to_render = [child for child in element if child.tag == "comp"]
else:
top_level_ids = tree.get_children(selected_id)
for t_id in top_level_ids:
comp_elem = app_state.tree_element_map.get(t_id)
if comp_elem is not None:
components_to_render.append(comp_elem)
wiresheet.render_components(components_to_render)
def delete_selected_component(event=None):
"""Deletes the highlighted canvas component and all its descendants recursively."""
if not app_state.selected_canvas_tag:
return
comp_element = app_state.canvas_comp_map.get(app_state.selected_canvas_tag)
if comp_element is not None:
sax_file.execute_cascading_deletion(comp_element)
def delete_tree_selected_component():
"""Triggered by the context popup menu to delete an item selected in the Treeview architecture."""
if not app_state.selected_tree_item_id:
return
comp_element = app_state.tree_element_map.get(app_state.selected_tree_item_id)
if comp_element is not None:
sax_file.execute_cascading_deletion(comp_element)
def on_tree_right_click(event):
"""Intercepts right-clicks on the treeview architecture to render branch context panels."""
tree = app_state.tree
item_id = tree.identify_row(event.y)
if not item_id:
return
tree.selection_set(item_id)
app_state.selected_tree_item_id = item_id
parent_id = tree.parent(item_id)
if parent_id == "":
tree_root_context_menu.post(event.x_root, event.y_root)
else:
tree_comp_context_menu.post(event.x_root, event.y_root)
def on_canvas_click(event):
canvas = app_state.canvas
canvas.itemconfig("block_outline", outline="#7f9db9", width=2)
app_state.drag_data["tag"] = None
app_state.selected_canvas_tag = None
cx = canvas.canvasx(event.x)
cy = canvas.canvasy(event.y)
clicked_item = canvas.find_closest(cx, cy)
if not clicked_item:
return
tags = canvas.gettags(clicked_item[0])
group_tag = next((t for t in tags if t.startswith("comp_group_")), None)
if group_tag:
app_state.drag_data["tag"] = group_tag
app_state.drag_data["x"] = cx
app_state.drag_data["y"] = cy
app_state.selected_canvas_tag = group_tag
# Snapshot the pre-drag meta so the move can be pushed as one undoable step.
dragged = app_state.canvas_comp_map.get(group_tag)
meta_prop = dragged.find("prop[@name='meta']") if dragged is not None else None
app_state.drag_data["meta_before"] = meta_prop.get("val") if meta_prop is not None else None
canvas.itemconfig(f"{group_tag} && block_outline", outline="#1f4e79", width=3)
# Bring the clicked box clear of any neighbour it happens to sit under.
canvas.tag_raise(group_tag)
wiresheet.draw_links()
def on_canvas_right_click(event):
"""Detects right-clicks on nodes to instantly show context-sensitive action overlays."""
canvas = app_state.canvas
cx = canvas.canvasx(event.x)
cy = canvas.canvasy(event.y)
clicked_item = canvas.find_closest(cx, cy)
if clicked_item:
tags = canvas.gettags(clicked_item[0])
group_tag = next((t for t in tags if t.startswith("comp_group_")), None)
if group_tag:
canvas.itemconfig("block_outline", outline="#7f9db9", width=2)
app_state.selected_canvas_tag = group_tag
canvas.itemconfig(f"{group_tag} && block_outline", outline="#1f4e79", width=3)
canvas_context_menu.post(event.x_root, event.y_root)
def on_canvas_drag(event):
if not app_state.drag_data["tag"]:
return
canvas = app_state.canvas
cx = canvas.canvasx(event.x)
cy = canvas.canvasy(event.y)
delta_x = cx - app_state.drag_data["x"]
delta_y = cy - app_state.drag_data["y"]
canvas.move(app_state.drag_data["tag"], delta_x, delta_y)
app_state.drag_data["x"] = cx
app_state.drag_data["y"] = cy
# Wires are not part of the dragged group, so they are re-routed against the new positions.
wiresheet.draw_links()
def on_canvas_release(event):
if not app_state.drag_data["tag"]:
return
canvas = app_state.canvas
grid_size = app_state.GRID_SIZE
group_tag = app_state.drag_data["tag"]
outline_item = canvas.find_withtag(f"{group_tag} && block_outline")
if outline_item:
coords = canvas.coords(outline_item[0])
current_x, current_y = coords[0], coords[1]
snapped_x = round(current_x / grid_size) * grid_size
snapped_y = round(current_y / grid_size) * grid_size
shift_x = snapped_x - current_x
shift_y = snapped_y - current_y
if shift_x != 0 or shift_y != 0:
canvas.move(group_tag, shift_x, shift_y)
app_state.mark_dirty()
raw_x = int(snapped_x / grid_size)
raw_y = int(snapped_y / grid_size)
comp_element = app_state.canvas_comp_map.get(group_tag)
if comp_element is not None:
meta_str = None
orig_prop = comp_element.find("prop[@name='meta']")
if orig_prop is not None:
meta_str = orig_prop.get("val")
orig_x, orig_y, _, _, _, _, _, _ = read_meta_property(meta_str)
if orig_x != raw_x or orig_y != raw_y:
meta_before = app_state.drag_data.get("meta_before", meta_str)
update_meta_property(comp_element, meta_str, raw_x, raw_y)
undo.push(undo.MetaMoveCommand(
[(comp_element, meta_before, raw_x, raw_y)],
label=f"move {comp_element.get('name', 'component')}"
))
app_state.mark_dirty()
app_state.drag_data["tag"] = None
wiresheet.draw_links()
# --- Dialogs ---
def open_preferences_dialog():
"""Pops open a tabular modal dialog to edit configuration attributes."""
window = app_state.window
dialog = tk.Toplevel(window)
dialog.title("Preferences")
dialog.geometry("470x250")
dialog.resizable(False, False)
dialog.transient(window)
dialog.grab_set()
dialog.geometry(f"+{window.winfo_x() + 100}+{window.winfo_y() + 100}")
frame_padded = ttk.Frame(dialog, padding=15)
frame_padded.pack(fill=tk.BOTH, expand=True)
frame_padded.columnconfigure(0, weight=1, minsize=160)
frame_padded.columnconfigure(1, weight=2)
lbl_home = ttk.Label(frame_padded, text="Sedona home folder:", anchor="w")
lbl_home.grid(row=0, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
frame_home_picker = ttk.Frame(frame_padded)
frame_home_picker.grid(row=0, column=1, sticky="ew", pady=(0, 10))
frame_home_picker.columnconfigure(0, weight=1)
entry_home = ttk.Entry(frame_home_picker)
entry_home.insert(0, app_state.SEDONA_HOME)
entry_home.grid(row=0, column=0, sticky="ew", padx=(0, 5))
def browse_home_folder():
chosen_dir = filedialog.askdirectory(title="Select Sedona Home Folder", initialdir=entry_home.get() or None)
if chosen_dir:
entry_home.delete(0, tk.END)
entry_home.insert(0, os.path.normpath(chosen_dir))
btn_home_browse = ttk.Button(frame_home_picker, text="...", width=3, command=browse_home_folder)
btn_home_browse.grid(row=0, column=1, sticky="e")
lbl_size = ttk.Label(frame_padded, text="Grid size (pixels):", anchor="w")
lbl_size.grid(row=1, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
entry_grid = ttk.Entry(frame_padded)
entry_grid.insert(0, str(app_state.GRID_SIZE))
entry_grid.grid(row=1, column=1, sticky="ew", pady=(0, 10))
lbl_box = ttk.Label(frame_padded, text="Component box width (pixels):", anchor="w")
lbl_box.grid(row=2, column=0, sticky="w", pady=(0, 10), padx=(0, 10))
entry_box_width = ttk.Entry(frame_padded)
entry_box_width.insert(0, str(app_state.BOX_WIDTH))
entry_box_width.grid(row=2, column=1, sticky="ew", pady=(0, 10))
lbl_timestamp = ttk.Label(frame_padded, text="Save with Date-Time stamp?", anchor="w")
lbl_timestamp.grid(row=3, column=0, sticky="w", pady=(0, 15), padx=(0, 10))
var_timestamp = tk.BooleanVar(value=app_state.ADD_TIMESTAMP)
chk_timestamp = ttk.Checkbutton(
frame_padded,
text="Yes",
variable=var_timestamp
)
chk_timestamp.grid(row=3, column=1, sticky="w", pady=(0, 15))
def apply_preferences():
try:
val = int(entry_grid.get().strip())
if val < 2 or val > 200:
raise ValueError("Grid size must be a whole number between 2 and 200.")
try:
width = int(entry_box_width.get().strip())
except ValueError:
raise ValueError(
f"Box width must be a whole number between "
f"{app_state.MIN_ALLOWED_BOX_WIDTH} and {app_state.MAX_ALLOWED_BOX_WIDTH}."
)
if not (app_state.MIN_ALLOWED_BOX_WIDTH <= width <= app_state.MAX_ALLOWED_BOX_WIDTH):
raise ValueError(
f"Box width must be between {app_state.MIN_ALLOWED_BOX_WIDTH} and "
f"{app_state.MAX_ALLOWED_BOX_WIDTH} pixels."
)
app_state.BOX_WIDTH = width
app_state.GRID_SIZE = val
app_state.ADD_TIMESTAMP = var_timestamp.get()
app_state.SEDONA_HOME = entry_home.get().strip()
app_state.save_preferences()
wiresheet.draw_grid()
if app_state.active_components_list:
wiresheet.render_components(app_state.active_components_list)
# Sedona home may have moved, so the palette's kit list is rebuilt.
refresh_palette_kits()
dialog.destroy()
except ValueError as error:
message = str(error) or "Please enter a valid integer size between 2 and 200."
messagebox.showerror("Invalid Input", message, parent=dialog)
btn_save_pref = ttk.Button(frame_padded, text="Apply Changes", command=apply_preferences)
btn_save_pref.grid(row=4, column=1, sticky="e")
if not app_state.SEDONA_HOME:
entry_home.focus_set()
else:
entry_grid.focus_set()
def show_app_info():
"""Displays information tracking details for the currently active schema file in standard columns."""
window = app_state.window
if app_state.current_file_path:
file_name = os.path.basename(app_state.current_file_path)
folder_path = os.path.dirname(app_state.current_file_path)
else:
file_name = "No profile loaded"
folder_path = "None"
lastReachedID = 0
all_found_ids = set()
if app_state.xml_root_element is not None:
for elem in app_state.xml_root_element.iter():
if elem.tag == "comp":
id_str = elem.get("id")
if id_str is not None:
try:
val = int(id_str)
all_found_ids.add(val)
if val > lastReachedID:
lastReachedID = val
except ValueError:
pass
not_used_ids = [i for i in range(1, lastReachedID + 1) if i not in all_found_ids]
usedComponentNumber = lastReachedID - len(not_used_ids)
not_used_str = ", ".join(map(str, not_used_ids)) if not_used_ids else "None"
if len(not_used_str) > 40:
not_used_str = not_used_str[:37] + "..."
info_win = tk.Toplevel(window)
info_win.title("App Info")
info_win.resizable(False, False)
info_win.transient(window)
info_win.grab_set()
frame = ttk.Frame(info_win, padding=15)
frame.pack(fill=tk.BOTH, expand=True)
rows_data = [
("App File Name:", file_name),
("File Location:", folder_path),
("Last Reached ID:", str(lastReachedID)),
("Used Component Count:", str(max(0, usedComponentNumber))),
("Unused IDs:", not_used_str)
]
for idx, (lbl_txt, val_txt) in enumerate(rows_data):
lbl = tk.Label(frame, text=lbl_txt, font=("TkDefaultFont", 9, "bold"), anchor="w")
lbl.grid(row=idx, column=0, sticky="w", padx=(0, 15), pady=4)
val = tk.Label(frame, text=val_txt, anchor="w", justify="left")
val.grid(row=idx, column=1, sticky="w", pady=4)
info_win.update_idletasks()
x = window.winfo_x() + (window.winfo_width() // 2) - (info_win.winfo_width() // 2)
y = window.winfo_y() + (window.winfo_height() // 2) - (info_win.winfo_height() // 2)
info_win.geometry(f"+{x}+{y}")
def verify_startup_configurations():
"""Checks for configuration gaps, triggers an alert, and opens setup if empty."""
if not app_state.SEDONA_HOME:
messagebox.showwarning(
"Configuration Required",
"The 'Sedona home folder' path is not configured.\n\n"
"Please specify the directory path in the configuration options "
"before trying to view or load profile source files."
)
open_preferences_dialog()
# --- UI Setup ---
window = tk.Tk()
app_state.window = window
window.title(f"Sedona SAX Tree View Viewer & Program Editor [v{app_state.VERSION}]")
window.geometry("950x600")
# --- Context Popups Structures ---
canvas_context_menu = tk.Menu(window, tearoff=0)
canvas_context_menu.add_command(label="Delete Component", command=delete_selected_component)
tree_comp_context_menu = tk.Menu(window, tearoff=0)
tree_comp_context_menu.add_command(label="Delete Component", command=delete_tree_selected_component)
tree_root_context_menu = tk.Menu(window, tearoff=0)
tree_root_context_menu.add_command(label="Save", command=sax_file.execute_direct_overwrite_save)
# --- Keyboard shortcuts ---
# Command and Control are both bound on every platform, so muscle memory from either
# side works and Windows users are not asked to learn a Mac chord.
SHORTCUTS = [
("s", do_save),
("z", do_undo),
("y", do_redo),
]
for key, handler in SHORTCUTS:
window.bind_all(f"<Command-{key}>", handler)
window.bind_all(f"<Control-{key}>", handler)
# Redo also as Shift-Z, the usual mac chord.
window.bind_all("<Command-Shift-Z>", do_redo)
window.bind_all("<Control-Shift-Z>", do_redo)
# Delete: the mac Delete key reports as BackSpace, the Windows one as Delete.
window.bind_all("<Delete>", delete_selected_component)
window.bind_all("<BackSpace>", delete_selected_component)
accel_mod = "Cmd" if sys.platform == "darwin" else "Ctrl"
# --- Native Application Menu Configurations ---
menu_bar = tk.Menu(window)
menu_file = tk.Menu(menu_bar, tearoff=0)
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 As...", command=sax_file.save_file_as)
menu_file.add_separator()
menu_file.add_command(label="Exit", command=window.quit)
menu_bar.add_cascade(label="File", menu=menu_file)
menu_edit = tk.Menu(menu_bar, tearoff=0)
menu_edit.add_command(label="Undo", command=do_undo, accelerator=f"{accel_mod}+Z")
menu_edit.add_command(label="Redo", command=do_redo, accelerator=f"{accel_mod}+Y")
menu_edit.add_separator()
menu_edit.add_command(label="Delete Component", command=delete_selected_component, accelerator="Del")
menu_bar.add_cascade(label="Edit", menu=menu_edit)
menu_layout = tk.Menu(menu_bar, tearoff=0)
menu_layout.add_command(label="Tidy Layout", command=do_tidy_layout)
menu_bar.add_cascade(label="Layout", menu=menu_layout)
menu_app_info = tk.Menu(menu_bar, tearoff=0)
menu_app_info.add_command(label="App Info", command=show_app_info)
menu_bar.add_cascade(label="App", menu=menu_app_info)
menu_prefs = tk.Menu(menu_bar, tearoff=0)
menu_prefs.add_command(label="Preferences", command=open_preferences_dialog)
menu_bar.add_cascade(label="Preferences", menu=menu_prefs)
window.config(menu=menu_bar)
paned_window = ttk.PanedWindow(window, orient=tk.HORIZONTAL)
paned_window.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# Left column splits into the navigation tree above and the component palette below.
left_pane = ttk.PanedWindow(paned_window, orient=tk.VERTICAL)
frame_tree = ttk.Frame(left_pane)
tree_scrollbar = ttk.Scrollbar(frame_tree)
tree_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
tree = ttk.Treeview(
frame_tree,
yscrollcommand=tree_scrollbar.set,
show="tree",
selectmode="browse",
)
app_state.tree = tree
tree.pack(fill=tk.BOTH, expand=True)
tree_scrollbar.config(command=tree.yview)
tree.bind("<<TreeviewSelect>>", on_tree_select)
tree.bind("<ButtonPress-3>", on_tree_right_click)
tree.bind("<ButtonPress-2>", on_tree_right_click)
left_pane.add(frame_tree, weight=3)
# --- Component palette ---
frame_palette = ttk.Frame(left_pane)
ttk.Label(frame_palette, text="Palette", font=("TkDefaultFont", 9, "bold")).pack(
anchor="w", padx=4, pady=(4, 2)
)
palette_kit_var = tk.StringVar()
palette_kit_combo = ttk.Combobox(frame_palette, textvariable=palette_kit_var, state="readonly")
palette_kit_combo.pack(fill=tk.X, padx=4)
frame_palette_list = ttk.Frame(frame_palette)
frame_palette_list.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
palette_scrollbar = ttk.Scrollbar(frame_palette_list)
palette_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
palette_tree = ttk.Treeview(
frame_palette_list,
yscrollcommand=palette_scrollbar.set,
show="tree",
selectmode="browse",
)
palette_tree.pack(fill=tk.BOTH, expand=True)
palette_scrollbar.config(command=palette_tree.yview)
palette_status = ttk.Label(frame_palette, text="", foreground="#666666")
palette_status.pack(anchor="w", padx=4, pady=(0, 4))
def populate_palette_types(event=None):
"""Lists the component types of the kit chosen in the palette dropdown."""
for item in palette_tree.get_children():
palette_tree.delete(item)
index = palette_kit_combo.current()
if index < 0 or index >= len(palette_kit_names):
palette_status.config(text="")
return
kit_name = palette_kit_names[index]
types = palette.list_component_types(kit_name)
for type_name, base in types:
palette_tree.insert("", "end", text=type_name, values=(f"{kit_name}::{type_name}", base))
palette_status.config(text=f"{len(types)} component{'' if len(types) == 1 else 's'}")
def refresh_palette_kits():
"""Reloads the kit list from Sedona home. Also called after Preferences changes it."""
global palette_kit_names
palette.invalidate()
palette_kit_names = palette.list_kits()
labels = []
for kit_name in palette_kit_names:
version = palette.kit_version_label(kit_name)
labels.append(f"{kit_name} ({version})" if version else kit_name)
palette_kit_combo.config(values=labels)
for item in palette_tree.get_children():
palette_tree.delete(item)
if not palette_kit_names:
palette_status.config(
text="No kits found — check Sedona home" if app_state.SEDONA_HOME
else "Set Sedona home in Preferences"
)
palette_kit_var.set("")
return
palette_status.config(text=f"{len(palette_kit_names)} kits")
palette_kit_var.set("")
# --- Palette drag and drop ---
# Tk has no drag-and-drop between widgets, so the gesture is assembled by hand:
# remember what was pressed, start dragging past a small threshold, follow the pointer
# with a borderless ghost window, and on release test the pointer against the canvas.
DRAG_THRESHOLD = 6
palette_drag = {"kit": None, "type": None, "origin": None, "active": False, "ghost": None}
def _destroy_drag_ghost():
if palette_drag["ghost"] is not None:
palette_drag["ghost"].destroy()
palette_drag["ghost"] = None
def _reset_palette_drag():
_destroy_drag_ghost()
palette_drag.update(kit=None, type=None, origin=None, active=False)
def on_palette_press(event):
item = palette_tree.identify_row(event.y)
if not item:
_reset_palette_drag()
return
palette_tree.selection_set(item)
index = palette_kit_combo.current()
if index < 0 or index >= len(palette_kit_names):
return
palette_drag.update(
kit=palette_kit_names[index],
type=palette_tree.item(item, "text"),
origin=(event.x_root, event.y_root),
active=False,
)
def on_palette_motion(event):
if palette_drag["type"] is None or palette_drag["origin"] is None:
return
origin_x, origin_y = palette_drag["origin"]
moved = abs(event.x_root - origin_x) + abs(event.y_root - origin_y)
if not palette_drag["active"]:
if moved < DRAG_THRESHOLD:
return
palette_drag["active"] = True
ghost = tk.Toplevel(window)
ghost.overrideredirect(True)
ghost.attributes("-topmost", True)
tk.Label(
ghost, text=f"{palette_drag['type']}",
bg="#dce6f2", fg="black", relief="solid", borderwidth=1,
padx=6, pady=2, font=("TkDefaultFont", 9),
).pack()
palette_drag["ghost"] = ghost
if palette_drag["ghost"] is not None:
palette_drag["ghost"].geometry(f"+{event.x_root + 12}+{event.y_root + 10}")
def _pointer_over_canvas(x_root, y_root):
left = canvas.winfo_rootx()
top = canvas.winfo_rooty()
return (left <= x_root < left + canvas.winfo_width()
and top <= y_root < top + canvas.winfo_height())
def on_palette_release(event):
was_dragging = palette_drag["active"]
kit_name = palette_drag["kit"]
type_name = palette_drag["type"]
_reset_palette_drag()
if not was_dragging or not kit_name or not type_name:
return
if not _pointer_over_canvas(event.x_root, event.y_root):
return
if app_state.xml_root_element is None:
messagebox.showwarning("Add Component", "Open a SAX file first.")
return
# Pointer position -> canvas coordinates -> grid units, snapped like a dragged box.
drop_x = canvas.canvasx(event.x_root - canvas.winfo_rootx())
drop_y = canvas.canvasy(event.y_root - canvas.winfo_rooty())
grid_size = app_state.GRID_SIZE
grid_x = max(0, min(wiresheet.META_COORD_MAX, round(drop_x / grid_size)))
grid_y = max(0, min(wiresheet.META_COORD_MAX, round(drop_y / grid_size)))
sax_file.add_component(kit_name, type_name, grid_x, grid_y)
palette_kit_names = []
palette_kit_combo.bind("<<ComboboxSelected>>", populate_palette_types)
palette_tree.bind("<ButtonPress-1>", on_palette_press)
palette_tree.bind("<B1-Motion>", on_palette_motion)
palette_tree.bind("<ButtonRelease-1>", on_palette_release)
left_pane.add(frame_palette, weight=2)
paned_window.add(left_pane, weight=1)
frame_editor = ttk.Frame(paned_window)
canvas_vscroll = ttk.Scrollbar(frame_editor, orient=tk.VERTICAL)
canvas_vscroll.pack(side=tk.RIGHT, fill=tk.Y)
canvas_hscroll = ttk.Scrollbar(frame_editor, orient=tk.HORIZONTAL)
canvas_hscroll.pack(side=tk.BOTTOM, fill=tk.X)
canvas = tk.Canvas(
frame_editor,
bg="white",
highlightthickness=0,
xscrollcommand=canvas_hscroll.set,
yscrollcommand=canvas_vscroll.set,
scrollregion=(0, 0, 2000, 2000),
)
app_state.canvas = canvas
canvas.pack(fill=tk.BOTH, expand=True)
canvas_vscroll.config(command=canvas.yview)
canvas_hscroll.config(command=canvas.xview)
canvas.bind("<ButtonPress-1>", on_canvas_click)
canvas.bind("<B1-Motion>", on_canvas_drag)
canvas.bind("<ButtonRelease-1>", on_canvas_release)
canvas.bind("<Configure>", lambda e: wiresheet.draw_grid())
canvas.bind("<ButtonPress-3>", on_canvas_right_click)
canvas.bind("<ButtonPress-2>", on_canvas_right_click)
paned_window.add(frame_editor, weight=3)
undo.set_refresh_callback(refresh_all_views)
refresh_palette_kits()
window.after(100, verify_startup_configurations)
window.mainloop()