Render Ox widgets on the Page Editor canvas as the device draws them

The Page Editor canvas now paints widgets WYSIWYG instead of as
labelled placeholder boxes, following the ontrolOx draw() code and the
R-ION firmware:

- text drawn with the device's own bitmap fonts (fontBank .bin /
  fontBankXL .oxfl), Windows font fallback; alignment, clipping at the
  box edge, half-size subscript digits, transparent vs filled boxes;
  the line is centred on the box even when the font is taller
- each widget shows the text its `in` selects: NumericLabel formats,
  lookup items, Boolean true/false text and colours, date and time
- images from the local image banks, centred or cropped; missing ones
  drawn as the device's red/white checker; page background image
- 10x10 pixel grid over the page colour, with a Grid toggle
- widgets hit-tested by layout box so transparent labels stay
  clickable; hidden widgets shown as a dashed outline
- layout row gets an X/Y/Width/Height dialog
- scaled device frame cached per zoom (redraws ~15 ms)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 15:16:51 +03:00
co-authored by Claude Opus 5.5
parent 43d4849533
commit e5cb04b8cf
4 changed files with 662 additions and 65 deletions
+43 -1
View File
@@ -356,7 +356,7 @@ def build_props(comp):
if m.has(*LAYOUT_SLOTS):
consumed.update(LAYOUT_SLOTS)
rows.append(Prop("layout", "text", lambda: ", ".join(
str(m.int(n)) for n in LAYOUT_SLOTS), _layout_writer(m)))
str(m.int(n)) for n in LAYOUT_SLOTS), _layout_writer(m), dialog="layout"))
if m.has(*FONT_SLOTS):
consumed.update(FONT_SLOTS)
@@ -728,6 +728,9 @@ class OxWidgetPropertiesPanel:
def _build_text(self, cell, row):
prop = row["prop"]
if prop.dialog:
# Typed in place or edited field by field, as Workbench offers both.
self._dots(cell, getattr(self, f"_open_{prop.dialog}_dialog"))
if prop.unit:
tk.Label(cell, text=prop.unit, font=self._name_font, bg=SHEET_BG,
fg=READONLY_FG, padx=4).pack(side=tk.RIGHT)
@@ -852,6 +855,45 @@ class OxWidgetPropertiesPanel:
dialog.destroy()
self.commit(comp, values)
def _open_layout_dialog(self):
"""X, Y, Width and Height, one field each, as Workbench's layout editor has them."""
comp = self.comp
if comp is None:
return
self.flush_edits()
m = _Model(comp)
dialog, body = self._dialog("layout")
table = tk.Frame(body, bg=GRID_LINE)
table.grid(row=0, column=0, sticky="nsew")
tk.Label(table, text="", bg=NAME_BG, width=8).grid(row=0, column=0, sticky="nsew",
padx=(1, 1), pady=1)
tk.Label(table, text="Value", bg=NAME_BG, anchor="w", padx=4, width=16,
font=self._name_font).grid(row=0, column=1, sticky="nsew", padx=(0, 1), pady=1)
variables = {}
entries = []
for index, (name, label) in enumerate(zip(LAYOUT_SLOTS, ("X", "Y", "Width", "Height"))):
tk.Label(table, text=label, bg=SHEET_BG, anchor="w", padx=4,
font=self._name_font).grid(row=index + 1, column=0, sticky="nsew",
padx=(1, 1), pady=(0, 1))
var = tk.StringVar(value=str(m.int(name)))
entry = tk.Entry(table, textvariable=var, font=self._name_font, relief=tk.FLAT,
bg=SHEET_BG, highlightthickness=1, highlightcolor="#7f9db9",
highlightbackground=SHEET_BG, width=16)
entry.grid(row=index + 1, column=1, sticky="nsew", padx=(0, 1), pady=(0, 1))
variables[name] = var
entries.append(entry)
def apply():
text = ", ".join(variables[name].get().strip() or "?" for name in LAYOUT_SLOTS)
self._apply_values(dialog, comp, *_layout_writer(m)(text))
dialog.bind("<Return>", lambda e: apply())
self._finish_dialog(dialog, body, 1, apply)
entries[0].focus_set()
entries[0].select_range(0, tk.END)
def _open_font_dialog(self):
comp = self.comp
if comp is None: