SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
settings: group the Node Graph tab into Nodes, Navigation and Grid
Setting gains a `group` heading the panel draws rows under. It is visual
only: the JSON keys stay flat, so every reader of node_graph.* is untouched.
A nested schema dict would have grouped the panel too, but it renames the
keys in the file.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
python3.13libs/hc/hcschema.py | 47 ++++++++++++++++++++++++-----------------
python3.13libs/hc/hcsettings.py | 29 +++++++++++++++++++------
2 files changed, 51 insertions(+), 25 deletions(-)
diff --git a/python3.13libs/hc/hcschema.py b/python3.13libs/hc/hcschema.py
index 47a8238..b7a48b1 100644
--- a/python3.13libs/hc/hcschema.py
+++ b/python3.13libs/hc/hcschema.py
@@ -42,13 +42,18 @@ class Setting:
restart marks a value that is read only while Houdini starts (uiready.py,
123.py). The settings panel flags such rows and shows a restart notice
once the saved value differs from the one this session started with.
+
+ group is a heading the settings panel draws the row under. It is purely
+ visual: the JSON stays flat, so readers of e.g. node_graph.grid_x_step
+ are untouched. A nested dict in SCHEMA would have grouped the panel too,
+ but it also renames every key in the file.
"""
__slots__ = ("kind", "default", "label", "choices", "range", "decimals",
- "help", "restart")
+ "help", "restart", "group")
def __init__(self, kind, default, label=None, choices=None, range=None,
- decimals=2, help=None, restart=False):
+ decimals=2, help=None, restart=False, group=None):
self.kind = kind
self.default = default
self.label = label
@@ -57,6 +62,7 @@ class Setting:
self.decimals = decimals
self.help = help
self.restart = restart
+ self.group = group
# Nested dicts mirror the JSON. A dict value is a section; a Setting is a leaf.
@@ -114,40 +120,43 @@ SCHEMA = {
"drag_sensitivity": Setting("slider", 0.25, range=(0.0, 1.0)),
},
"node_graph": {
- "node_shape": Setting("choice", "rect", choices=tuple((s, s) for s in NODE_SHAPES)),
+ # Groups are panel headings only; the keys stay flat in the file.
+ "node_shape": Setting("choice", "rect", choices=tuple((s, s) for s in NODE_SHAPES),
+ group="Nodes"),
"node_coloring": Setting(
- "bool", True, label="Custom Node Coloring",
+ "bool", True, label="Custom Node Coloring", group="Nodes",
help="Color new nodes with the colour below and keep them in sync. "
"Off leaves every node the colour Houdini gives it.",
),
- "node_color": Setting("color", "#607070"),
- "zoom_center": Setting("choice", "mouse_cursor", choices=ZOOM_CENTERS),
+ "node_color": Setting("color", "#607070", group="Nodes"),
+ "zoom_center": Setting("choice", "mouse_cursor", choices=ZOOM_CENTERS,
+ group="Navigation"),
"hcnetcursor": Setting(
- "bool", True, label="hcnetcursor",
+ "bool", True, label="hcnetcursor", group="Navigation",
help="The grid cursor in the network editor. Off also returns the "
"unmodified f key to Houdini's own frame-selection.",
),
"current_node_arrow_color": Setting(
- "color", "#618f8f", label="Current Node Arrow Color",
+ "color", "#618f8f", label="Current Node Arrow Color", group="Navigation",
help="The off-screen current-node arrow drawn over the network editor.",
),
+ "grid_snap": Setting(
+ "bool", True, label="Hard Grid Snap", group="Grid",
+ help="Every drag, Tab-menu placement and box resize lands on the "
+ "grid. Off restores Houdini's magnetic snap radius and its "
+ "node-to-node alignment guides.",
+ ),
"drop_swap": Setting(
- "bool", True, label="Drop to Swap",
+ "bool", True, label="Drop to Swap", group="Grid",
help="Dragging a node onto another node's cell, or stepping it "
"there with the move keys, moves that node into the cell the "
"first one left. Two nodes wired directly to each other also "
"trade places in the chain.",
),
- "grid_snap": Setting(
- "bool", True, label="Hard Grid Snap",
- help="Every drag, Tab-menu placement and box resize lands on the "
- "grid. Off restores Houdini's magnetic snap radius and its "
- "node-to-node alignment guides.",
- ),
- "grid_x_step": Setting("slider", 2.0, range=(0.25, 8.0)),
- "grid_y_step": Setting("slider", 1.0, range=(0.25, 8.0)),
- "node_center_offset_x": Setting("slider", 0.5, range=(0.0, 2.0)),
- "node_center_offset_y": Setting("slider", 0.15, range=(0.0, 2.0)),
+ "grid_x_step": Setting("slider", 2.0, range=(0.25, 8.0), group="Grid"),
+ "grid_y_step": Setting("slider", 1.0, range=(0.25, 8.0), group="Grid"),
+ "node_center_offset_x": Setting("slider", 0.5, range=(0.0, 2.0), group="Grid"),
+ "node_center_offset_y": Setting("slider", 0.15, range=(0.0, 2.0), group="Grid"),
},
}
diff --git a/python3.13libs/hc/hcsettings.py b/python3.13libs/hc/hcsettings.py
index 4d61e1f..5354730 100644
--- a/python3.13libs/hc/hcsettings.py
+++ b/python3.13libs/hc/hcsettings.py
@@ -432,6 +432,29 @@ class HCSettingsPanel(QWidget):
scalars = {k: v for k, v in schema.items() if isinstance(v, hcschema.Setting)}
sections = {k: v for k, v in schema.items() if isinstance(v, dict)}
+ # Ungrouped rows first, then one box per `group` in order of first
+ # appearance. The groups are headings only -- the field paths, and
+ # so the file, are the same as for a flat form.
+ groups = {}
+ for key, setting in scalars.items():
+ groups.setdefault(setting.group, {})[key] = setting
+ for name, members in groups.items():
+ if name is None:
+ self._addForm(members, values, parent_layout, path)
+ for name, members in groups.items():
+ if name is not None:
+ group = QGroupBox(name)
+ group_layout = QVBoxLayout(group)
+ self._addForm(members, values, group_layout, path)
+ parent_layout.addWidget(group)
+
+ for key, section in sections.items():
+ group = QGroupBox(hcschema.label_for(key))
+ group_layout = QVBoxLayout(group)
+ self._addSection(section, values.get(key, {}), group_layout, path + (key,))
+ parent_layout.addWidget(group)
+
+ def _addForm(self, scalars, values, parent_layout, path):
if scalars:
form = QFormLayout()
parent_layout.addLayout(form)
@@ -458,12 +481,6 @@ class HCSettingsPanel(QWidget):
self._restart_marks[path + (key,)] = mark
form.addRow(label + ":", row)
- for key, section in sections.items():
- group = QGroupBox(hcschema.label_for(key))
- group_layout = QVBoxLayout(group)
- self._addSection(section, values.get(key, {}), group_layout, path + (key,))
- parent_layout.addWidget(group)
-
def _makeWidget(self, setting, value):
"""Widget from the declared kind -- never guessed from the value's type."""
kind = setting.kind