Add Workbench-style property sheet to the Page Editor
The Page Editor's right pane now shows the selected widget's properties under the widget tree, laid out like Workbench's Ox editor rather than as raw Sedona slots: - layout (x, y, width, height), font (name, size, style) - alignment / transparent / subscriptDigits from the attributes bits - shape, RGB565 colours with a swatch and picker - DateLabelSet date options split out of dateFormatOptions - lookup labelText editor (prefix, items, suffix) and NumericLabel printf-format editor, min/max as one row - image name picker, slot link chooser for in/blink/visibility/touch - Buf text decoded from base64 (Latin-1), plain text in live mode Edits commit on Enter/focus-out/selection as one undo step and are written to the device in live mode. Canvas drag-moves now use the same path, so they are undoable and reach the device too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+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.128"
|
VERSION = "0.0.0.129"
|
||||||
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
|
||||||
|
|||||||
+72
-26
@@ -23,6 +23,10 @@ except ImportError:
|
|||||||
_HAS_PIL = False
|
_HAS_PIL = False
|
||||||
|
|
||||||
import app_state
|
import app_state
|
||||||
|
import live_hooks
|
||||||
|
import undo
|
||||||
|
from ox_widget_properties import OxWidgetPropertiesPanel
|
||||||
|
from sax_dom import set_prop_value
|
||||||
|
|
||||||
HEADER_BG = "#f2f2f2"
|
HEADER_BG = "#f2f2f2"
|
||||||
|
|
||||||
@@ -92,15 +96,6 @@ def _get_prop(comp, name, default=None):
|
|||||||
return prop.get("val", default) if prop is not None else default
|
return prop.get("val", default) if prop is not None else default
|
||||||
|
|
||||||
|
|
||||||
def _set_prop(comp, name, value):
|
|
||||||
prop = comp.find(f"prop[@name='{name}']")
|
|
||||||
if prop is not None:
|
|
||||||
prop.set("val", value)
|
|
||||||
else:
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
ET.SubElement(comp, "prop", name=name, val=value)
|
|
||||||
|
|
||||||
|
|
||||||
def _short_type(full_type):
|
def _short_type(full_type):
|
||||||
short = full_type.rsplit("::", 1)[-1] if "::" in full_type else full_type
|
short = full_type.rsplit("::", 1)[-1] if "::" in full_type else full_type
|
||||||
return _SHORT_TYPE_NAMES.get(short, short)
|
return _SHORT_TYPE_NAMES.get(short, short)
|
||||||
@@ -189,7 +184,7 @@ class OxPageViewPanel:
|
|||||||
self._build_header()
|
self._build_header()
|
||||||
self._build_body()
|
self._build_body()
|
||||||
self._build_footer()
|
self._build_footer()
|
||||||
self._render_page()
|
self.refresh()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _screen_canvas_x(self):
|
def _screen_canvas_x(self):
|
||||||
@@ -230,9 +225,16 @@ class OxPageViewPanel:
|
|||||||
self._build_canvas(canvas_frame)
|
self._build_canvas(canvas_frame)
|
||||||
body.add(canvas_frame, weight=1)
|
body.add(canvas_frame, weight=1)
|
||||||
|
|
||||||
tree_frame = ttk.Frame(body)
|
# Workbench's layout: the widget tree above, the selected widget's sheet below.
|
||||||
|
side = ttk.PanedWindow(body, orient=tk.VERTICAL)
|
||||||
|
body.add(side, weight=0)
|
||||||
|
|
||||||
|
tree_frame = ttk.Frame(side)
|
||||||
self._build_widget_tree(tree_frame)
|
self._build_widget_tree(tree_frame)
|
||||||
body.add(tree_frame, weight=0)
|
side.add(tree_frame, weight=1)
|
||||||
|
|
||||||
|
self._props = OxWidgetPropertiesPanel(side, self._commit_props, self._repaint_all)
|
||||||
|
side.add(self._props.frame, weight=3)
|
||||||
|
|
||||||
def _build_widget_tree(self, parent):
|
def _build_widget_tree(self, parent):
|
||||||
ttk.Label(parent, text="Widgets", font=("TkDefaultFont", 9, "bold")).pack(
|
ttk.Label(parent, text="Widgets", font=("TkDefaultFont", 9, "bold")).pack(
|
||||||
@@ -242,7 +244,7 @@ class OxPageViewPanel:
|
|||||||
tree_container.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
|
tree_container.pack(fill=tk.BOTH, expand=True, padx=4, pady=4)
|
||||||
|
|
||||||
self._tree = ttk.Treeview(tree_container, show="tree", selectmode="browse",
|
self._tree = ttk.Treeview(tree_container, show="tree", selectmode="browse",
|
||||||
height=12)
|
height=8)
|
||||||
vscroll = ttk.Scrollbar(tree_container, orient=tk.VERTICAL,
|
vscroll = ttk.Scrollbar(tree_container, orient=tk.VERTICAL,
|
||||||
command=self._tree.yview)
|
command=self._tree.yview)
|
||||||
self._tree.configure(yscrollcommand=vscroll.set)
|
self._tree.configure(yscrollcommand=vscroll.set)
|
||||||
@@ -386,6 +388,8 @@ class OxPageViewPanel:
|
|||||||
c.delete("selection_handle")
|
c.delete("selection_handle")
|
||||||
|
|
||||||
self._selected_widget = widget
|
self._selected_widget = widget
|
||||||
|
# With nothing selected the sheet shows the page itself, as Workbench does.
|
||||||
|
self._props.show(widget if widget is not None else self.comp)
|
||||||
if widget is None:
|
if widget is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -433,6 +437,48 @@ class OxPageViewPanel:
|
|||||||
fill=HANDLE_COLOUR, outline=HANDLE_COLOUR,
|
fill=HANDLE_COLOUR, outline=HANDLE_COLOUR,
|
||||||
tags=("selection_handle",))
|
tags=("selection_handle",))
|
||||||
|
|
||||||
|
def _repaint_all(self):
|
||||||
|
"""Every view redrawn from the document, this one included."""
|
||||||
|
if self.on_change is not None:
|
||||||
|
self.on_change()
|
||||||
|
else:
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def _commit_props(self, comp, values):
|
||||||
|
"""Writes {prop: raw} to `comp` as one undoable step, and to the device when live.
|
||||||
|
|
||||||
|
Returns whether anything changed; a change repaints every view, this one included.
|
||||||
|
"""
|
||||||
|
changes = []
|
||||||
|
for name, after in values.items():
|
||||||
|
prop = comp.find(f"prop[@name='{name}']")
|
||||||
|
before = prop.get("val") if prop is not None else None
|
||||||
|
if before != after:
|
||||||
|
changes.append((comp, name, before, after))
|
||||||
|
if not changes:
|
||||||
|
return False
|
||||||
|
|
||||||
|
live_failures = []
|
||||||
|
for element, name, _before, after in changes:
|
||||||
|
set_prop_value(element, name, after)
|
||||||
|
if live_hooks.is_live() and not live_hooks.live_set_prop_value(element, name, after):
|
||||||
|
live_failures.append(name)
|
||||||
|
|
||||||
|
if not app_state.live_mode:
|
||||||
|
undo.push(undo.SlotValueCommand(changes))
|
||||||
|
app_state.mark_dirty()
|
||||||
|
|
||||||
|
self._repaint_all()
|
||||||
|
|
||||||
|
who = comp.get("name", "?")
|
||||||
|
if live_failures:
|
||||||
|
app_state.set_status(
|
||||||
|
f"{who}: written locally but failed on device: {', '.join(live_failures)}")
|
||||||
|
else:
|
||||||
|
app_state.set_status(
|
||||||
|
f"{who}: {', '.join(name for _, name, _, _ in changes)} changed.")
|
||||||
|
return True
|
||||||
|
|
||||||
def _on_canvas_click(self, event):
|
def _on_canvas_click(self, event):
|
||||||
c = self._canvas
|
c = self._canvas
|
||||||
cx = c.canvasx(event.x)
|
cx = c.canvasx(event.x)
|
||||||
@@ -484,9 +530,11 @@ class OxPageViewPanel:
|
|||||||
self._drag["tag"] = None
|
self._drag["tag"] = None
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._drag["tag"] = None
|
||||||
c = self._canvas
|
c = self._canvas
|
||||||
z = self._zoom
|
z = self._zoom
|
||||||
bbox = c.bbox(f"{tag} && ox_widget_rect")
|
bbox = c.bbox(f"{tag} && ox_widget_rect")
|
||||||
|
moved = False
|
||||||
if bbox:
|
if bbox:
|
||||||
new_x = int((bbox[0] - self._screen_canvas_x * z) / z)
|
new_x = int((bbox[0] - self._screen_canvas_x * z) / z)
|
||||||
new_y = int((bbox[1] - self._screen_canvas_y * z) / z)
|
new_y = int((bbox[1] - self._screen_canvas_y * z) / z)
|
||||||
@@ -494,19 +542,12 @@ class OxPageViewPanel:
|
|||||||
new_x = max(0, min(new_x, self._screen_w))
|
new_x = max(0, min(new_x, self._screen_w))
|
||||||
new_y = max(0, min(new_y, self._screen_h))
|
new_y = max(0, min(new_y, self._screen_h))
|
||||||
|
|
||||||
old_x = _get_prop(widget, "x", "0")
|
moved = self._commit_props(widget, {"x": str(new_x), "y": str(new_y)})
|
||||||
old_y = _get_prop(widget, "y", "0")
|
|
||||||
|
|
||||||
if str(new_x) != old_x or str(new_y) != old_y:
|
# A commit re-renders through refresh(); a drag that went nowhere still has to
|
||||||
_set_prop(widget, "x", str(new_x))
|
# put back the handles the motion took down.
|
||||||
_set_prop(widget, "y", str(new_y))
|
if not moved:
|
||||||
app_state.mark_dirty()
|
self.refresh()
|
||||||
|
|
||||||
self._drag["tag"] = None
|
|
||||||
|
|
||||||
self._render_page()
|
|
||||||
if widget is not None:
|
|
||||||
self._select_widget(widget)
|
|
||||||
|
|
||||||
def _on_canvas_right_click(self, event):
|
def _on_canvas_right_click(self, event):
|
||||||
c = self._canvas
|
c = self._canvas
|
||||||
@@ -605,10 +646,15 @@ class OxPageViewPanel:
|
|||||||
zw = int(self._canvas_w * z)
|
zw = int(self._canvas_w * z)
|
||||||
zh = int(self._canvas_h * z)
|
zh = int(self._canvas_h * z)
|
||||||
self._canvas.config(scrollregion=(0, 0, zw, zh))
|
self._canvas.config(scrollregion=(0, 0, zw, zh))
|
||||||
self._render_page()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
|
"""Redraws from the document, keeping the selection when the widget survives."""
|
||||||
|
selected = self._selected_widget
|
||||||
self._render_page()
|
self._render_page()
|
||||||
|
if selected is not None and selected not in _get_widget_children(self.comp):
|
||||||
|
selected = None
|
||||||
|
self._select_widget(selected)
|
||||||
|
|
||||||
def _background(self):
|
def _background(self):
|
||||||
return app_state.DIRTY_TINT if app_state.is_dirty else CANVAS_BG
|
return app_state.DIRTY_TINT if app_state.is_dirty else CANVAS_BG
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user