Properties: a hidden-slots toggle, and bools filled by their state
@hidden is now filtered by default. niagara.html says hidden slots "are
designed to be invisible to the user" and that interfaces should rarely
display them; every one under Sedona home is a void action a kit uses
internally, such as HdwAI.checkType. A "Hidden slots (n)" checkbox in the
footer brings them back and the answer sticks for the session. The checkbox
only appears on a type that has some, and the rows are always built -- the
toggle grid_removes and re-grids them, so an emptied grid row collapses rather
than leaving a gap.
A bool now says which state it is in by the colour of its field rather than by
a dot beside it that had to be learnt: green true, red false, blue null, with
the words left black so they read as easily as every other value. Dropping the
dot also lets a bool's field line up with the numbers above and below it.
Two things that needed changing to get there:
- a dropdown is offered only where there is a choice to make. A read-only
bool or enum gets a plain field, since a disabled dropdown is an arrow
that does nothing -- and a read-only bool is still filled, because a
runtime bool has a state too.
- the dropdown is a tk.OptionMenu, not a ttk.Combobox. On the Windows themes
a Combobox draws its field with a native element and ignores
fieldbackground outright, so a bool could never be filled with its colour.
An OptionMenu reports through its variable rather than an event, which means
loading a row looks exactly like choosing one -- so the load is flagged and the
unsaved-state tint left alone until every row is in, or the view would open
already claiming edits nobody made.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@ component hierarchy as a navigation tree, paints the selected folder's children
|
|||||||
wiresheet of draggable component boxes, draws the links between them, and writes box
|
wiresheet of draggable component boxes, draws the links between them, and writes box
|
||||||
positions back into each component's `meta` property on save.
|
positions back into each component's `meta` property on save.
|
||||||
|
|
||||||
Current version: **0.0.0.048** (see [Versioning](#versioning)).
|
Current version: **0.0.0.051** (see [Versioning](#versioning)).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ than leaving the selection looking like it did nothing. So does deleting the com
|
|||||||
is showing, from anywhere -- every delete in the UI goes through `delete_component`,
|
is showing, from anywhere -- every delete in the UI goes through `delete_component`,
|
||||||
because `sax_file` does its own view refresh rather than going through `refresh_all_views`.
|
because `sax_file` does its own view refresh rather than going through `refresh_all_views`.
|
||||||
|
|
||||||
It lists **every** slot the manifest declares,
|
It lists every slot the manifest declares -- bar one kind, see **Hidden slots** below --
|
||||||
in manifest order, each row numbered with the same slot id the wiresheet box uses. The
|
in manifest order, each row numbered with the same slot id the wiresheet box uses. The
|
||||||
box's visibility rules do not apply here: they are right for a diagram and wrong for the
|
box's visibility rules do not apply here: they are right for a diagram and wrong for the
|
||||||
one place a value is meant to be changed, so `HvacControl` shows all 47 rows rather than
|
one place a value is meant to be changed, so `HvacControl` shows all 47 rows rather than
|
||||||
@@ -177,10 +177,27 @@ A runtime slot holds a live value that no offline file carries, so nothing here
|
|||||||
usefully write one; a `@readonly` config slot is the manifest saying the same about a
|
usefully write one; a `@readonly` config slot is the manifest saying the same about a
|
||||||
value that *is* persisted. Both read the same way on screen.
|
value that *is* persisted. Both read the same way on screen.
|
||||||
|
|
||||||
|
A **dropdown is offered only where there is a choice to make**. A read-only bool or enum
|
||||||
|
gets a plain field instead, since a disabled dropdown is an arrow that does nothing -- and
|
||||||
|
a read-only bool is still filled by its state, because a runtime bool has a state too.
|
||||||
|
|
||||||
|
The dropdown is a `tk.OptionMenu` rather than a `ttk.Combobox`, for one reason: on the
|
||||||
|
Windows themes a Combobox draws its field with a native element and **ignores
|
||||||
|
`fieldbackground` outright**, so a bool could never be filled with its colour. The plain Tk
|
||||||
|
widget takes the colour it is given. It reports a change through its variable rather than
|
||||||
|
an event, which also catches a value set from anywhere else -- and means loading a row
|
||||||
|
looks exactly like choosing one, so the load is flagged and the tint left alone until every
|
||||||
|
row is in.
|
||||||
|
|
||||||
Each row is drawn from the slot's facets, so it reads the way the kit author meant it to:
|
Each row is drawn from the slot's facets, so it reads the way the kit author meant it to:
|
||||||
|
|
||||||
- `@trueText` / `@falseText` turn a bool into its own two words -- `Disabled` / `Enabled`
|
- `@trueText` / `@falseText` turn a bool into its own two words -- `Disabled` / `Enabled`
|
||||||
rather than false / true -- in a dropdown, with a red or green dot beside it.
|
rather than false / true -- and **the field is filled by the state**: green for true,
|
||||||
|
red for false, blue for null. It began as a coloured dot beside the field, which meant
|
||||||
|
learning what the dot stood for, then as coloured text; a block of colour carries down a
|
||||||
|
long list of slots where a letterform does not, and the words stay black so they read as
|
||||||
|
easily as every other value. Dropping the dot also let a bool's field line up with the
|
||||||
|
numbers above and below it.
|
||||||
- **Null is a value, not an absence.** Sedona models it as NaN for a float and a sentinel
|
- **Null is a value, not an absence.** Sedona models it as NaN for a float and a sentinel
|
||||||
for the integer types, and a config slot can genuinely hold it: `control::WriteFloat`
|
for the integer types, and a config slot can genuinely hold it: `control::WriteFloat`
|
||||||
pairs an `in` property with a `setNull` action, and the sample app carries
|
pairs an `in` property with a `setNull` action, and the sample app carries
|
||||||
@@ -262,6 +279,19 @@ system selection.
|
|||||||
`mark_clean` through a callback `main` registers -- the same shape `undo` uses for its view
|
`mark_clean` through a callback `main` registers -- the same shape `undo` uses for its view
|
||||||
refresh, and what keeps `app_state` free of any idea that a Properties view exists.
|
refresh, and what keeps `app_state` free of any idea that a Properties view exists.
|
||||||
|
|
||||||
|
### Hidden slots
|
||||||
|
|
||||||
|
`@hidden` is the one facet the view filters on, and only by default. niagara.html says
|
||||||
|
hidden slots "are designed to be invisible to the user" and that interfaces should rarely
|
||||||
|
display them; every one under Sedona home is a `void` action a kit uses internally, such
|
||||||
|
as `HdwAI.checkType`. A **Hidden slots (n)** checkbox in the footer brings them back, and
|
||||||
|
the answer sticks for the session rather than being asked again for every component.
|
||||||
|
|
||||||
|
The rows are always built -- the checkbox `grid_remove`s and re-`grid`s them, which keeps
|
||||||
|
each widget's options so putting a row back needs no arithmetic, and an emptied grid row
|
||||||
|
collapses rather than leaving a gap. The checkbox itself only appears on a type that has
|
||||||
|
something to reveal, so most components carry no control for a thing they do not have.
|
||||||
|
|
||||||
### Unsaved edits in the Properties view
|
### Unsaved edits in the Properties view
|
||||||
|
|
||||||
An edited field paints the whole view in **`app_state.DIRTY_TINT`** -- the same colour
|
An edited field paints the whole view in **`app_state.DIRTY_TINT`** -- the same colour
|
||||||
@@ -431,7 +461,6 @@ Read against `SedonaDoc/niagara.html`, which is what tells Niagara -- and so thi
|
|||||||
|
|
||||||
| Facet | Where it is used | Why not |
|
| Facet | Where it is used | Why not |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `hidden` | 129 slots, every one a `void` action such as `HdwAI.checkType` | The doc says user interfaces should rarely display hidden slots, and the reference tool appears not to. This view shows every slot because that is what it is for; say the word and it will skip them. |
|
|
||||||
| `fieldWidth` | 3082 slots | A per-slot column count for the text field. Applying it would make the value column ragged; a table reads better with one width. |
|
| `fieldWidth` | 3082 slots | A per-slot column count for the text field. Applying it would make the value column ragged; a table reads better with one width. |
|
||||||
| `radix`, `showSeparators`, `multiLine`, `fieldEditor` | unused by any kit under Sedona home | Nothing to render. |
|
| `radix`, `showSeparators`, `multiLine`, `fieldEditor` | unused by any kit under Sedona home | Nothing to render. |
|
||||||
|
|
||||||
|
|||||||
+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.048"
|
VERSION = "0.0.0.051"
|
||||||
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
|
||||||
|
|||||||
+195
-60
@@ -5,6 +5,11 @@ table are the same piece of screen and only one of them is ever the thing being
|
|||||||
`main` is what hides the canvas and puts this in its place; this module knows only the
|
`main` is what hides the canvas and puts this in its place; this module knows only the
|
||||||
frame it was handed.
|
frame it was handed.
|
||||||
|
|
||||||
|
The one slot it does filter is `@hidden`, and only by default: niagara.html says user
|
||||||
|
interfaces should rarely display hidden slots, and every one under Sedona home is a `void`
|
||||||
|
action a kit uses internally. A checkbox in the footer brings them back, and the choice
|
||||||
|
sticks for the session.
|
||||||
|
|
||||||
The wiresheet box hides most of a type's slots — `HvacControl` paints 8 of its 47 — which
|
The wiresheet box hides most of a type's slots — `HvacControl` paints 8 of its 47 — which
|
||||||
is right for a diagram and wrong for the one place a value is meant to be changed. This
|
is right for a diagram and wrong for the one place a value is meant to be changed. This
|
||||||
view is that place, so it filters nothing: every slot the manifest declares gets a row,
|
view is that place, so it filters nothing: every slot the manifest declares gets a row,
|
||||||
@@ -89,9 +94,21 @@ ROW_BG = "#ffffff"
|
|||||||
HEADER_BG = "#f2f2f2"
|
HEADER_BG = "#f2f2f2"
|
||||||
TYPE_COLOUR = "#555555"
|
TYPE_COLOUR = "#555555"
|
||||||
PATH_COLOUR = "#7a7a7a"
|
PATH_COLOUR = "#7a7a7a"
|
||||||
DOT_TRUE = "#3fa34d"
|
# A bool says which state it is in by the colour of its field. Filled rather than
|
||||||
DOT_FALSE = "#d9534f"
|
# lettered: a block of colour carries down a long list of slots where coloured text does
|
||||||
DOT_NULL = "#4a90d9"
|
# not, and the words stay black so they read as easily as every other value.
|
||||||
|
BOOL_BACKGROUNDS = {
|
||||||
|
"true": "#c8e6c9",
|
||||||
|
"false": "#ffcdd2",
|
||||||
|
"null": "#bbdefb",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def bool_state(raw):
|
||||||
|
"""Which of the three a value is in: 'true', 'false' or 'null'."""
|
||||||
|
if is_null(raw):
|
||||||
|
return "null"
|
||||||
|
return "true" if str(raw).strip().lower() in ("true", "1") else "false"
|
||||||
|
|
||||||
FIELD_WIDTH = 18 # characters; the entry and the combobox share it so fields line up
|
FIELD_WIDTH = 18 # characters; the entry and the combobox share it so fields line up
|
||||||
ROW_HEIGHT_HINT = 22
|
ROW_HEIGHT_HINT = 22
|
||||||
@@ -100,6 +117,10 @@ ROW_HEIGHT_HINT = 22
|
|||||||
# models null as NaN for a float and a sentinel for the integer types, and a config slot
|
# models null as NaN for a float and a sentinel for the integer types, and a config slot
|
||||||
# can genuinely hold it: control::WriteFloat pairs an 'in' property with a 'setNull'
|
# can genuinely hold it: control::WriteFloat pairs an 'in' property with a 'setNull'
|
||||||
# action, and the sample app carries <prop name="in" val="null"/> on four of them.
|
# action, and the sample app carries <prop name="in" val="null"/> on four of them.
|
||||||
|
# Whether hidden slots are listed. Held for the session rather than per panel, so the
|
||||||
|
# answer does not have to be given again for every component looked at.
|
||||||
|
show_hidden_slots = False
|
||||||
|
|
||||||
NULL_VALUE = "null"
|
NULL_VALUE = "null"
|
||||||
NULL_TEXT = "null"
|
NULL_TEXT = "null"
|
||||||
|
|
||||||
@@ -221,6 +242,16 @@ def effective_value(comp, slot):
|
|||||||
return type_zero(slot["type"])
|
return type_zero(slot["type"])
|
||||||
|
|
||||||
|
|
||||||
|
def is_hidden(slot):
|
||||||
|
"""The '@hidden' facet: a slot a kit keeps for itself.
|
||||||
|
|
||||||
|
niagara.html puts it plainly -- hidden slots "are designed to be invisible to the
|
||||||
|
user", and interfaces should rarely display them. Every one under Sedona home is a
|
||||||
|
void action such as `HdwAI.checkType`.
|
||||||
|
"""
|
||||||
|
return slot["facets"].get("hidden") is True
|
||||||
|
|
||||||
|
|
||||||
def is_editable(slot):
|
def is_editable(slot):
|
||||||
"""Config and not read-only. The rule the whole view's greying comes from."""
|
"""Config and not read-only. The rule the whole view's greying comes from."""
|
||||||
return "c" in slot["flags"] and not slot["readonly"]
|
return "c" in slot["flags"] and not slot["readonly"]
|
||||||
@@ -470,6 +501,10 @@ class PropertiesPanel:
|
|||||||
# different thing from whether it has been.
|
# different thing from whether it has been.
|
||||||
self._tint_targets = []
|
self._tint_targets = []
|
||||||
self.dirty = False
|
self.dirty = False
|
||||||
|
# Set while rows are being filled from the document. An OptionMenu reports through
|
||||||
|
# its variable, so loading a value looks exactly like choosing one; without this
|
||||||
|
# the view would open already claiming unsaved edits.
|
||||||
|
self._loading = False
|
||||||
|
|
||||||
comp_type = comp.get("type", "sys::Component")
|
comp_type = comp.get("type", "sys::Component")
|
||||||
self.slots = schema.get_full_slots_for_type(comp_type)
|
self.slots = schema.get_full_slots_for_type(comp_type)
|
||||||
@@ -481,8 +516,12 @@ class PropertiesPanel:
|
|||||||
# sibling nothing at all.
|
# sibling nothing at all.
|
||||||
self._build_header(comp_type)
|
self._build_header(comp_type)
|
||||||
self._build_footer()
|
self._build_footer()
|
||||||
|
self._loading = True
|
||||||
self._build_rows_area()
|
self._build_rows_area()
|
||||||
|
self._loading = False
|
||||||
|
self._apply_hidden_filter()
|
||||||
self._load_path()
|
self._load_path()
|
||||||
|
self._update_tint()
|
||||||
|
|
||||||
if not self.slots:
|
if not self.slots:
|
||||||
self.status_var.set(
|
self.status_var.set(
|
||||||
@@ -505,7 +544,7 @@ class PropertiesPanel:
|
|||||||
for row in self.rows:
|
for row in self.rows:
|
||||||
if not row["editable"]:
|
if not row["editable"]:
|
||||||
continue
|
continue
|
||||||
if (row["widget"].get() or "").strip() != row["loaded_text"].strip():
|
if (self.row_text(row) or "").strip() != row["loaded_text"].strip():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if self.meta_row is not None:
|
if self.meta_row is not None:
|
||||||
@@ -518,6 +557,8 @@ class PropertiesPanel:
|
|||||||
|
|
||||||
def _update_tint(self):
|
def _update_tint(self):
|
||||||
"""Repaints the view in the same colour the canvas wears when the file is dirty."""
|
"""Repaints the view in the same colour the canvas wears when the file is dirty."""
|
||||||
|
if self._loading:
|
||||||
|
return
|
||||||
was_dirty = self.dirty
|
was_dirty = self.dirty
|
||||||
self.dirty = self.has_unsaved_changes()
|
self.dirty = self.has_unsaved_changes()
|
||||||
for widget, normal_background in self._tint_targets:
|
for widget, normal_background in self._tint_targets:
|
||||||
@@ -532,15 +573,42 @@ class PropertiesPanel:
|
|||||||
if was_dirty != self.dirty and self.on_dirty_change is not None:
|
if was_dirty != self.dirty and self.on_dirty_change is not None:
|
||||||
self.on_dirty_change()
|
self.on_dirty_change()
|
||||||
|
|
||||||
def _watch_for_edits(self, widget, kind):
|
def _watch_for_edits(self, row, widget, kind):
|
||||||
"""Makes a field repaint the view as soon as its value moves."""
|
"""Makes a field repaint the view as soon as its value moves.
|
||||||
|
|
||||||
|
`kind` is the widget's, not the slot's: an editable bool is a dropdown, a
|
||||||
|
read-only one is a plain field, and the two report a change differently.
|
||||||
|
"""
|
||||||
if kind == "text":
|
if kind == "text":
|
||||||
widget.bind("<KeyRelease>", lambda _event: self._update_tint(), add="+")
|
widget.bind("<KeyRelease>", lambda _event: self._update_tint(), add="+")
|
||||||
widget.bind("<<Paste>>", lambda _event: self._update_tint(), add="+")
|
widget.bind("<<Paste>>", lambda _event: self._update_tint(), add="+")
|
||||||
widget.bind("<FocusOut>", lambda _event: self._update_tint(), add="+")
|
widget.bind("<FocusOut>", lambda _event: self._update_tint(), add="+")
|
||||||
|
return
|
||||||
|
|
||||||
|
# An OptionMenu reports through its variable rather than an event, which also
|
||||||
|
# catches a value set from anywhere else.
|
||||||
|
row["var"].trace_add("write", lambda *_args: self._on_choice_changed(row))
|
||||||
|
|
||||||
|
def _on_choice_changed(self, row):
|
||||||
|
# A bool's fill is its value, so picking the other one has to recolour the field
|
||||||
|
# straight away rather than waiting for the next load.
|
||||||
|
if row["kind"] == "bool":
|
||||||
|
self._recolour_bool(row)
|
||||||
|
self._update_tint()
|
||||||
|
|
||||||
|
def _recolour_bool(self, row):
|
||||||
|
"""Fills a bool row's field by which of the three states it now reads as."""
|
||||||
|
slot = row["slot"]
|
||||||
|
text = (self.row_text(row) or "").strip()
|
||||||
|
false_text, true_text = bool_texts(slot)
|
||||||
|
if text == true_text:
|
||||||
|
state = "true"
|
||||||
|
elif text == false_text:
|
||||||
|
state = "false"
|
||||||
else:
|
else:
|
||||||
widget.bind("<<ComboboxSelected>>",
|
state = "null"
|
||||||
lambda _event: self._update_tint(), add="+")
|
fill = BOOL_BACKGROUNDS[state]
|
||||||
|
row["widget"].configure(bg=fill, activebackground=fill)
|
||||||
|
|
||||||
# -- construction --
|
# -- construction --
|
||||||
|
|
||||||
@@ -639,47 +707,80 @@ class PropertiesPanel:
|
|||||||
dot.grid(row=grid_row, column=0, padx=(6, 2))
|
dot.grid(row=grid_row, column=0, padx=(6, 2))
|
||||||
return dot
|
return dot
|
||||||
|
|
||||||
|
def _apply_hidden_filter(self):
|
||||||
|
"""Takes hidden rows out of the grid, or puts them back.
|
||||||
|
|
||||||
|
grid_remove keeps each widget's options, so putting a row back needs no
|
||||||
|
arithmetic, and an emptied grid row collapses to nothing rather than leaving a
|
||||||
|
gap where the slot was.
|
||||||
|
"""
|
||||||
|
for row in self.rows:
|
||||||
|
if not is_hidden(row["slot"]):
|
||||||
|
continue
|
||||||
|
for widget in row["widgets"]:
|
||||||
|
if show_hidden_slots:
|
||||||
|
widget.grid()
|
||||||
|
else:
|
||||||
|
widget.grid_remove()
|
||||||
|
|
||||||
|
self.scroll_canvas.configure(scrollregion=self.scroll_canvas.bbox("all"))
|
||||||
|
|
||||||
def _build_slot_row(self, grid_row, slot):
|
def _build_slot_row(self, grid_row, slot):
|
||||||
self._add_bullet(grid_row)
|
bullet = self._add_bullet(grid_row)
|
||||||
|
|
||||||
self._tinted(tk.Label(self.rows_frame, text=self._row_label(slot), anchor="w",
|
name_label = self._tinted(tk.Label(self.rows_frame, text=self._row_label(slot),
|
||||||
bg=ROW_BG, fg=LABEL_COLOUR),
|
anchor="w", bg=ROW_BG, fg=LABEL_COLOUR),
|
||||||
ROW_BG).grid(row=grid_row, column=1, sticky="w", padx=(2, 8))
|
ROW_BG)
|
||||||
|
name_label.grid(row=grid_row, column=1, sticky="w", padx=(2, 8))
|
||||||
|
|
||||||
self._tinted(tk.Label(self.rows_frame, text=qualified_type(slot["type"]),
|
type_label = self._tinted(tk.Label(self.rows_frame,
|
||||||
anchor="w", bg=ROW_BG, fg=TYPE_COLOUR),
|
text=qualified_type(slot["type"]),
|
||||||
ROW_BG).grid(row=grid_row, column=2, sticky="w", padx=(0, 10))
|
anchor="w", bg=ROW_BG, fg=TYPE_COLOUR), ROW_BG)
|
||||||
|
type_label.grid(row=grid_row, column=2, sticky="w", padx=(0, 10))
|
||||||
|
|
||||||
field_frame = self._tinted(tk.Frame(self.rows_frame, bg=ROW_BG), ROW_BG)
|
field_frame = self._tinted(tk.Frame(self.rows_frame, bg=ROW_BG), ROW_BG)
|
||||||
field_frame.grid(row=grid_row, column=3, sticky="w", padx=(0, 4))
|
field_frame.grid(row=grid_row, column=3, sticky="w", padx=(0, 4))
|
||||||
|
|
||||||
editable = is_editable(slot)
|
editable = is_editable(slot)
|
||||||
row = {"slot": slot, "editable": editable, "kind": None, "widget": None,
|
row = {"slot": slot, "editable": editable, "kind": None, "widget": None,
|
||||||
"state_dot": None, "loaded_text": ""}
|
"loaded_text": "",
|
||||||
|
"widgets": [bullet, name_label, type_label, field_frame]}
|
||||||
|
|
||||||
names = enum_range(slot)
|
names = enum_range(slot)
|
||||||
if slot["type"] == "bool":
|
if slot["type"] == "bool":
|
||||||
row["kind"] = "bool"
|
row["kind"] = "bool"
|
||||||
state_dot = self._tinted(tk.Canvas(field_frame, width=14, height=16,
|
|
||||||
highlightthickness=0, bg=ROW_BG), ROW_BG)
|
|
||||||
state_dot.grid(row=0, column=0)
|
|
||||||
row["state_dot"] = state_dot
|
|
||||||
false_text, true_text = bool_texts(slot)
|
|
||||||
choices = [false_text, true_text]
|
|
||||||
# '@allowNull false' is the manifest saying this bool is only ever one of the
|
|
||||||
# two, so null is offered on every other bool and withheld here.
|
|
||||||
if allows_null(slot):
|
|
||||||
choices.append(null_text(slot))
|
|
||||||
widget = ttk.Combobox(field_frame, values=choices,
|
|
||||||
width=FIELD_WIDTH - 3, state="readonly")
|
|
||||||
widget.grid(row=0, column=1)
|
|
||||||
elif names and slot["type"] in INTEGER_TYPES:
|
elif names and slot["type"] in INTEGER_TYPES:
|
||||||
row["kind"] = "enum"
|
row["kind"] = "enum"
|
||||||
widget = ttk.Combobox(field_frame, values=list(names),
|
|
||||||
width=FIELD_WIDTH - 1, state="readonly")
|
|
||||||
widget.grid(row=0, column=1)
|
|
||||||
else:
|
else:
|
||||||
row["kind"] = "text"
|
row["kind"] = "text"
|
||||||
|
|
||||||
|
# A dropdown is offered only where there is a choice to make: a disabled one on a
|
||||||
|
# runtime slot is an arrow that does nothing.
|
||||||
|
row["is_choice"] = editable and row["kind"] in ("bool", "enum")
|
||||||
|
|
||||||
|
if row["is_choice"]:
|
||||||
|
if row["kind"] == "bool":
|
||||||
|
false_text, true_text = bool_texts(slot)
|
||||||
|
choices = [false_text, true_text]
|
||||||
|
# '@allowNull false' is the manifest saying this bool is only ever one of
|
||||||
|
# the two, so null is offered on every other bool and withheld here.
|
||||||
|
if allows_null(slot):
|
||||||
|
choices.append(null_text(slot))
|
||||||
|
else:
|
||||||
|
choices = list(names)
|
||||||
|
|
||||||
|
# An OptionMenu rather than a ttk.Combobox, for one reason: the themed
|
||||||
|
# Combobox draws its field with a native element and ignores fieldbackground
|
||||||
|
# outright, so a bool could never be filled with its colour. This is a plain
|
||||||
|
# Tk widget and takes the colour it is given.
|
||||||
|
row["var"] = tk.StringVar()
|
||||||
|
widget = tk.OptionMenu(field_frame, row["var"], *choices)
|
||||||
|
widget.configure(width=FIELD_WIDTH - 5, anchor="w", relief="solid", bd=1,
|
||||||
|
highlightthickness=0, padx=4, pady=0,
|
||||||
|
bg=EDITABLE_FIELD_BG, activebackground=EDITABLE_FIELD_BG)
|
||||||
|
widget["menu"].configure(bg=EDITABLE_FIELD_BG)
|
||||||
|
else:
|
||||||
|
row["var"] = None
|
||||||
widget = tk.Entry(field_frame, width=FIELD_WIDTH, relief="solid", bd=1,
|
widget = tk.Entry(field_frame, width=FIELD_WIDTH, relief="solid", bd=1,
|
||||||
highlightthickness=0, justify="left",
|
highlightthickness=0, justify="left",
|
||||||
readonlybackground=READONLY_FIELD_BG)
|
readonlybackground=READONLY_FIELD_BG)
|
||||||
@@ -688,13 +789,15 @@ class PropertiesPanel:
|
|||||||
|
|
||||||
row["widget"] = widget
|
row["widget"] = widget
|
||||||
if editable:
|
if editable:
|
||||||
self._watch_for_edits(widget, row["kind"])
|
self._watch_for_edits(row, widget,
|
||||||
|
"choice" if row["is_choice"] else "text")
|
||||||
|
|
||||||
suffix = " ".join(part for part in (unit_symbol(slot["facets"].get("unit")),
|
suffix = " ".join(part for part in (unit_symbol(slot["facets"].get("unit")),
|
||||||
range_text(slot)) if part)
|
range_text(slot)) if part)
|
||||||
self._tinted(tk.Label(self.rows_frame, text=suffix, anchor="w", bg=ROW_BG,
|
suffix_label = self._tinted(tk.Label(self.rows_frame, text=suffix, anchor="w",
|
||||||
fg="#555555"),
|
bg=ROW_BG, fg="#555555"), ROW_BG)
|
||||||
ROW_BG).grid(row=grid_row, column=4, sticky="w", padx=(6, 6))
|
suffix_label.grid(row=grid_row, column=4, sticky="w", padx=(6, 6))
|
||||||
|
row["widgets"].append(suffix_label)
|
||||||
|
|
||||||
self.rows.append(row)
|
self.rows.append(row)
|
||||||
self._load_row(row)
|
self._load_row(row)
|
||||||
@@ -745,6 +848,19 @@ class PropertiesPanel:
|
|||||||
# the tint has to put back.
|
# the tint has to put back.
|
||||||
self._tinted(footer, footer.cget("bg"))
|
self._tinted(footer, footer.cget("bg"))
|
||||||
|
|
||||||
|
# Offered only where there is something to reveal, so most components carry no
|
||||||
|
# control for a thing they do not have.
|
||||||
|
hidden_count = sum(1 for slot in self.slots if is_hidden(slot))
|
||||||
|
if hidden_count:
|
||||||
|
self.show_hidden_var = tk.BooleanVar(value=show_hidden_slots)
|
||||||
|
box = tk.Checkbutton(
|
||||||
|
footer, variable=self.show_hidden_var,
|
||||||
|
text=f"Hidden slots ({hidden_count})",
|
||||||
|
command=self._on_show_hidden_toggled,
|
||||||
|
)
|
||||||
|
self._tinted(box, box.cget("bg"))
|
||||||
|
box.pack(side=tk.LEFT, padx=(0, 12))
|
||||||
|
|
||||||
self.status_var = tk.StringVar(value="")
|
self.status_var = tk.StringVar(value="")
|
||||||
status = tk.Label(footer, textvariable=self.status_var, anchor="w", fg="#333333")
|
status = tk.Label(footer, textvariable=self.status_var, anchor="w", fg="#333333")
|
||||||
self._tinted(status, status.cget("bg"))
|
self._tinted(status, status.cget("bg"))
|
||||||
@@ -757,11 +873,34 @@ class PropertiesPanel:
|
|||||||
ttk.Button(footer, text="Refresh", width=9,
|
ttk.Button(footer, text="Refresh", width=9,
|
||||||
command=self.refresh).pack(side=tk.RIGHT)
|
command=self.refresh).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
def _on_show_hidden_toggled(self):
|
||||||
|
"""The checkbox: remember the answer for the session, then re-filter the rows."""
|
||||||
|
global show_hidden_slots
|
||||||
|
show_hidden_slots = bool(self.show_hidden_var.get())
|
||||||
|
self._apply_hidden_filter()
|
||||||
|
|
||||||
# -- loading --
|
# -- loading --
|
||||||
|
|
||||||
def _set_state_dot(self, canvas_widget, colour):
|
def row_text(self, row):
|
||||||
canvas_widget.delete("all")
|
"""What a row currently reads, whichever kind of field it wears."""
|
||||||
canvas_widget.create_oval(3, 4, 12, 13, fill=colour, outline="#666666")
|
if row["var"] is not None:
|
||||||
|
return row["var"].get()
|
||||||
|
return row["widget"].get()
|
||||||
|
|
||||||
|
def _set_row_text(self, row, text):
|
||||||
|
if row["var"] is not None:
|
||||||
|
row["var"].set(text)
|
||||||
|
return
|
||||||
|
widget = row["widget"]
|
||||||
|
widget.configure(state="normal")
|
||||||
|
widget.delete(0, tk.END)
|
||||||
|
widget.insert(0, text)
|
||||||
|
|
||||||
|
def _field_background(self, row, raw):
|
||||||
|
"""The fill a row's field takes: a bool's state, or its editability."""
|
||||||
|
if row["kind"] == "bool":
|
||||||
|
return BOOL_BACKGROUNDS[bool_state(raw)]
|
||||||
|
return EDITABLE_FIELD_BG if row["editable"] else READONLY_FIELD_BG
|
||||||
|
|
||||||
def _load_row(self, row):
|
def _load_row(self, row):
|
||||||
"""Puts the DOM's current value into one row's widget and remembers it as loaded."""
|
"""Puts the DOM's current value into one row's widget and remembers it as loaded."""
|
||||||
@@ -770,28 +909,21 @@ class PropertiesPanel:
|
|||||||
text = display_text(slot, raw)
|
text = display_text(slot, raw)
|
||||||
widget = row["widget"]
|
widget = row["widget"]
|
||||||
|
|
||||||
if row["kind"] == "text":
|
# Set before the fill, so a StringVar trace sees the value it is colouring for.
|
||||||
widget.configure(state="normal")
|
self._set_row_text(row, text)
|
||||||
widget.delete(0, tk.END)
|
fill = self._field_background(row, raw)
|
||||||
widget.insert(0, text)
|
|
||||||
if row["editable"]:
|
|
||||||
widget.configure(state="normal", fg=LABEL_COLOUR, bg=EDITABLE_FIELD_BG)
|
|
||||||
else:
|
|
||||||
widget.configure(state="readonly", fg=READONLY_TEXT)
|
|
||||||
else:
|
|
||||||
widget.configure(state="readonly")
|
|
||||||
widget.set(text)
|
|
||||||
if not row["editable"]:
|
|
||||||
widget.configure(state="disabled")
|
|
||||||
|
|
||||||
if row["state_dot"] is not None:
|
if row["is_choice"]:
|
||||||
if is_null(raw):
|
widget.configure(bg=fill, activebackground=fill)
|
||||||
colour = DOT_NULL
|
widget["menu"].configure(bg=EDITABLE_FIELD_BG)
|
||||||
elif str(raw).strip().lower() in ("true", "1"):
|
|
||||||
colour = DOT_TRUE
|
|
||||||
else:
|
else:
|
||||||
colour = DOT_FALSE
|
# A bool keeps its fill whether or not it can be changed: the colour is what
|
||||||
self._set_state_dot(row["state_dot"], colour)
|
# says which state the slot is in, and a runtime bool has a state too.
|
||||||
|
widget.configure(fg=LABEL_COLOUR if row["editable"] else READONLY_TEXT)
|
||||||
|
if row["editable"]:
|
||||||
|
widget.configure(state="normal", bg=fill)
|
||||||
|
else:
|
||||||
|
widget.configure(state="readonly", readonlybackground=fill)
|
||||||
|
|
||||||
row["loaded_text"] = text
|
row["loaded_text"] = text
|
||||||
|
|
||||||
@@ -902,8 +1034,11 @@ class PropertiesPanel:
|
|||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Re-reads every value from the DOM, discarding edits that were not saved."""
|
"""Re-reads every value from the DOM, discarding edits that were not saved."""
|
||||||
|
self._loading = True
|
||||||
for row in self.rows:
|
for row in self.rows:
|
||||||
self._load_row(row)
|
self._load_row(row)
|
||||||
|
self._loading = False
|
||||||
|
|
||||||
if self.meta_row is not None:
|
if self.meta_row is not None:
|
||||||
self._load_meta_row()
|
self._load_meta_row()
|
||||||
self._load_path()
|
self._load_path()
|
||||||
@@ -932,7 +1067,7 @@ class PropertiesPanel:
|
|||||||
if not row["editable"]:
|
if not row["editable"]:
|
||||||
continue
|
continue
|
||||||
slot = row["slot"]
|
slot = row["slot"]
|
||||||
current = (row["widget"].get() or "").strip()
|
current = (self.row_text(row) or "").strip()
|
||||||
if current == row["loaded_text"].strip():
|
if current == row["loaded_text"].strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user