git clone https://git.lucas.co/hou-control.git
hc: refactor and expand session/UI management
.mcp.json | 10 +
CLAUDE.md | 69 +++++
MainMenuCommon.xml | 37 ++-
NetworkViewMenu.xml | 261 ++++++++++++++++++
config/Cursors/CursorMapping | 19 ++
config/Cursors/grab.svg | 9 +
config/Cursors/grabbing.svg | 15 ++
config/Cursors/white.svg | 9 +
config/Cursors/wire.svg | 9 +
config/hc_colors.inc | 2 +-
desktop/{hc_desktop.desk => hc_attached.desk} | 0
desktop/hc_detached.desk | 14 +
scripts/hotkeys_assign.json => hc_hotkeys.json | 35 ++-
scripts/settings.json => hc_settings.json | 1 +
mcpserver/houdini_mcp.py | 99 +++++++
mcpserver/requirements.txt | 2 +
notes/IM.md | 0
notes/style.md | 0
python3.11libs/hc/__init__.py | 15 +-
python3.11libs/hc/hcbindings.py | 47 ++--
python3.11libs/hc/hcguides.py | 4 +-
python3.11libs/hc/hclayout.py | 202 ++++++++++++++
python3.11libs/hc/hclayouts.py | 88 -------
python3.11libs/hc/hcmaps.py | 6 +
python3.11libs/hc/hcnetworkeditor.py | 352 +++++++++++++++++++++++--
python3.11libs/hc/hcpane.py | 12 +-
python3.11libs/hc/hcparametertab.py | 12 +
python3.11libs/hc/hcprefs.py | 14 -
python3.11libs/hc/hcsceneviewer.py | 29 ++
python3.11libs/hc/hcsession.py | 172 +++++++++++-
python3.11libs/hc/hcsettings.py | 266 +++++++++++++++++++
python3.11libs/hc/hcsplithandles.py | 212 +++++++++++++++
python3.11libs/hc/hcstatus.py | 137 ++++++----
python3.11libs/hc/hcstatusbar.py | 202 ++++++++++++++
python3.11libs/hc/hctab.py | 6 +
python3.11libs/hc/hcwindowwatcher.py | 98 +++++++
python3.11libs/nodegraphhooks.py | 79 ++----
python3.11libs/uiready.py | 41 ++-
scripts/OnCreated.py | 4 +-
scripts/hotkeys_clear.json | 152 -----------
viewer_states/keycam.py | 4 +-
41 files changed, 2287 insertions(+), 458 deletions(-)
diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 0000000..f81bc57
--- /dev/null
+++ b/.mcp.json
@@ -0,0 +1,10 @@
+{
+ "mcpServers": {
+ "houdini": {
+ "command": "/home/lsgalante/src/hou-control/mcpserver/.venv/bin/python",
+ "args": [
+ "/home/lsgalante/src/hou-control/mcpserver/houdini_mcp.py"
+ ]
+ }
+ }
+}
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..9e83b60
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,69 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Overview
+
+`hou-control` (hc) is a SideFX Houdini customization package — not a standalone application. There is no build, no test suite, and no package manager. Code here is loaded by Houdini at runtime via the `houdini-studio-utils.json` package manifest (which adds this directory to `HOUDINI_PATH`) and the `HC_PATH` env var, which the code reads with `hou.getenv("HC_PATH")` to locate config/scripts on disk.
+
+Everything runs inside Houdini's embedded Python 3.11. To exercise changes, reload from within Houdini:
+- From the `hc` main menu: **Reload Hotkeys**, **Reload Colors**, **Reload Keycam**.
+- Full package reload: `hou.ui.reloadPackage(...)` (see `HCSession.reloadHC`).
+- Hotkey JSON changes take effect via `HCSession.reloadHotkeys()` which calls `HCBindings().load()` (assigns from `hc_hotkeys.json`, auto-clearing any conflicts via `hou.hotkeys.findConflicts`).
+
+## Architecture
+
+### The `hc` Python package (`python3.11libs/hc/`)
+
+The core abstraction is a set of wrapper classes around Houdini's `hou` API objects. Each wrapper holds the underlying `hou_*` object and adds higher-level behavior. Classes are re-exported from `hc/__init__.py`.
+
+Key hierarchy:
+- **`HCSession`** (`hcsession.py`) — top-level entry point. Enumerates panes/tabs/viewports, controls desktop-wide visibility (menus, stowbars, shelf), update mode, hotkey reloads, and launches the HC Panel. Most menu items and hotkey bindings go through `HCSession`.
+- **`HCPane`** wraps `hou.Pane`; knows how to split, resize, convert its current `hou.PaneTab` into the right `HC*` subclass via `HCPane.convertTab`.
+- **`HCTab`** → **`HCPathTab`** → **`HCNetworkEditor`** / **`HCSceneViewer`**. `convertTab` dispatches on `hou.paneTabType` to the correct subclass; `HCPathTab` is used for Parm and DetailsView tabs. Tab type is also identified via a string `.type()` method (e.g. `'HCNetworkEditor'`) used in `isinstance`-style branching throughout `HCSession` and `HCMaps`.
+- **`HCBindings`** — loads hotkey assignments by reading `hc_hotkeys.json` and calling `hou.hotkeys.addAssignment`. Before each assignment, `hou.hotkeys.findConflicts` identifies any ancestor/descendant bindings using the same key and clears them. The JSON keys are Houdini symbol paths like `h.pane.gview.foo`; the context is derived via `symbol.rpartition('.')[0]`.
+- **`HCMaps`** (`hcmaps.py`) — central registry mapping human-readable command names → bound methods on session/pane/tab. Composed at runtime by `HCSession.hcPanel()` based on the current tab's type (`tab_map_base` always, plus one of `tab_map_path` / `tab_map_network_editor` / `tab_map_scene_viewer`). Used to populate the HC Panel selection dialog.
+- **`HCWidgets`** (`hcwidgets.py`) — PySide6 widgets, notably `SelectionDialog` used for the HC Panel.
+
+When adding a new command: implement it on the appropriate wrapper (`HCSession`/`HCPane`/`HC*Tab`), then register it in the matching `HCMaps.tab_map_*` so it appears in the HC Panel. If it should be hotkey-bound, add a Houdini symbol entry to `hc_hotkeys.json`.
+
+### Non-package Python (`python3.11libs/`)
+
+These are recognized by Houdini's startup/event system by filename convention:
+- `uiready.py` — runs once when the UI is ready; instantiates `HCSession` and calls `reloadHotkeys` + `toggleStowbars`.
+- `nodegraphhooks.py` — Houdini's network-editor event hook. Implements `createEventHandler(uievent, pending_actions)` and dispatches `KeyboardEvent`s through a local `keymap` dict that calls into `HCNetworkEditor`. Return `(None, True)` to consume the event, `(None, False)` to let Houdini handle it.
+
+### Scripts (`scripts/`)
+
+- `123.py` / `456.py` are Houdini's magic filenames: `123.py` runs when Houdini starts without a `.hip`, `456.py` runs after any `.hip` load. Currently used for "open last file" tracking via `$HOUDINI_USER_PREF_DIR/st_data/state.json`.
+- `OnCreated.py` — node OnCreated event script.
+- `hc_hotkeys.json` (at repo root) — source of truth for keybindings, loaded by `HCBindings`. Conflicts with existing bindings are detected and cleared automatically.
+- `settings.json` — runtime settings for `keycam` viewer state and node graph defaults.
+
+### Viewer states (`viewer_states/`)
+
+`keycam.py` is a custom Houdini viewer state ("keycam" navigator) registered via the viewer state API. `HCSession.reloadKeycam()` calls `hou.ui.reloadViewerState('keycam')`.
+
+### Configuration (`config/`)
+
+Houdini-format config files:
+- `UIDark.hcs` — UI color scheme.
+- `3DSceneColors.dark` — viewport color scheme.
+- `NodeGraphDark.inc` + `NodeGraphCommon.inc` — node graph styling (included via Houdini's `.inc` mechanism).
+- `NodeShapes/`, `NodeShapeFlags/` — custom node shape definitions.
+
+These are reloaded via `HCSession.reloadColorSchemes()` (`hou.ui.reloadColorScheme()` + `hou.ui.reloadViewportColorSchemes()`).
+
+### Other asset directories
+
+- `otls/` — HDAs (Houdini Digital Assets). `otls/backup/` is gitignored.
+- `desktop/hc_attached.desk` — main desktop layout (single-window, multi-pane). `desktop/hc_detached.desk` — minimal layout (single SceneViewer pane) used when `desktop_mode` is `detached` and floating panels handle the rest.
+- `radialmenu/`, `toolbar/`, `presets/`, `vex/`, `help/` — Houdini conventional subdirectories loaded by path.
+- `MainMenuCommon.xml`, `OPmenu.xml`, `PARMmenu.xmlx`, `ParmGearMenu.xml` — menu definitions. Each `scriptItem` typically does `from hc import HCSession; HCSession().someMethod()`.
+
+## Conventions
+
+- Wrappers never subclass `hou.*` types; they store the hou object on `self.hou_tab` / `self.hou_pane` and delegate.
+- `.type()` methods return string discriminators (`'HCNetworkEditor'`, `'HCSceneViewer'`, `'HCParameterTab'`, `'HCPathTab'`) — this is the idiomatic way to branch on tab kind in this codebase, not `isinstance`. When branching, remember that `'HCParameterTab'` is a sibling of `'HCPathTab'` (Parm tabs return the former, DetailsView returns the latter) — code that wants both should check `tab.type() in ('HCPathTab', 'HCParameterTab')`.
+- Toggle-style methods often use a small string map (e.g. `{'0': '1', '1': '0'}`) because Houdini prefs are stored as strings.
+- Paths to bundled files are built from `hou.getenv("HC_PATH")` — don't hardcode absolute paths.
diff --git a/MainMenuCommon.xml b/MainMenuCommon.xml
index 383b2c2..ace9d0b 100755
--- a/MainMenuCommon.xml
+++ b/MainMenuCommon.xml
@@ -23,7 +23,7 @@
<scriptItem id="h.hc_session_keycam">
<label>Keycam</label>
- <scriptCode><![CDATA[from hc import HCSession; HCSession().currentTab().keycam()]]></scriptCode>
+ <scriptCode><![CDATA[from hc import HCSession; HCSession().keycam()]]></scriptCode>
</scriptItem>
<scriptItem id="h.hc_session_reloadcolors">
@@ -46,6 +46,31 @@
<scriptCode><![CDATA[from hc import HCSession; HCSession().reloadKeycam()]]></scriptCode>
</scriptItem>
+ <scriptItem id="h.hc_session_rotatesplit">
+ <label>Rotate Split</label>
+ <scriptCode><![CDATA[from hc import HCSession; HCSession().rotateSplit()]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="h.hc_session_opensettings">
+ <label>Open HC Settings</label>
+ <scriptCode><![CDATA[from hc import HCSession; HCSession().openSettings()]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="h.hc_session_togglecurrenttabs">
+ <label>Toggle Tabs (Current Pane)</label>
+ <scriptCode><![CDATA[from hc import HCSession; HCSession().toggleCurrentPaneTabs()]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="h.hc_session_newtabdialog">
+ <label>New Pane Tab...</label>
+ <scriptCode><![CDATA[from hc import HCSession; HCSession().newTabDialog()]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="h.hc_session_toggletabs">
+ <label>Toggle Tabs (All Panes)</label>
+ <scriptCode><![CDATA[from hc import HCSession; HCSession().toggleAllTabs()]]></scriptCode>
+ </scriptItem>
+
<!-- <scriptItem id="h.hc_session_restarthoudini"> -->
<!-- <label>Restart Houdini</label> -->
<!-- <scriptCode><![CDATA[from hc import HCSession; HCSession().restartHoudini()]]></scriptCode> -->
@@ -61,16 +86,16 @@
<scriptCode><![CDATA[from hc import HCSession; HCSession().toggleNetworkControls()]]></scriptCode>
</scriptItem>
- <scriptItem id="h.hc_session_toggletabs">
- <label>Toggle Tabs</label>
- <scriptCode><![CDATA[from hc import HCSession; HCSession().toggleTabs()]]></scriptCode>
- </scriptItem>
-
<scriptItem id="h.hc_session_togglestowbars">
<label>Toggle Stowbars</label>
<scriptCode><![CDATA[from hc import HCSession; HCSession().toggleStowbars()]]></scriptCode>
</scriptItem>
+ <scriptItem id="h.hc_session_togglemainmenu">
+ <label>Toggle Main Menu</label>
+ <scriptCode><![CDATA[from hc import HCSession; HCSession().toggleMainMenu()]]></scriptCode>
+ </scriptItem>
+
<!-- <scriptItem id="h.hc_session_updatemainmenubar"> -->
<!-- <label>Update Main Menu Bar</label> -->
<!-- <scriptCode><![CDATA[from hc import HCSession; HCSession().updateMainMenuBar()]]></scriptCode> -->
diff --git a/NetworkViewMenu.xml b/NetworkViewMenu.xml
new file mode 100644
index 0000000..3dc96a7
--- /dev/null
+++ b/NetworkViewMenu.xml
@@ -0,0 +1,261 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<mainMenu>
+ <menuBar>
+
+ <subMenu id="hc_network_menu">
+ <label>HC</label>
+
+ <titleItem>
+ <label>Navigate (Current)</label>
+ </titleItem>
+
+ <scriptItem id="pane.wsheet.hc_nav_up">
+ <label>Navigate Up</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).navigateDirection('up')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_nav_down">
+ <label>Navigate Down</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).navigateDirection('down')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_nav_left">
+ <label>Navigate Left</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).navigateDirection('left')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_nav_right">
+ <label>Navigate Right</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).navigateDirection('right')
+]]></scriptCode>
+ </scriptItem>
+
+ <separatorItem />
+
+ <titleItem>
+ <label>Extend Selection</label>
+ </titleItem>
+
+ <scriptItem id="pane.wsheet.hc_select_up">
+ <label>Extend Selection Up</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).addToSelection('up')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_select_down">
+ <label>Extend Selection Down</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).addToSelection('down')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_select_left">
+ <label>Extend Selection Left</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).addToSelection('left')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_select_right">
+ <label>Extend Selection Right</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).addToSelection('right')
+]]></scriptCode>
+ </scriptItem>
+
+ <separatorItem />
+
+ <titleItem>
+ <label>Move Selected</label>
+ </titleItem>
+
+ <scriptItem id="pane.wsheet.hc_move_up">
+ <label>Move Up</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).translateSelectedNodes('up')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_move_down">
+ <label>Move Down</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).translateSelectedNodes('down')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_move_left">
+ <label>Move Left</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).translateSelectedNodes('left')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_move_right">
+ <label>Move Right</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).translateSelectedNodes('right')
+]]></scriptCode>
+ </scriptItem>
+
+ <separatorItem />
+
+ <titleItem>
+ <label>View</label>
+ </titleItem>
+
+ <scriptItem id="pane.wsheet.hc_view_up">
+ <label>Pan View Up</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).translateView('up')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_view_down">
+ <label>Pan View Down</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).translateView('down')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_view_left">
+ <label>Pan View Left</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).translateView('left')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_view_right">
+ <label>Pan View Right</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).translateView('right')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_zoom_in">
+ <label>Zoom In</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).zoom('in')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_zoom_out">
+ <label>Zoom Out</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).zoom('out')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_frame_all">
+ <label>Frame All</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).frameAll()
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_toggle_grid">
+ <label>Toggle Grid</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).toggleGridMode()
+]]></scriptCode>
+ </scriptItem>
+
+ <separatorItem />
+
+ <titleItem>
+ <label>Selection</label>
+ </titleItem>
+
+ <scriptItem id="pane.wsheet.hc_select_all">
+ <label>Select All</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).selectAllNodes()
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_deselect_all">
+ <label>Deselect All</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).deselectAllNodes()
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_select_next_component">
+ <label>Select Next Connected Group</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).selectComponent('next')
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_select_prev_component">
+ <label>Select Previous Connected Group</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).selectComponent('prev')
+]]></scriptCode>
+ </scriptItem>
+
+ <separatorItem />
+
+ <titleItem>
+ <label>Flags</label>
+ </titleItem>
+
+ <scriptItem id="pane.wsheet.hc_toggle_bypass">
+ <label>Toggle Bypass</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).toggleBypassFlag()
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_toggle_template">
+ <label>Toggle Template</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).toggleTemplateFlag()
+]]></scriptCode>
+ </scriptItem>
+
+ <scriptItem id="pane.wsheet.hc_set_display">
+ <label>Set Display</label>
+ <scriptCode><![CDATA[
+from hc import HCNetworkEditor
+HCNetworkEditor(kwargs['editor']).setDisplayFlag()
+]]></scriptCode>
+ </scriptItem>
+
+ </subMenu>
+
+ </menuBar>
+</mainMenu>
diff --git a/config/Cursors/CursorMapping b/config/Cursors/CursorMapping
new file mode 100644
index 0000000..5f146a0
--- /dev/null
+++ b/config/Cursors/CursorMapping
@@ -0,0 +1,19 @@
+# Local overrides for Houdini's cursor mapping.
+# When Houdini merges this with $HFS/houdini/config/Cursors/CursorMapping,
+# entries here replace the defaults.
+
+# Default arrow cursor: auto-center hotspot (our white.svg is a centered cross)
+iris.x := -1;
+iris.y := -1;
+
+# Default viewport hover cursor: auto-center hotspot (our grab.svg is a centered cross)
+hand.x := -1;
+hand.y := -1;
+
+# Viewport tumble (LMB held): auto-center hotspot (our grabbing.svg is a centered 8-point star)
+DM_viewTumble.x := -1;
+DM_viewTumble.y := -1;
+
+# Network editor wire cursor (over node inputs/outputs): auto-center hotspot
+wire.x := -1;
+wire.y := -1;
diff --git a/config/Cursors/grab.svg b/config/Cursors/grab.svg
new file mode 100644
index 0000000..30a4134
--- /dev/null
+++ b/config/Cursors/grab.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
+ <!-- Black outline: two thick rectangles forming a cross -->
+ <rect x="21" y="3" width="6" height="42" fill="black"/>
+ <rect x="3" y="21" width="42" height="6" fill="black"/>
+ <!-- Light blue-green fill: two narrower rectangles on top -->
+ <rect x="23" y="5" width="2" height="38" fill="#a0d8cc"/>
+ <rect x="5" y="23" width="38" height="2" fill="#a0d8cc"/>
+</svg>
diff --git a/config/Cursors/grabbing.svg b/config/Cursors/grabbing.svg
new file mode 100644
index 0000000..0205165
--- /dev/null
+++ b/config/Cursors/grabbing.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
+ <!-- Main cross -->
+ <rect x="21" y="3" width="6" height="42" fill="black"/>
+ <rect x="3" y="21" width="42" height="6" fill="black"/>
+ <rect x="23" y="5" width="2" height="38" fill="#a0d8cc"/>
+ <rect x="5" y="23" width="38" height="2" fill="#a0d8cc"/>
+ <!-- Shorter cross rotated 45° -->
+ <g transform="rotate(45 24 24)">
+ <rect x="21" y="12" width="6" height="24" fill="black"/>
+ <rect x="12" y="21" width="24" height="6" fill="black"/>
+ <rect x="23" y="14" width="2" height="20" fill="#a0d8cc"/>
+ <rect x="14" y="23" width="20" height="2" fill="#a0d8cc"/>
+ </g>
+</svg>
diff --git a/config/Cursors/white.svg b/config/Cursors/white.svg
new file mode 100644
index 0000000..e4d9d45
--- /dev/null
+++ b/config/Cursors/white.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
+ <!-- Black outline: two thick rectangles forming a cross -->
+ <rect x="21" y="3" width="6" height="42" fill="black"/>
+ <rect x="3" y="21" width="42" height="6" fill="black"/>
+ <!-- Dimmed fill: two narrower rectangles on top -->
+ <rect x="23" y="5" width="2" height="38" fill="#bbbbbb"/>
+ <rect x="5" y="23" width="38" height="2" fill="#bbbbbb"/>
+</svg>
diff --git a/config/Cursors/wire.svg b/config/Cursors/wire.svg
new file mode 100644
index 0000000..30a4134
--- /dev/null
+++ b/config/Cursors/wire.svg
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
+ <!-- Black outline: two thick rectangles forming a cross -->
+ <rect x="21" y="3" width="6" height="42" fill="black"/>
+ <rect x="3" y="21" width="42" height="6" fill="black"/>
+ <!-- Light blue-green fill: two narrower rectangles on top -->
+ <rect x="23" y="5" width="2" height="38" fill="#a0d8cc"/>
+ <rect x="5" y="23" width="38" height="2" fill="#a0d8cc"/>
+</svg>
diff --git a/config/hc_colors.inc b/config/hc_colors.inc
index 74aefa2..3977bc0 100644
--- a/config/hc_colors.inc
+++ b/config/hc_colors.inc
@@ -60,7 +60,7 @@
#define graph_wire_unused #ff0000
#define graph_node_connector hl_high
#define graph_node_current hl_high
-#define graph_node_current_non_selected #30a090
+#define graph_node_current_non_selected graph_node_current
#define graph_node_selected #c0c0c0
#define graph_node_flag #303030
diff --git a/desktop/hc_desktop.desk b/desktop/hc_attached.desk
similarity index 100%
rename from desktop/hc_desktop.desk
rename to desktop/hc_attached.desk
diff --git a/desktop/hc_detached.desk b/desktop/hc_detached.desk
new file mode 100644
index 0000000..e0ed864
--- /dev/null
+++ b/desktop/hc_detached.desk
@@ -0,0 +1,14 @@
+fullscreen -1
+playbarshown 0
+playbar -1
+shelf 0
+menubar -1
+statusbar -1
+root {
+ tabs 0 0 {
+ pane panetab0 -a 1 -l 10 -H /obj/ -T OBJ -m sceneview
+ }
+}
+viewerstow -x stow -b stow -l stow -m hide -c hide -d stow ${DESKNAME}.panetab0.world
+vieweroption -a 2 ${DESKNAME}.panetab0.world
+viewlayout -s 2 ${DESKNAME}.panetab0.world
diff --git a/scripts/hotkeys_assign.json b/hc_hotkeys.json
similarity index 57%
rename from scripts/hotkeys_assign.json
rename to hc_hotkeys.json
index c1b1bda..27e492f 100644
--- a/scripts/hotkeys_assign.json
+++ b/hc_hotkeys.json
@@ -2,16 +2,43 @@
"assignments": {
"h.hc_panel": "alt+shift+d",
"h.hc_session_keycam": "shift+k",
+ "h.hc_session_rotatesplit": "alt+tab",
+ "h.hc_session_opensettings": "alt+,",
+ "h.hc_session_toggletabs": "alt+t",
+ "h.hc_session_newtabdialog": "alt+shift+t",
+ "h.hc_session_togglemainmenu": "alt+m",
"h.pane.nexttab": "ctrl+tab",
"h.pane.prevtab": "ctrl+shift+tab",
"h.pane.gview.refplane": "shift+g",
"h.pane.gview.state.view.home_selection": "shift+s",
"h.pane.parms.edit_expression": "alt+e",
"h.pane.editparms.selectall": "alt+h",
- "h.pane.wsheet.up": "ctrl+k",
- "h.pane.wsheet.down": "ctrl+j",
- "h.pane.wsheet.left": "ctrl+h",
- "h.pane.wsheet.right": "ctrl+l",
+ "h.pane.wsheet.hc_nav_up": "ctrl+k",
+ "h.pane.wsheet.hc_nav_down": "ctrl+j",
+ "h.pane.wsheet.hc_nav_left": "ctrl+h",
+ "h.pane.wsheet.hc_nav_right": "ctrl+l",
+ "h.pane.wsheet.hc_select_up": "ctrl+shift+k",
+ "h.pane.wsheet.hc_select_down": "ctrl+shift+j",
+ "h.pane.wsheet.hc_select_left": "ctrl+shift+h",
+ "h.pane.wsheet.hc_select_right": "ctrl+shift+l",
+ "h.pane.wsheet.hc_move_up": "alt+k",
+ "h.pane.wsheet.hc_move_down": "alt+j",
+ "h.pane.wsheet.hc_move_left": "alt+h",
+ "h.pane.wsheet.hc_move_right": "alt+l",
+ "h.pane.wsheet.hc_view_up": "k",
+ "h.pane.wsheet.hc_view_down": "j",
+ "h.pane.wsheet.hc_view_left": "h",
+ "h.pane.wsheet.hc_view_right": "l",
+ "h.pane.wsheet.hc_zoom_in": "=",
+ "h.pane.wsheet.hc_zoom_out": "-",
+ "h.pane.wsheet.hc_toggle_grid": "shift+g",
+ "h.pane.wsheet.hc_select_all": "ctrl+a",
+ "h.pane.wsheet.hc_deselect_all": "ctrl+d",
+ "h.pane.wsheet.hc_select_next_component": "shift+]",
+ "h.pane.wsheet.hc_select_prev_component": "shift+[",
+ "h.pane.wsheet.hc_toggle_bypass": "q",
+ "h.pane.wsheet.hc_toggle_template": "w",
+ "h.pane.wsheet.hc_set_display": "e",
"h.pane.wsheet.drop_on_wire_mode": "pad8",
"h.pane.wsheet.home_selected": "shift+s",
"h.pane.wsheet.node_quick_nav": "ctrl+f",
diff --git a/scripts/settings.json b/hc_settings.json
similarity index 95%
rename from scripts/settings.json
rename to hc_settings.json
index 326e412..b816cc0 100644
--- a/scripts/settings.json
+++ b/hc_settings.json
@@ -1,4 +1,5 @@
{
+ "desktop_mode": "detached",
"keycam": {
"guides": {
"axis_size": 0.2,
diff --git a/mcpserver/houdini_mcp.py b/mcpserver/houdini_mcp.py
new file mode 100644
index 0000000..3edae4a
--- /dev/null
+++ b/mcpserver/houdini_mcp.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+import os
+import rpyc
+from mcp.server.fastmcp import FastMCP
+
+HOUDINI_HOST = os.environ.get("HOUDINI_MCP_HOST", "localhost")
+HOUDINI_PORT = int(os.environ.get("HOUDINI_MCP_PORT", "18811"))
+
+mcp = FastMCP("houdini")
+
+
+_REMOTE_RUNNER_SRC = r'''
+def _hcrun(code):
+ import io, contextlib, traceback, sys
+ buf = io.StringIO()
+ result_repr = None
+ error = None
+ main_ns = sys.modules["__main__"].__dict__
+ try:
+ import hou
+ if "hou" not in main_ns:
+ main_ns["hou"] = hou
+ except ImportError:
+ pass
+ try:
+ with contextlib.redirect_stdout(buf), contextlib.redirect_stderr(buf):
+ try:
+ result = eval(compile(code, "<mcp>", "eval"), main_ns)
+ if result is not None:
+ try:
+ result_repr = repr(result)
+ except Exception as _e:
+ result_repr = "<unrepr-able: " + repr(_e) + ">"
+ except SyntaxError:
+ exec(compile(code, "<mcp>", "exec"), main_ns)
+ except Exception:
+ error = traceback.format_exc()
+ return buf.getvalue(), result_repr, error
+'''
+
+
[email protected]()
+def houdini_eval(code: str) -> str:
+ """Run Python code inside the running Houdini session.
+
+ The `hou` module and anything in __main__ is available. Captures stdout/stderr.
+ If the code is a single expression, its repr is returned too.
+ Requires Houdini to be running with hrpyc.start_server() called (happens
+ automatically in uiready.py for this project).
+ """
+ try:
+ conn = rpyc.classic.connect(HOUDINI_HOST, HOUDINI_PORT)
+ except ConnectionRefusedError:
+ return (
+ f"ERROR: Could not connect to Houdini at {HOUDINI_HOST}:{HOUDINI_PORT}. "
+ "Is Houdini running with hrpyc started?"
+ )
+
+ try:
+ conn.execute(_REMOTE_RUNNER_SRC)
+ stdout, result_repr, error = conn.namespace["_hcrun"](code)
+ stdout = str(stdout) if stdout else ""
+ result_repr = str(result_repr) if result_repr else None
+ error = str(error) if error else None
+ finally:
+ try:
+ conn.close()
+ except Exception:
+ pass
+
+ parts = []
+ if stdout:
+ parts.append(stdout.rstrip())
+ if result_repr is not None:
+ parts.append(f"=> {result_repr}")
+ if error:
+ parts.append(error.rstrip())
+ return "\n".join(parts) if parts else "(no output)"
+
+
[email protected]()
+def houdini_ping() -> str:
+ """Check whether the Houdini RPC server is reachable."""
+ try:
+ conn = rpyc.classic.connect(HOUDINI_HOST, HOUDINI_PORT)
+ try:
+ version = conn.modules.hou.applicationVersionString()
+ hip = conn.modules.hou.hipFile.path()
+ return f"OK — Houdini {version}, hip: {hip}"
+ finally:
+ conn.close()
+ except ConnectionRefusedError:
+ return f"Unreachable at {HOUDINI_HOST}:{HOUDINI_PORT}"
+ except Exception as e:
+ return f"Error: {e!r}"
+
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/mcpserver/requirements.txt b/mcpserver/requirements.txt
new file mode 100644
index 0000000..84b85ea
--- /dev/null
+++ b/mcpserver/requirements.txt
@@ -0,0 +1,2 @@
+mcp>=1.0
+rpyc>=4.1,<5
diff --git a/notes/IM.md b/notes/IM.md
deleted file mode 100644
index e69de29..0000000
diff --git a/notes/style.md b/notes/style.md
deleted file mode 100644
index e69de29..0000000
diff --git a/python3.11libs/hc/__init__.py b/python3.11libs/hc/__init__.py
index 2c46b9e..50f066d 100755
--- a/python3.11libs/hc/__init__.py
+++ b/python3.11libs/hc/__init__.py
@@ -3,15 +3,20 @@ from .hccam import HCCam
from .hcdefaultcam import HCDefaultCam
from .hcgeo import HCGeo
from .hcguides import HCGuides
+from .hclayout import HCLayout
from .hcnetworkeditor import HCNetworkEditor
from .hcnode import HCNode
from .hcpane import HCPane
+from .hcparametertab import HCParameterTab
from .hcpathtab import HCPathTab
-from .hcprefs import HCPrefs
+from .hcsettings import HCSettings, HCSettingsPanel
from .hcradial import HCRadial
from .hcsceneviewer import HCSceneViewer
from .hcsession import HCSession
+from .hcsplithandles import HCSplitHandles
from .hcstatus import HCStatus
+from .hcstatusbar import HCStatusBar
+from .hcwindowwatcher import HCWindowWatcher
from .hctab import HCTab
from .hcviewport import HCViewport
from .hcvisualizers import HCVisualizers
@@ -25,16 +30,22 @@ __all__ = [
"HCDefaultCam",
"HCGeo",
"HCGuides",
+ "HCLayout",
"HCNetworkEditor",
"HCNode",
"HCPane",
+ "HCParameterTab",
"HCPathTab",
- "HCPrefs",
"HCRadial",
+ "HCSettings",
+ "HCSettingsPanel",
"HCSession",
"HCSceneViewer",
+ "HCSplitHandles",
"HCStatus",
+ "HCStatusBar",
"HCTab",
+ "HCWindowWatcher",
"HCViewport",
"HCVisualizers",
"HCWidgets"
diff --git a/python3.11libs/hc/hcbindings.py b/python3.11libs/hc/hcbindings.py
index e516653..3026676 100644
--- a/python3.11libs/hc/hcbindings.py
+++ b/python3.11libs/hc/hcbindings.py
@@ -15,35 +15,40 @@ pause
space
"""
+
class HCBindings:
def __init__(self):
return
def load(self):
- self.clear()
self.assign()
- def clear(self):
- path = Path(hou.getenv("HC_PATH"))
- file = path / "scripts" / "hotkeys_clear.json"
- data = json.loads(file.read_text())
- commands = data.get("commands")
- for command in commands:
- context = command.rpartition('.')[0]
- hou.hotkeys.clearAssignments(context, command)
-
def assign(self):
- path = Path(hou.getenv("HC_PATH"))
- file = path / "scripts" / "hotkeys_assign.json"
- data = json.loads(file.read_text())
- assignments = data.get("assignments")
+ path = Path(hou.getenv("HC_PATH")) / "hc_hotkeys.json"
+ data = json.loads(path.read_text())
+ assignments = data.get("assignments", {})
for symbol, key in assignments.items():
context = symbol.rpartition('.')[0]
- hou.hotkeys.addAssignment(context, symbol, key)
+ self._clearConflicts(context, symbol, key)
+ ok = hou.hotkeys.addAssignment(context, symbol, key)
+ if not ok:
+ print(f"[HCBindings] failed to assign {symbol!r} -> {key!r} in context {context!r}")
- def bindCommands(self):
- list = ()
- for command in list:
- context = binding[0]
- command = binding[1]
- hou.hotkeys.addCommandBinding(context, command)
+ def _clearConflicts(self, context, symbol, key):
+ try:
+ conflicts = hou.hotkeys.findConflicts(context, symbol, key)
+ except hou.OperationFailed as e:
+ print(f"[HCBindings] findConflicts error for {symbol!r} {key!r}: {e}")
+ return
+ for entry in conflicts:
+ if '?' in entry:
+ conflict_ctx, conflict_sym = entry.split('?', 1)
+ else:
+ conflict_ctx, conflict_sym = context, entry
+ if conflict_sym == symbol:
+ continue
+ ok = hou.hotkeys.removeAssignment(conflict_ctx, conflict_sym, key)
+ if ok:
+ print(f"[HCBindings] unbound {key!r} from conflicting {conflict_sym!r}")
+ else:
+ print(f"[HCBindings] failed to unbind {key!r} from {conflict_sym!r}")
diff --git a/python3.11libs/hc/hcguides.py b/python3.11libs/hc/hcguides.py
index a68f311..b179e1f 100644
--- a/python3.11libs/hc/hcguides.py
+++ b/python3.11libs/hc/hcguides.py
@@ -84,8 +84,8 @@ class HCGuides:
if self.ray_enabled: self.ray.draw(draw_handle, {})
def update(self):
- from hc import HCPrefs
- prefs = HCPrefs().prefs().get('keycam').get('guides')
+ from hc import HCSettings
+ prefs = HCSettings().prefs().get('keycam').get('guides')
self.axis_size = prefs.get('axis_size')
self.tie_axis_to_radius = prefs.get('tie_axis_to_radius')
self.bbox_enabled = prefs.get('bbox_enabled')
diff --git a/python3.11libs/hc/hclayout.py b/python3.11libs/hc/hclayout.py
new file mode 100644
index 0000000..0d9c157
--- /dev/null
+++ b/python3.11libs/hc/hclayout.py
@@ -0,0 +1,202 @@
+import hou
+from .hcsession import HCSession
+
+
+DEFAULT_FLOATING_PANE_TYPES = (
+ hou.paneTabType.Parm,
+ hou.paneTabType.NetworkEditor,
+)
+
+
+class HCLayout:
+ """Wraps the current Houdini desktop / pane / floating-panel state."""
+
+
+ """ Desktop """
+
+
+ def desktop(self):
+ return hou.ui.curDesktop()
+
+ def desktopName(self):
+ return self.desktop().name()
+
+ def mode(self):
+ return "detached" if self.desktopName() == "hc_detached" else "attached"
+
+ def setDesktop(self, name):
+ for d in hou.ui.desktops():
+ if d.name() == name:
+ d.setAsCurrent()
+ return True
+ return False
+
+
+ """ Panes """
+
+
+ def panes(self):
+ return self.desktop().panes()
+
+ def paneCount(self):
+ return len(self.panes())
+
+ def paneTabs(self):
+ tabs = []
+ for pane in self.panes():
+ tabs.extend(pane.tabs())
+ return tabs
+
+ def paneTabTypes(self):
+ return [tab.type() for tab in self.paneTabs()]
+
+
+ """ Floating """
+
+
+ def floatingPanels(self):
+ return [p for p in hou.ui.floatingPanels() if p.qtParentWindow() is not None]
+
+ def floatingPaneTabs(self):
+ tabs = []
+ for fp in self.floatingPanels():
+ tabs.extend(fp.paneTabs())
+ return tabs
+
+ def floatingPaneTabTypes(self):
+ return [tab.type() for tab in self.floatingPaneTabs()]
+
+ def launchFloating(self, pane_types=None):
+ """Open one floating panel per pane type, skipping any already present.
+
+ Each floating panel is a separate OS window the WM can tile. The main
+ Houdini window still exists alongside them. Each new pane tab gets
+ initialize() called on its HC* wrapper so per-type defaults can apply.
+ """
+ from .hcpane import HCPane
+ if pane_types is None:
+ pane_types = DEFAULT_FLOATING_PANE_TYPES
+ existing = set(self.floatingPaneTabTypes())
+ created = []
+ for pane_type in pane_types:
+ if pane_type in existing:
+ continue
+ panel = self.desktop().createFloatingPanel(pane_type, immediate=True)
+ panel.attachToDesktop(True)
+ created.append(panel)
+ initialized_panes = set()
+ for hou_tab in panel.paneTabs():
+ pane = hou_tab.pane()
+ if pane is None:
+ continue
+ hc_pane = HCPane(pane)
+ if pane.id() not in initialized_panes:
+ try:
+ hc_pane.initialize()
+ except Exception as e:
+ print(f"[HCLayout] HCPane.initialize() failed: {e!r}")
+ initialized_panes.add(pane.id())
+ hc_tab = hc_pane.convertTab(hou_tab)
+ try:
+ hc_tab.initialize()
+ except Exception as e:
+ print(f"[HCLayout] initialize() failed for {hou_tab.type().name()}: {e!r}")
+ return created
+
+
+ """ Snapshot """
+
+
+ def info(self):
+ return {
+ "desktop": self.desktopName(),
+ "mode": self.mode(),
+ "pane_count": self.paneCount(),
+ "pane_tab_types": [t.name() for t in self.paneTabTypes()],
+ "floating_pane_count": len(self.floatingPanels()),
+ "floating_pane_tab_types": [t.name() for t in self.floatingPaneTabTypes()],
+ }
+
+
+def launchFloatingLayout(pane_types=None):
+ return HCLayout().launchFloating(pane_types)
+
+
+""" Legacy free functions (broken — kept for reference) """
+
+
+def setLayoutQuad(self):
+ session = HCSession()
+ session.clearLayout()
+ session.allTabs()[0].setType("PythonShell")
+ session.allPanes()[0].splitHorizontal()
+ session.allPanes()[0].splitVertical()
+ session.allPanes()[1].splitVertical()
+
+def setLayoutRamp(self):
+ session = HCSession()
+ session.removeEventLoopCallbacks()
+ session.clearLayout()
+ session.allPanes()[0].splitVertical()
+ session.allTabs()[1].setType("Parm")
+ session.allPanes()[1].setSplitRatio(0.3)
+ session.allPanes()[1].createTab()
+
+def setLayoutTriH(self):
+ session = HCSession()
+ session.removeEventLoopCallbacks()
+ session.clearLayout()
+ session.allTabs()[0].setType("PythonShell")
+ session.allPanes()[0].splitHorizontal()
+ session.allPanes()[1].splitHorizontal()
+ session.allPanes()[1].createTab("PythonShell")
+ session.allPanes()[1].allTabs()[0].setIsCurrentTab()
+ session.allPanes()[0].tabs()[0].setType("SceneViewer")
+ session.allPanes()[1].tabs()[0].setType("Parm")
+ session.allPanes()[1].tabs()[1].setType("DetailsView")
+ session.allPanes()[2].tabs()[0].setType("NetworkEditor")
+ session.allPanes()[0].setSplitFraction(0.5)
+ session.prev_pane = self.pane()
+ hou.ui.addEventLoopCallback(triHCallback)
+
+def triHCallback(session):
+ panes = session.panes()
+ pane = session.pane()
+ if str(pane) != str(hou.session.lastPane):
+ hou.session.last_pane = pane
+ if str(pane) == str(panes[1]):
+ pane.setSplitFraction(0.6)
+ elif str(pane) == str(panes[2]):
+ pane.setSplitFraction(0.3)
+ return True
+
+def setLayoutTriV(session):
+ session.removeEventLoopCallbacks()
+ session.clearLayout()
+ session.allPanes()[0].tabs()[0].setType("PythonShell")
+ session.allPanes()[0].splitHorizontal()
+ session.allPanes()[1].splitVertical()
+ session.allPanes()[1].createTab("PythonShell")
+ session.allPanes()[1].allTabs()[0].setIsCurrentTab()
+ session.allPanes()[0].tabs()[0].setType("SceneViewer")
+ session.allPanes()[1].tabs()[0].setType("Parm")
+ session.allPanes()[1].tabs()[1].setType("DetailsView")
+ session.allPanes()[2].tabs()[0].setType("NetworkEditor")
+ session.allPanes()[0].setSplitFraction(0.66)
+ hou.session.last_pane = self.pane()
+ hou.ui.addEventLoopCallback(self.triVCallback)
+
+def triVCallback(session):
+ panes = session.allPanes()
+ pane = session.currentPane()
+ if str(pane) != str(hou.session.last_pane):
+ hou.session.last_pane = pane
+ if str(pane) == str(panes[0]):
+ pane.setSplitFraction(0.7)
+ elif str(pane) == str(panes[1]):
+ panes[0].setSplitFraction(0.4)
+ pane.setSplitFraction(0.33)
+ elif str(pane) == str(panes[2]):
+ panes[0].setSplitFraction(0.4)
+ pane.setSplitFraction(0.66)
+ return True
diff --git a/python3.11libs/hc/hclayouts.py b/python3.11libs/hc/hclayouts.py
deleted file mode 100644
index 6dc4bb8..0000000
--- a/python3.11libs/hc/hclayouts.py
+++ /dev/null
@@ -1,88 +0,0 @@
-from .hcsession import HCSession
-
-def setLayoutQuad(self):
- session = HCSession()
- session.clearLayout()
- session.allTabs()[0].setType("PythonShell")
- session.allPanes()[0].splitHorizontal()
- session.allPanes()[0].splitVertical()
- session.allPanes()[1].splitVertical()
-
-def setLayoutRamp(self):
- session = HCSession()
- session.removeEventLoopCallbacks()
- session.clearLayout()
- session.allPanes()[0].splitVertical()
- session.allTabs()[1].setType("Parm")
- session.allPanes()[1].setSplitRatio(0.3)
- session.allPanes()[1].createTab()
-
-def setLayoutTriH(self):
- session = HCSession()
- session.removeEventLoopCallbacks()
- session.clearLayout()
- # Make panes
- session.allTabs()[0].setType("PythonShell")
- session.allPanes()[0].splitHorizontal()
- session.allPanes()[1].splitHorizontal()
- # Make tabs
- session.allPanes()[1].createTab("PythonShell")
- session.allPanes()[1].allTabs()[0].setIsCurrentTab()
- # Set types
- session.allPanes()[0].tabs()[0].setType("SceneViewer")
- session.allPanes()[1].tabs()[0].setType("Parm")
- session.allPanes()[1].tabs()[1].setType("DetailsView")
- session.allPanes()[2].tabs()[0].setType("NetworkEditor")
- # Ratios
- session.allPanes()[0].setSplitFraction(0.5)
- session.prev_pane = self.pane()
- hou.ui.addEventLoopCallback(triHCallback)
-
-def triHCallback(session):
- panes = session.panes()
- pane = session.pane()
- if str(pane) != str(hou.session.lastPane):
- hou.session.last_pane = pane
- if str(pane) == str(panes[1]):
- pane.setSplitFraction(0.6)
- elif str(pane) == str(panes[2]):
- pane.setSplitFraction(0.3)
- return True
-
-def setLayoutTriV(session):
- # Remove callbacks
- session.removeEventLoopCallbacks()
- # Reset layout
- session.clearLayout()
- # Make panes
- session.allPanes()[0].tabs()[0].setType("PythonShell")
- session.allPanes()[0].splitHorizontal()
- session.allPanes()[1].splitVertical()
- # Make pane tabs
- session.allPanes()[1].createTab("PythonShell")
- session.allPanes()[1].allTabs()[0].setIsCurrentTab()
- # Set types
- session.allPanes()[0].tabs()[0].setType("SceneViewer")
- session.allPanes()[1].tabs()[0].setType("Parm")
- session.allPanes()[1].tabs()[1].setType("DetailsView")
- session.allPanes()[2].tabs()[0].setType("NetworkEditor")
- # Set ratios
- session.allPanes()[0].setSplitFraction(0.66)
- # Ok
- hou.session.last_pane = self.pane()
- hou.ui.addEventLoopCallback(self.triVCallback)
-
-def triVCallback(session):
- panes = session.allPanes()
- pane = session.currentPane()
- if str(pane) != str(hou.session.last_pane):
- hou.session.last_pane = pane
- if str(pane) == str(panes[0]):
- pane.setSplitFraction(0.7)
- elif str(pane) == str(panes[1]):
- panes[0].setSplitFraction(0.4)
- pane.setSplitFraction(0.33)
- elif str(pane) == str(panes[2]):
- panes[0].setSplitFraction(0.4)
- pane.setSplitFraction(0.66)
- return True
diff --git a/python3.11libs/hc/hcmaps.py b/python3.11libs/hc/hcmaps.py
index 88f04b0..65294c7 100644
--- a/python3.11libs/hc/hcmaps.py
+++ b/python3.11libs/hc/hcmaps.py
@@ -15,6 +15,7 @@ class HCMaps:
'Next Tab': pane.nextTab,
'Open File': session.openFile,
'Open Preferences': session.openPreferences,
+ 'Open HC Settings': session.openSettings,
'Previous Tab': pane.prevTab,
'Reload Colors': session.reloadColorSchemes,
'Reload HC': session.reloadHC,
@@ -33,7 +34,11 @@ class HCMaps:
'Toggle All Menus': session.toggleMenus,
'Toggle All Paths': session.toggleAllTabs,
'Toggle Autosave': session.toggleAutoSave,
+ 'Toggle HC Status': session.toggleStatus,
'Toggle Main Menu': session.toggleMainMenu,
+ 'Toggle Split Handles': session.toggleSplitHandles,
+ 'Refresh Split Handles': session.refreshSplitHandles,
+ 'Toggle Window Watcher': session.toggleWindowWatcher,
'Toggle Stowbars': session.toggleStowbars,
'Toggle Tabs': pane.toggleTabs,
'Toggle Update Mode': session.toggleUpdateMode,
@@ -49,6 +54,7 @@ class HCMaps:
'Toggle Backface': tab.toggleBackface,
'Toggle Bars': tab.toggleBars,
'Toggle Display Bar': tab.toggleDisplayBar,
+ 'Toggle Event Listener': tab.toggleEventListener,
'Toggle Grid': tab.toggleGrid,
'Toggle Group List': tab.toggleGroupList,
'Toggle Light Geo': tab.toggleLightGeo,
diff --git a/python3.11libs/hc/hcnetworkeditor.py b/python3.11libs/hc/hcnetworkeditor.py
index d3c34d0..f0b2df4 100644
--- a/python3.11libs/hc/hcnetworkeditor.py
+++ b/python3.11libs/hc/hcnetworkeditor.py
@@ -1,6 +1,13 @@
import hou, math, types
from .hcpathtab import HCPathTab
-from .hcprefs import HCPrefs
+from .hcsettings import HCSettings
+
+
+# Per-network selection-extension paths, keyed by pwd path.
+# Each entry is a list of (node_path, arrived_via_direction) tuples from
+# the anchor (index 0, direction=None) to the head (index -1).
+_selection_paths = {}
+_OPPOSITE = {'up': 'down', 'down': 'up', 'left': 'right', 'right': 'left'}
class HCNetworkEditor(HCPathTab):
@@ -9,19 +16,238 @@ class HCNetworkEditor(HCPathTab):
self.delta_t = 2
self.network_node = self.pwd()
+ def initialize(self):
+ self.setMenuOpen(0)
+
+
+ """ Navigation """
+
+
+ def _traversalTarget(self, direction, from_node=None):
+ """Find the next node via wire traversal from from_node (default: the
+ editor's current node). up = first input; down = first output;
+ left/right = cycle through nodes sharing a downstream child (ordered
+ by x-position)."""
+ if from_node is None:
+ from_node = self.hou_tab.currentNode()
+ if from_node is None:
+ return None
+
+ if direction == 'up':
+ inputs = [n for n in from_node.inputs() if n is not None]
+ return inputs[0] if inputs else None
+ if direction == 'down':
+ outputs = list(from_node.outputs())
+ return outputs[0] if outputs else None
+ if direction in ('left', 'right'):
+ neighbors = set()
+ for child in from_node.outputs():
+ for sibling in child.inputs():
+ if sibling is not None and sibling != from_node:
+ neighbors.add(sibling)
+ if not neighbors:
+ return None
+ ordered = sorted(
+ neighbors | {from_node},
+ key=lambda n: (n.position()[0], n.position()[1]),
+ )
+ idx = ordered.index(from_node)
+ step = -1 if direction == 'left' else 1
+ return ordered[(idx + step) % len(ordered)]
+ return None
+
+ def navigateDirection(self, direction):
+ target = self._traversalTarget(direction)
+ if target is not None:
+ self.hou_tab.setCurrentNode(target)
+ self._frameSelectionInView()
+ self.updateCurrentNodeOverlay()
+
+ def _frameSelectionInView(self, margin_frac=0.1):
+ """Pan or zoom the view so every selected node is visible with a
+ margin proportional to the current viewport size on each side. Pans
+ when the selection fits the current viewport; zooms out otherwise."""
+ parent = self.hou_tab.pwd()
+ selected = parent.selectedChildren()
+ if not selected:
+ return
+
+ sel_rect = self.hou_tab.itemRect(selected[0])
+ for n in selected[1:]:
+ sel_rect.enlargeToContain(self.hou_tab.itemRect(n))
+
+ view = self.hou_tab.visibleBounds()
+ view_size = view.size()
+ margin = hou.Vector2(view_size[0] * margin_frac,
+ view_size[1] * margin_frac)
+ sel_rect.expand(margin)
+
+ needed = sel_rect.size()
+ if needed[0] > view_size[0] or needed[1] > view_size[1]:
+ aspect = view_size[0] / view_size[1]
+ nw, nh = needed[0], needed[1]
+ if nw / nh > aspect:
+ nh = nw / aspect
+ else:
+ nw = nh * aspect
+ center = sel_rect.center()
+ self.setBounds(hou.BoundingRect(
+ center[0] - nw / 2, center[1] - nh / 2,
+ center[0] + nw / 2, center[1] + nh / 2,
+ ))
+ return
+
+ vmin, vmax = view.min(), view.max()
+ smin, smax = sel_rect.min(), sel_rect.max()
+ dx = 0.0
+ if smin[0] < vmin[0]:
+ dx = smin[0] - vmin[0]
+ elif smax[0] > vmax[0]:
+ dx = smax[0] - vmax[0]
+ dy = 0.0
+ if smin[1] < vmin[1]:
+ dy = smin[1] - vmin[1]
+ elif smax[1] > vmax[1]:
+ dy = smax[1] - vmax[1]
+ if dx or dy:
+ view.translate(hou.Vector2(dx, dy))
+ self.setBounds(view)
+
+ def updateCurrentNodeOverlay(self):
+ """Draw an arrow at the viewport edge pointing at the current node if
+ it is off-screen. Clears any overlay when the current node is in
+ view or there is no current node. Uses hou.NetworkShape primitives
+ so the arrow lives on the native overlay layer."""
+ ed = self.hou_tab
+ current = ed.currentNode()
+ if current is None or current.parent() != ed.pwd():
+ ed.setOverlayShapes([])
+ return
+
+ rect = ed.itemRect(current)
+ node_center = ed.posToScreen(hou.Vector2(rect.center()[0],
+ rect.center()[1]))
+ sb = ed.screenBounds()
+ w, h = sb.size()[0], sb.size()[1]
+ margin = 30
+
+ if (margin <= node_center[0] <= w - margin
+ and margin <= node_center[1] <= h - margin):
+ ed.setOverlayShapes([])
+ return
+
+ cx, cy = w / 2.0, h / 2.0
+ dx, dy = node_center[0] - cx, node_center[1] - cy
+ if dx == 0.0 and dy == 0.0:
+ ed.setOverlayShapes([])
+ return
+
+ tvals = []
+ if dx != 0.0:
+ for tx in (margin, w - margin):
+ tt = (tx - cx) / dx
+ if tt > 0:
+ tvals.append(tt)
+ if dy != 0.0:
+ for ty in (margin, h - margin):
+ tt = (ty - cy) / dy
+ if tt > 0:
+ tvals.append(tt)
+ if not tvals:
+ ed.setOverlayShapes([])
+ return
+ t = min(min(tvals), 1.0)
+ tip_x, tip_y = cx + t * dx, cy + t * dy
+
+ angle = math.atan2(dy, dx)
+ shaft_len = 40.0
+ head_len = 14.0
+ head_angle = math.pi / 6.0
+ tail_x = tip_x - shaft_len * math.cos(angle)
+ tail_y = tip_y - shaft_len * math.sin(angle)
+ hl_x = tip_x - head_len * math.cos(angle - head_angle)
+ hl_y = tip_y - head_len * math.sin(angle - head_angle)
+ hr_x = tip_x - head_len * math.cos(angle + head_angle)
+ hr_y = tip_y - head_len * math.sin(angle + head_angle)
+
+ color = hou.Color((0.38, 0.56, 0.56))
+ width = 3.0
+ shapes = [
+ hou.NetworkShapeLine(hou.Vector2(tail_x, tail_y),
+ hou.Vector2(tip_x, tip_y),
+ color=color, alpha=1.0, width=width,
+ screen_space=True, smooth=True),
+ hou.NetworkShapeLine(hou.Vector2(tip_x, tip_y),
+ hou.Vector2(hl_x, hl_y),
+ color=color, alpha=1.0, width=width,
+ screen_space=True, smooth=True),
+ hou.NetworkShapeLine(hou.Vector2(tip_x, tip_y),
+ hou.Vector2(hr_x, hr_y),
+ color=color, alpha=1.0, width=width,
+ screen_space=True, smooth=True),
+ ]
+ ed.setOverlayShapes(shapes)
+
+ def _nearestNodeToViewportCenter(self):
+ bounds = self.bounds()
+ center = bounds.center()
+ best = None
+ best_dist = float("inf")
+ for node in self.hou_tab.pwd().children():
+ npos = node.position()
+ dx = npos[0] - center[0]
+ dy = npos[1] - center[1]
+ dist = (dx * dx + dy * dy) ** 0.5
+ if dist < best_dist:
+ best = node
+ best_dist = dist
+ return best
+
""" Selection """
def addToSelection(self, direction):
- return
- print(direction)
- # direction_map = {
- # 'left':
- # 'down':
- # 'up':
- # 'right':
- # }
+ """Extend or contract a selection chain anchored at the current node.
+ Mirrors text-editor shift+arrow behavior: each press walks the head
+ by one hop via wires; pressing the opposite direction contracts
+ (deselects) the tail node. The path auto-resets whenever the
+ current node no longer matches the anchor."""
+ parent = self.hou_tab.pwd()
+ current = self.hou_tab.currentNode()
+ if current is None:
+ return
+
+ key = parent.path()
+ path = _selection_paths.get(key, [])
+
+ # Reset if stale: anchor must still be the current node, and every
+ # node referenced in the path must still exist.
+ if (not path
+ or path[0][0] != current.path()
+ or any(hou.node(p) is None for p, _ in path)):
+ path = [(current.path(), None)]
+
+ if len(path) >= 2 and _OPPOSITE.get(path[-1][1]) == direction:
+ head = hou.node(path[-1][0])
+ if head is not None:
+ head.setSelected(False)
+ path.pop()
+ _selection_paths[key] = path
+ return
+
+ head = hou.node(path[-1][0])
+ target = self._traversalTarget(direction, from_node=head)
+ if target is None:
+ return
+ if any(p == target.path() for p, _ in path):
+ return
+
+ current.setSelected(True)
+ target.setSelected(True)
+ path.append((target.path(), direction))
+ _selection_paths[key] = path
+ self._frameSelectionInView()
def currentNode(self):
return self.hou_tab.currentNode()
@@ -38,6 +264,98 @@ class HCNetworkEditor(HCPathTab):
""" Connectivity """
+ def connectedComponents(self):
+ """Returns a list of lists of hou.Node, where each inner list is a
+ connected component (nodes reachable via input/output wires in any
+ direction)."""
+ parent = self.hou_tab.pwd()
+ nodes = list(parent.children())
+ node_set = set(nodes)
+ visited = set()
+ components = []
+ for start in nodes:
+ if start in visited:
+ continue
+ component = []
+ stack = [start]
+ while stack:
+ node = stack.pop()
+ if node in visited:
+ continue
+ visited.add(node)
+ component.append(node)
+ neighbors = list(node.inputs()) + list(node.outputs())
+ for neighbor in neighbors:
+ if neighbor is None:
+ continue
+ if neighbor not in node_set:
+ continue
+ if neighbor in visited:
+ continue
+ stack.append(neighbor)
+ components.append(component)
+ return components
+
+ def selectComponent(self, direction):
+ """Select the connected component closest to the current selection in
+ the given spatial direction. direction is 'next' (to the right) or
+ 'prev' (to the left). When two components share the same center x, the
+ one with the higher y wins the tiebreaker."""
+ components = self.connectedComponents()
+ if not components:
+ return
+
+ def sort_key(comp):
+ xs = [n.position()[0] for n in comp]
+ ys = [n.position()[1] for n in comp]
+ cx = (min(xs) + max(xs)) / 2.0
+ cy = (min(ys) + max(ys)) / 2.0
+ # Negate cy so higher y sorts earlier in ascending order.
+ return (cx, -cy)
+
+ current = self.hou_tab.currentNode()
+ selected_set = set(self.hou_tab.pwd().selectedChildren())
+ ref_comp = None
+ if current is not None:
+ for comp in components:
+ if current in comp:
+ ref_comp = comp
+ break
+ if ref_comp is None and selected_set:
+ for comp in components:
+ if any(n in selected_set for n in comp):
+ ref_comp = comp
+ break
+
+ if ref_comp is None:
+ components.sort(key=sort_key)
+ target = components[0] if direction == 'next' else components[-1]
+ else:
+ ref_key = sort_key(ref_comp)
+ if direction == 'next':
+ candidates = [(sort_key(c), c) for c in components if sort_key(c) > ref_key]
+ if not candidates:
+ return
+ candidates.sort(key=lambda t: t[0])
+ target = candidates[0][1]
+ else:
+ candidates = [(sort_key(c), c) for c in components if sort_key(c) < ref_key]
+ if not candidates:
+ return
+ candidates.sort(key=lambda t: t[0], reverse=True)
+ target = candidates[0][1]
+
+ for node in self.hou_tab.pwd().children():
+ node.setSelected(node in target)
+ if target:
+ self.hou_tab.setCurrentNode(target[0], pick_node=False)
+ self._frameSelectionInView()
+ self.updateCurrentNodeOverlay()
+
+
+ """ Legacy / broken connectivity stubs """
+
+
def chunk(self):
search_nodes = [self.currentNode()]
tree = []
@@ -122,6 +440,7 @@ class HCNetworkEditor(HCPathTab):
def setBounds(self, bounds):
self.hou_tab.setVisibleBounds(bounds)
+ self.updateCurrentNodeOverlay()
def translateView(self, direction):
xform_map = {
@@ -134,13 +453,14 @@ class HCNetworkEditor(HCPathTab):
bounds.translate(xform_map[direction])
self.setBounds(bounds)
- def zoom(self, direction):
- scalemap = {
- 'in': (0.75, 0.75),
- 'out': (1.25, 1.25)
- }
+ def zoom(self, direction, amount=0.25, pivot=None):
+ factor = 1.0 - amount if direction == 'in' else 1.0 + amount
bounds = self.bounds()
- bounds.scale(scalemap[direction])
+ if pivot is None:
+ pivot = bounds.center()
+ bounds.translate(pivot * -1)
+ bounds.scale((factor, factor))
+ bounds.translate(pivot)
self.setBounds(bounds)
def zoomLevel(self):
@@ -207,7 +527,7 @@ class HCNetworkEditor(HCPathTab):
node.setName(name[1])
def setNodeColors(self):
- color = hou.ui.selectColor(hou.Color(HCPrefs().node_color))
+ color = hou.ui.selectColor(hou.Color(HCSettings().node_color))
nodes = self.pwd().selectedChildren()
for node in nodes:
node.setColor(color)
diff --git a/python3.11libs/hc/hcpane.py b/python3.11libs/hc/hcpane.py
index 3be0533..8ddef6a 100644
--- a/python3.11libs/hc/hcpane.py
+++ b/python3.11libs/hc/hcpane.py
@@ -1,6 +1,7 @@
import hou
from .hctab import HCTab
from .hcpathtab import HCPathTab
+from .hcparametertab import HCParameterTab
from .hcsceneviewer import HCSceneViewer
from .hcnetworkeditor import HCNetworkEditor
@@ -8,6 +9,11 @@ class HCPane:
def __init__(self, hou_pane):
self.hou_pane = hou_pane
+ def initialize(self):
+ """Per-pane setup hook. Called by factory callers (HCLayout.launchFloating)
+ after a pane is created so per-pane defaults can apply."""
+ self.showTabs(False)
+
""" Adjust size """
def setIsMaximized(self, bool):
@@ -87,7 +93,7 @@ class HCPane:
elif hou_tab_type == hou.paneTabType.DetailsView:
return HCPathTab(hou_tab)
elif hou_tab_type == hou.paneTabType.Parm:
- return HCPathTab(hou_tab)
+ return HCParameterTab(hou_tab)
else:
return HCTab(hou_tab)
@@ -99,8 +105,7 @@ class HCPane:
return self.hou_pane.isShowingPaneTabs()
def newTab(self, tab_type, python_panel_interface=None):
- # self.tab.
- return
+ return self.hou_pane.createTab(tab_type, python_panel_interface)
def nextTab(self):
tabs = self.tabs()
@@ -120,6 +125,7 @@ class HCPane:
def showTabs(self, value):
self.hou_pane.showPaneTabs(value)
+ self.hou_pane.showPaneTabsStow(value)
def tabs(self):
tabs = []
diff --git a/python3.11libs/hc/hcparametertab.py b/python3.11libs/hc/hcparametertab.py
new file mode 100644
index 0000000..272ca1f
--- /dev/null
+++ b/python3.11libs/hc/hcparametertab.py
@@ -0,0 +1,12 @@
+from .hcpathtab import HCPathTab
+
+
+class HCParameterTab(HCPathTab):
+ def __init__(self, hou_tab):
+ self.hou_tab = hou_tab
+
+ def type(self):
+ return "HCParameterTab"
+
+ def initialize(self):
+ self.showNetworkControls(False)
diff --git a/python3.11libs/hc/hcprefs.py b/python3.11libs/hc/hcprefs.py
deleted file mode 100644
index 331ef4c..0000000
--- a/python3.11libs/hc/hcprefs.py
+++ /dev/null
@@ -1,14 +0,0 @@
-import hou
-import json
-from pathlib import Path
-
-class HCPrefs:
- def __init__(self):
- self.node_color = (0.38, 0.38, 0.56)
-
- def prefs(self):
- base_path = hou.getenv('HC_PATH')
- config_file = Path(base_path) / "scripts" / "settings.json"
- if config_file.exists():
- data = json.loads(config_file.read_text())
- return data
diff --git a/python3.11libs/hc/hcsceneviewer.py b/python3.11libs/hc/hcsceneviewer.py
index 896659e..4ad7d8e 100644
--- a/python3.11libs/hc/hcsceneviewer.py
+++ b/python3.11libs/hc/hcsceneviewer.py
@@ -2,6 +2,15 @@ import hou, types
from .hcpathtab import HCPathTab
from .hcviewport import HCViewport
+
+def _hcPrintSceneViewerEvent(**kwargs):
+ event_type = kwargs.get("event_type")
+ viewer = kwargs.get("viewer")
+ viewer_name = viewer.name() if viewer is not None else "?"
+ extras = {k: v for k, v in kwargs.items() if k not in ("event_type", "desktop", "viewer")}
+ extras_str = " ".join(f"{k}={v!r}" for k, v in extras.items())
+ print(f"[HCSceneViewer {viewer_name}] {event_type} {extras_str}".rstrip())
+
"""
Layout/viewport IDs
@@ -230,6 +239,26 @@ class HCSceneViewer(HCPathTab):
self.showSelectionBar(1)
+ """ Events """
+
+ def addEventListener(self):
+ if _hcPrintSceneViewerEvent not in self.hou_tab.eventCallbacks():
+ self.hou_tab.addEventCallback(_hcPrintSceneViewerEvent)
+
+ def removeEventListener(self):
+ if _hcPrintSceneViewerEvent in self.hou_tab.eventCallbacks():
+ self.hou_tab.removeEventCallback(_hcPrintSceneViewerEvent)
+
+ def isListening(self):
+ return _hcPrintSceneViewerEvent in self.hou_tab.eventCallbacks()
+
+ def toggleEventListener(self):
+ if self.isListening():
+ self.removeEventListener()
+ else:
+ self.addEventListener()
+
+
""" Utils """
def type(self):
diff --git a/python3.11libs/hc/hcsession.py b/python3.11libs/hc/hcsession.py
index d261b8f..656fc13 100644
--- a/python3.11libs/hc/hcsession.py
+++ b/python3.11libs/hc/hcsession.py
@@ -24,8 +24,18 @@ class HCSession:
def allPanes(self):
panes = []
+ seen = set()
for pane in self.desktop().panes():
- panes.append(HCPane(pane))
+ if pane.id() not in seen:
+ panes.append(HCPane(pane))
+ seen.add(pane.id())
+ for fp in hou.ui.floatingPanels():
+ if fp.qtParentWindow() is None:
+ continue
+ for pane in fp.panes():
+ if pane.id() not in seen:
+ panes.append(HCPane(pane))
+ seen.add(pane.id())
return panes
def allSceneViewers(self):
@@ -38,8 +48,8 @@ class HCSession:
def allTabs(self):
tabs = []
for pane in self.allPanes():
- for tab in pane.allTabs():
- tabs.append(tab)
+ for hou_tab in pane.hou_pane.tabs():
+ tabs.append(pane.convertTab(hou_tab))
return tabs
def allViewports(self):
@@ -51,10 +61,26 @@ class HCSession:
return viewports
def currentPane(self):
- return HCPane(hou.ui.paneUnderCursor())
+ hou_pane = hou.ui.paneUnderCursor()
+ if hou_pane is None:
+ hou_pane = self._floatingPaneUnderCursor()
+ return HCPane(hou_pane) if hou_pane is not None else None
+
+ def _floatingPaneUnderCursor(self):
+ from PySide6.QtGui import QCursor
+ pos = QCursor.pos()
+ for fp in hou.ui.floatingPanels():
+ qw = fp.qtParentWindow()
+ if qw is None or not qw.geometry().contains(pos):
+ continue
+ panes = fp.panes()
+ if panes:
+ return panes[0]
+ return None
def currentTab(self):
- return self.currentPane().currentTab()
+ pane = self.currentPane()
+ return pane.currentTab() if pane is not None else None
""" Windows """
@@ -78,7 +104,7 @@ class HCSession:
maps = HCMaps()
list_dict = maps.tab_map_base(self, pane, tab)
- if tab_type == 'HCPathTab':
+ if tab_type in ('HCPathTab', 'HCParameterTab'):
list_dict = list_dict | maps.tab_map_path(self, pane, tab)
elif tab_type == 'HCNetworkEditor':
list_dict = list_dict | maps.tab_map_network_editor(self, pane, tab)
@@ -98,6 +124,55 @@ class HCSession:
def openPreferences(self):
hou.ui.openPreferences('ui', '')
+ def toggleStatus(self):
+ from .hcstatus import HCStatus
+ existing = hou.qt.mainWindow().findChild(HCStatus, HCStatus.OBJECT_NAME)
+ if existing is None:
+ existing = HCStatus()
+ if existing.isVisible():
+ existing.hide()
+ else:
+ existing.show()
+ existing.raise_()
+
+ def openSettings(self):
+ from .hcsettings import HCSettingsPanel
+ existing = hou.qt.mainWindow().findChild(HCSettingsPanel, HCSettingsPanel.OBJECT_NAME)
+ if existing is None:
+ existing = HCSettingsPanel()
+ existing.show()
+ existing.raise_()
+ existing.activateWindow()
+
+ def splitHandles(self):
+ from .hcsplithandles import HCSplitHandles
+ if not hasattr(hou.session, "_hc_split_handles"):
+ hou.session._hc_split_handles = HCSplitHandles()
+ return hou.session._hc_split_handles
+
+ def toggleSplitHandles(self):
+ self.splitHandles().toggle()
+
+ def refreshSplitHandles(self):
+ self.splitHandles().refresh()
+
+ def rotateSplit(self):
+ pane = hou.ui.paneUnderCursor()
+ if pane is None:
+ return
+ parent = pane.getSplitParent()
+ if parent is not None:
+ parent.splitRotate()
+
+ def windowWatcher(self):
+ from .hcwindowwatcher import HCWindowWatcher
+ if not hasattr(hou.session, "_hc_window_watcher"):
+ hou.session._hc_window_watcher = HCWindowWatcher()
+ return hou.session._hc_window_watcher
+
+ def toggleWindowWatcher(self):
+ self.windowWatcher().toggle()
+
""" Settings """
@@ -109,11 +184,36 @@ class HCSession:
hou.ui.reloadViewportColorSchemes()
def reloadHC(self):
- hou.ui.reloadPackage(hou.homeHoudiniDirectory() + '/packages/houdini-studio-utils.json')
+ import sys
+ from PySide6.QtWidgets import QWidget
+ # Close any HC-owned Qt widgets that would dangle with stale class refs
+ main = hou.qt.mainWindow()
+ for name in ("hc_status_dialog", "hc_status_overlay"):
+ for w in main.findChildren(QWidget, name):
+ w.hide()
+ w.setParent(None)
+ w.deleteLater()
+ # Drop every hc.* module so the next import reloads from disk
+ removed = [m for m in list(sys.modules) if m == "hc" or m.startswith("hc.")]
+ for m in removed:
+ del sys.modules[m]
+ import hc
+ hou.ui.setStatusMessage(f"Reloaded hc ({len(removed)} modules)")
def reloadHotkeys(self):
HCBindings().load()
+ def keycam(self):
+ tab = self.currentTab()
+ if tab is not None and tab.type() == 'HCSceneViewer':
+ tab.keycam()
+ return
+ scene_viewers = self.allSceneViewers()
+ if scene_viewers:
+ scene_viewers[0].keycam()
+ return
+ hou.ui.setStatusMessage('No scene viewer available', hou.severityType.Error)
+
def reloadKeycam(self):
hou.ui.reloadViewerState('keycam')
@@ -129,6 +229,8 @@ class HCSession:
def toggleAutoSave(self):
map = {'0': '1', '1': '0'}
hou.setPreference('autoSave', map[hou.getPreference('autoSave')])
+ from .hcstatusbar import HCStatusBar
+ HCStatusBar().updateAutosave()
def toggleUpdateMode(self):
mode_map = {
@@ -146,11 +248,6 @@ class HCSession:
def projectPath(self):
return hou.hipFile.path()
- def reloadHC(self):
- import importlib
- import hc
- importlib.reload(hc)
-
def removeEventLoopCallbacks(self):
callbacks = hou.ui.eventLoopCallbacks()
for callback in callbacks:
@@ -227,7 +324,7 @@ class HCSession:
visible = 1
elif tab.isShowingNetworkControls():
visible = 1
- elif tab.type() == 'HCPathTab':
+ elif tab.type() in ('HCPathTab', 'HCParameterTab'):
if tab.isShowingNetworkControls():
visible = 1
elif tab.type() == 'HCSceneViewer':
@@ -268,7 +365,7 @@ class HCSession:
if tab.type() == 'HCNetworkEditor':
tab.showNetworkControls(not visible)
tab.setMenuOpen(0)
- elif tab.type() == 'HCPathTab':
+ elif tab.type() in ('HCPathTab', 'HCParameterTab'):
tab.showNetworkControls(not visible)
elif tab.type() == 'HCSceneViewer':
tab.showNetworkControls(not visible)
@@ -297,6 +394,53 @@ class HCSession:
hou.ui.setHideAllMinimizedStowbars(True)
hou.ui.setHideAllMinimizedStowbars(True)
+ def toggleCurrentPaneTabs(self):
+ pane = self.currentPane()
+ if pane is not None:
+ pane.toggleTabs()
+
+ def newTabDialog(self):
+ pane = self.currentPane()
+ if pane is None:
+ hou.ui.setStatusMessage("No pane under cursor", hou.severityType.Warning)
+ return
+ type_map = {
+ "Apex Editor": hou.paneTabType.ApexEditor,
+ "Asset Browser": hou.paneTabType.AssetBrowser,
+ "Bundle List": hou.paneTabType.BundleList,
+ "Channel Editor": hou.paneTabType.ChannelEditor,
+ "Channel List": hou.paneTabType.ChannelList,
+ "Channel Viewer": hou.paneTabType.ChannelViewer,
+ "Compositor Viewer": hou.paneTabType.CompositorViewer,
+ "Context Viewer": hou.paneTabType.ContextViewer,
+ "Data Tree": hou.paneTabType.DataTree,
+ "Details View": hou.paneTabType.DetailsView,
+ "Handle List": hou.paneTabType.HandleList,
+ "Help Browser": hou.paneTabType.HelpBrowser,
+ "IPR Viewer": hou.paneTabType.IPRViewer,
+ "Light Linker": hou.paneTabType.LightLinker,
+ "Material Palette": hou.paneTabType.MaterialPalette,
+ "Network Editor": hou.paneTabType.NetworkEditor,
+ "Output Viewer": hou.paneTabType.OutputViewer,
+ "Parameter Editor": hou.paneTabType.Parm,
+ "Parameter Spreadsheet": hou.paneTabType.ParmSpreadsheet,
+ "Performance Monitor": hou.paneTabType.PerformanceMonitor,
+ "Python Panel": hou.paneTabType.PythonPanel,
+ "Python Shell": hou.paneTabType.PythonShell,
+ "Render Gallery": hou.paneTabType.RenderGallery,
+ "Scene Viewer": hou.paneTabType.SceneViewer,
+ "Shader Viewer": hou.paneTabType.ShaderViewer,
+ "Take List": hou.paneTabType.TakeList,
+ "Textport": hou.paneTabType.Textport,
+ "Tree View": hou.paneTabType.TreeView,
+ }
+ list_dict = {name: (lambda t=pt: pane.newTab(t)) for name, pt in type_map.items()}
+ from .hcwidgets import HCWidgets
+ dialog = HCWidgets.SelectionDialog("New Pane Tab", list_dict)
+ dialog.show()
+ dialog.raise_()
+ dialog.activateWindow()
+
def toggleAllTabs(self):
visible = 0
panes = self.allPanes()
diff --git a/python3.11libs/hc/hcsettings.py b/python3.11libs/hc/hcsettings.py
new file mode 100644
index 0000000..c1fa9af
--- /dev/null
+++ b/python3.11libs/hc/hcsettings.py
@@ -0,0 +1,266 @@
+import hou
+import json
+from pathlib import Path
+
+from PySide6.QtCore import QFileSystemWatcher, Qt
+from PySide6.QtWidgets import (
+ QCheckBox,
+ QComboBox,
+ QDialog,
+ QDialogButtonBox,
+ QDoubleSpinBox,
+ QFormLayout,
+ QGroupBox,
+ QLabel,
+ QLineEdit,
+ QScrollArea,
+ QSpinBox,
+ QVBoxLayout,
+ QWidget,
+)
+
+
+DESKTOP_MODES = ("attached", "detached")
+NODE_SHAPES = ("rect", "rounded_rect", "circle", "diamond", "tilted_rect", "trapezoid_down", "trapezoid_up")
+
+
+class HCSettings:
+ DEFAULTS = {
+ "desktop_mode": "attached",
+ "node_graph": {
+ "node_shape": "rect",
+ "node_color": "#607070",
+ },
+ }
+
+ def __init__(self):
+ self.node_color = (0.38, 0.38, 0.56)
+
+ def _path(self):
+ base = hou.getenv("HC_PATH")
+ if not base:
+ return None
+ return Path(base) / "hc_settings.json"
+
+ def prefs(self):
+ path = self._path()
+ if path is None or not path.exists():
+ return dict(self.DEFAULTS)
+ try:
+ return json.loads(path.read_text())
+ except Exception:
+ return dict(self.DEFAULTS)
+
+ def get(self, key, default=None):
+ return self.prefs().get(key, default)
+
+ def set(self, key, value):
+ path = self._path()
+ if path is None:
+ return False
+ data = self.prefs()
+ data[key] = value
+ path.write_text(json.dumps(data, indent=4) + "\n")
+ return True
+
+ def write(self, data):
+ path = self._path()
+ if path is None:
+ return False
+ path.write_text(json.dumps(data, indent=4) + "\n")
+ return True
+
+ def desktopMode(self):
+ return self.get("desktop_mode", self.DEFAULTS["desktop_mode"])
+
+ def setDesktopMode(self, mode):
+ if mode not in DESKTOP_MODES:
+ raise ValueError(f"invalid desktop mode: {mode}")
+ self.set("desktop_mode", mode)
+
+
+class HCSettingsPanel(QDialog):
+ OBJECT_NAME = "hc_settings_panel"
+ BOOL_HINT_KEYS = {
+ "bbox_enabled", "cam_axis_enabled", "cam_geo_enabled", "perim_enabled",
+ "pivot_2d_enabled", "pivot_3d_enabled", "pivot_axis_enabled", "ray_enabled",
+ "tie_axis_to_radius_enabled", "center_on_geo", "lock_cam", "reset",
+ }
+
+ def __init__(self):
+ super().__init__(hou.qt.mainWindow())
+ self.setObjectName(self.OBJECT_NAME)
+ self.setWindowTitle("HC Settings")
+ self.setWindowFlags(Qt.Tool)
+ self.resize(720, 960)
+ self.settings = HCSettings()
+
+ # path_tuple (e.g. ("keycam","guides","axis_size")) -> (widget, original_value)
+ self._fields = {}
+
+ self._content = QWidget()
+ self._content_layout = QVBoxLayout(self._content)
+
+ scroll = QScrollArea()
+ scroll.setWidgetResizable(True)
+ scroll.setWidget(self._content)
+
+ buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
+ buttons.accepted.connect(self._save)
+ buttons.rejected.connect(self.close)
+
+ layout = QVBoxLayout(self)
+ layout.addWidget(scroll)
+ layout.addWidget(QLabel("Some settings (e.g. Desktop Mode) take effect on next start."))
+ layout.addWidget(buttons)
+
+ self._watcher = QFileSystemWatcher(self)
+ self._watcher.fileChanged.connect(self._onFileChanged)
+ self._startWatching()
+ self._rebuild()
+
+ def showEvent(self, event):
+ self._rebuild()
+ self._startWatching()
+ super().showEvent(event)
+
+ def _startWatching(self):
+ path = self.settings._path()
+ if path is None:
+ return
+ if str(path) not in self._watcher.files():
+ self._watcher.addPath(str(path))
+
+ def _onFileChanged(self, path):
+ # Some editors replace the file on save, which drops the watch.
+ self._startWatching()
+ self._rebuild()
+
+
+ """ Form builder """
+
+
+ def _rebuild(self):
+ # Clear existing fields and rebuild from current file state
+ self._fields.clear()
+ while self._content_layout.count():
+ item = self._content_layout.takeAt(0)
+ w = item.widget()
+ if w is not None:
+ w.setParent(None)
+ w.deleteLater()
+ data = self.settings.prefs()
+ self._addSection(data, self._content_layout, ())
+ self._content_layout.addStretch()
+
+ def _addSection(self, data, parent_layout, path):
+ scalars = {k: v for k, v in data.items() if not isinstance(v, dict)}
+ dicts = {k: v for k, v in data.items() if isinstance(v, dict)}
+
+ if scalars:
+ if not path:
+ # Root-level scalars go in a "General" group box so they align
+ # visually with the other top-level groups.
+ group = QGroupBox("General")
+ gl = QVBoxLayout(group)
+ form = QFormLayout()
+ gl.addLayout(form)
+ parent_layout.addWidget(group)
+ else:
+ form = QFormLayout()
+ parent_layout.addLayout(form)
+ for key, value in scalars.items():
+ sub_path = path + (key,)
+ widget = self._makeWidget(key, value)
+ self._fields[sub_path] = widget
+ form.addRow(self._prettify(key) + ":", widget)
+
+ for key, value in dicts.items():
+ sub_path = path + (key,)
+ group = QGroupBox(self._prettify(key))
+ gl = QVBoxLayout(group)
+ self._addSection(value, gl, sub_path)
+ parent_layout.addWidget(group)
+
+ def _prettify(self, key):
+ return key.replace("_", " ").title()
+
+ def _makeWidget(self, key, value):
+ if isinstance(value, bool):
+ w = QCheckBox()
+ w.setChecked(value)
+ return w
+ if isinstance(value, int) and (key in self.BOOL_HINT_KEYS or value in (0, 1)):
+ w = QCheckBox()
+ w.setChecked(bool(value))
+ return w
+ if isinstance(value, float):
+ w = QDoubleSpinBox()
+ w.setDecimals(4)
+ w.setRange(-1e9, 1e9)
+ w.setSingleStep(0.1)
+ w.setValue(value)
+ return w
+ if isinstance(value, int):
+ w = QSpinBox()
+ w.setRange(-1000000, 1000000)
+ w.setValue(value)
+ return w
+ if isinstance(value, str):
+ if key == "desktop_mode":
+ w = QComboBox()
+ w.addItems(DESKTOP_MODES)
+ w.setCurrentText(value)
+ return w
+ if key == "node_shape":
+ w = QComboBox()
+ w.addItems(NODE_SHAPES)
+ if value not in NODE_SHAPES:
+ w.addItem(value)
+ w.setCurrentText(value)
+ return w
+ w = QLineEdit(value)
+ return w
+ return QLabel(repr(value))
+
+
+ """ IO """
+
+
+ def _readWidget(self, widget, original_value):
+ if isinstance(widget, QCheckBox):
+ return int(widget.isChecked()) if isinstance(original_value, int) and not isinstance(original_value, bool) else widget.isChecked()
+ if isinstance(widget, QDoubleSpinBox):
+ return widget.value()
+ if isinstance(widget, QSpinBox):
+ return widget.value()
+ if isinstance(widget, QComboBox):
+ return widget.currentText()
+ if isinstance(widget, QLineEdit):
+ return widget.text()
+ return original_value
+
+ def _save(self):
+ data = self.settings.prefs()
+ for path, widget in self._fields.items():
+ original = self._lookup(data, path)
+ new_value = self._readWidget(widget, original)
+ self._assign(data, path, new_value)
+ self.settings.write(data)
+
+ def _lookup(self, data, path):
+ node = data
+ for key in path:
+ if isinstance(node, dict) and key in node:
+ node = node[key]
+ else:
+ return None
+ return node
+
+ def _assign(self, data, path, value):
+ node = data
+ for key in path[:-1]:
+ if key not in node or not isinstance(node[key], dict):
+ node[key] = {}
+ node = node[key]
+ node[path[-1]] = value
diff --git a/python3.11libs/hc/hcsplithandles.py b/python3.11libs/hc/hcsplithandles.py
new file mode 100644
index 0000000..7b30806
--- /dev/null
+++ b/python3.11libs/hc/hcsplithandles.py
@@ -0,0 +1,212 @@
+import hou
+from PySide6.QtCore import QEvent, QObject, QPoint, Qt, QTimer
+from PySide6.QtWidgets import QWidget
+
+
+HCSPLITHANDLE_THICKNESS = 20
+HCSPLITHANDLE_LENGTH = 80
+HCSPLITHANDLE_MIN_FRACTION = 0.05
+HCSPLITHANDLE_MAX_FRACTION = 0.95
+
+
+class _SplitHandle(QWidget):
+ REST_STYLE = f"background: #3a3a3a; border-radius: {HCSPLITHANDLE_THICKNESS // 2}px;"
+ HOVER_STYLE = f"background: #90EE90; border-radius: {HCSPLITHANDLE_THICKNESS // 2}px;"
+
+ def __init__(self, parent, hou_pane, horizontal_boundary, manager):
+ super().__init__(parent)
+ self.hou_pane = hou_pane
+ self.horizontal_boundary = horizontal_boundary
+ self.manager = manager
+ self.setObjectName(f"hc_split_handle_{hou_pane.id()}")
+ self.setCursor(Qt.SplitVCursor if horizontal_boundary else Qt.SplitHCursor)
+ self.setAttribute(Qt.WA_StyledBackground, True)
+ self.setStyleSheet(self.REST_STYLE)
+ self._origin = None
+ self._start_fraction = None
+ self._parent_extent = None
+ self._drag_child = None
+ self._pending_fraction = None
+ self._throttle = QTimer(self)
+ self._throttle.setSingleShot(True)
+ self._throttle.setInterval(33)
+ self._throttle.timeout.connect(self._applyPending)
+
+ def event(self, event):
+ et = event.type()
+ if et == QEvent.Enter:
+ self.setStyleSheet(self.HOVER_STYLE)
+ self.repaint()
+ elif et == QEvent.Leave:
+ self.setStyleSheet(self.REST_STYLE)
+ self.repaint()
+ return super().event(event)
+
+ def mousePressEvent(self, event):
+ if event.button() != Qt.LeftButton:
+ return
+ self._origin = event.globalPosition().toPoint()
+ self._drag_child = self.hou_pane.getSplitChild(0)
+ self._start_fraction = self._drag_child.getSplitFraction()
+ geom = self.hou_pane.qtScreenGeometry()
+ self._parent_extent = geom.height() if self.horizontal_boundary else geom.width()
+ event.accept()
+
+ def mouseMoveEvent(self, event):
+ if self._origin is None or not self._parent_extent:
+ return
+ delta = event.globalPosition().toPoint() - self._origin
+ pixel_delta = -delta.y() if self.horizontal_boundary else delta.x()
+ fraction_delta = pixel_delta / self._parent_extent
+ new_fraction = max(
+ HCSPLITHANDLE_MIN_FRACTION,
+ min(HCSPLITHANDLE_MAX_FRACTION, self._start_fraction + fraction_delta),
+ )
+ self._pending_fraction = new_fraction
+ if not self._throttle.isActive():
+ self._throttle.start()
+ event.accept()
+
+ def mouseReleaseEvent(self, event):
+ if self._pending_fraction is not None:
+ self._applyPending()
+ self._origin = None
+ self._start_fraction = None
+ self._parent_extent = None
+ self._drag_child = None
+ self._pending_fraction = None
+ self._throttle.stop()
+ self.manager._pin_all()
+
+ def _applyPending(self):
+ if self._pending_fraction is None or self._drag_child is None:
+ return
+ self._drag_child.setSplitFraction(self._pending_fraction)
+ self._pending_fraction = None
+ self.manager._pin(self)
+
+
+class _ResizeFilter(QObject):
+ def __init__(self, manager):
+ super().__init__(manager.main)
+ self.manager = manager
+
+ def eventFilter(self, obj, event):
+ if event.type() == QEvent.Resize:
+ self.manager._pin_all()
+ return False
+
+
+class HCSplitHandles:
+ def __init__(self):
+ self.main = hou.qt.mainWindow()
+ self.handles = []
+ self._filter = None
+
+ def show(self):
+ if self.handles:
+ return
+ for split in self._collectSplits():
+ orientation = self._orientation(split)
+ if orientation is None:
+ continue
+ handle = _SplitHandle(
+ self.main,
+ split,
+ horizontal_boundary=(orientation == "horizontal"),
+ manager=self,
+ )
+ handle.show()
+ handle.raise_()
+ self.handles.append(handle)
+ self._filter = _ResizeFilter(self)
+ self.main.installEventFilter(self._filter)
+ self._pin_all()
+
+ def hide(self):
+ for handle in self.handles:
+ handle.setParent(None)
+ handle.deleteLater()
+ self.handles = []
+ if self._filter is not None:
+ self.main.removeEventFilter(self._filter)
+ self._filter = None
+
+ def refresh(self):
+ self.hide()
+ self.show()
+
+ def isShowing(self):
+ return bool(self.handles)
+
+ def toggle(self):
+ if self.isShowing():
+ self.hide()
+ else:
+ self.show()
+
+ def _collectSplits(self):
+ splits = {}
+ for leaf in hou.ui.curDesktop().panes():
+ parent = leaf.getSplitParent()
+ while parent is not None:
+ splits[parent.id()] = parent
+ parent = parent.getSplitParent()
+ return splits.values()
+
+ def _orientation(self, split):
+ try:
+ c0 = split.getSplitChild(0)
+ c1 = split.getSplitChild(1)
+ except Exception:
+ return None
+ g0 = c0.qtScreenGeometry()
+ g1 = c1.qtScreenGeometry()
+ if g0.width() <= 0 or g0.height() <= 0 or g1.width() <= 0 or g1.height() <= 0:
+ return None
+ if abs(g0.y() - g1.y()) < 5 and g0.width() > 0:
+ return "vertical"
+ if abs(g0.x() - g1.x()) < 5 and g0.height() > 0:
+ return "horizontal"
+ return None
+
+ def _pin_all(self):
+ for handle in self.handles:
+ self._pin(handle)
+
+ def _pin(self, handle):
+ split = handle.hou_pane
+ try:
+ c0 = split.getSplitChild(0)
+ c1 = split.getSplitChild(1)
+ except Exception:
+ handle.hide()
+ return
+ g0 = c0.qtScreenGeometry()
+ g1 = c1.qtScreenGeometry()
+ if g0.width() <= 0 or g1.width() <= 0:
+ handle.hide()
+ return
+ split_geom = split.qtScreenGeometry()
+ if handle.horizontal_boundary:
+ y_screen = (g0.y() + g0.height() + g1.y()) // 2
+ x_screen = split_geom.x() + split_geom.width() // 2
+ center = self.main.mapFromGlobal(QPoint(x_screen, y_screen))
+ handle.setGeometry(
+ center.x() - HCSPLITHANDLE_LENGTH // 2,
+ center.y() - HCSPLITHANDLE_THICKNESS // 2,
+ HCSPLITHANDLE_LENGTH,
+ HCSPLITHANDLE_THICKNESS,
+ )
+ else:
+ x_screen = (g0.x() + g0.width() + g1.x()) // 2
+ y_screen = split_geom.y() + split_geom.height() // 2
+ center = self.main.mapFromGlobal(QPoint(x_screen, y_screen))
+ handle.setGeometry(
+ center.x() - HCSPLITHANDLE_THICKNESS // 2,
+ center.y() - HCSPLITHANDLE_LENGTH // 2,
+ HCSPLITHANDLE_THICKNESS,
+ HCSPLITHANDLE_LENGTH,
+ )
+ handle.show()
+ handle.raise_()
diff --git a/python3.11libs/hc/hcstatus.py b/python3.11libs/hc/hcstatus.py
index 8791dcb..c16a47f 100644
--- a/python3.11libs/hc/hcstatus.py
+++ b/python3.11libs/hc/hcstatus.py
@@ -1,69 +1,94 @@
import hou
-from PySide6 import QtWidgets
-from PySide6.QtWidgets import QLabel, QVBoxLayout
-from PySide6.QtCore import Qt
+from PySide6.QtCore import Qt, QTimer
+from PySide6.QtGui import QCursor
+from PySide6.QtWidgets import QDialog, QLabel, QVBoxLayout
-class HCStatus(QtWidgets.QDialog):
+class HCStatus(QDialog):
+ OBJECT_NAME = "hc_status_dialog"
+
def __init__(self):
- super(Dialog, self).__init__(hou.qt.mainWindow())
- self.resize(500, 200)
+ super().__init__(hou.qt.mainWindow())
+ self.setObjectName(self.OBJECT_NAME)
self.setWindowTitle("HC Status")
self.setWindowFlags(Qt.Tool | Qt.WindowStaysOnTopHint)
+ self.resize(440, 240)
+
+ self.desktop_label = QLabel()
+ self.pane_label = QLabel()
+ self.tab_type_label = QLabel()
+ self.tab_name_label = QLabel()
+ self.context_label = QLabel()
+ self.path_label = QLabel()
+ self.cursor_screen_label = QLabel()
+ self.cursor_main_label = QLabel()
+ self.hip_label = QLabel()
- # Widgets
- self.project_path_label = QLabel("Project Path")
- self.network_path_label = QLabel("Network Path")
- self.tab_type_label = QLabel("Tab Type")
+ layout = QVBoxLayout(self)
+ for w in (
+ self.desktop_label,
+ self.hip_label,
+ self.pane_label,
+ self.tab_type_label,
+ self.tab_name_label,
+ self.context_label,
+ self.path_label,
+ self.cursor_screen_label,
+ self.cursor_main_label,
+ ):
+ layout.addWidget(w)
+ layout.addStretch()
- # Layout
- self.layout = QVBoxLayout()
- self.layout.addWidget(self.project_path_label)
- self.layout.addWidget(self.network_path_label)
- self.layout.addWidget(self.tab_type_label)
- self.layout.addStretch()
- self.setLayout(self.layout)
+ self.timer = QTimer(self)
+ self.timer.timeout.connect(self.refresh)
+ self.refresh()
- self.update()
+ def showEvent(self, event):
+ self.timer.start(100)
+ self.refresh()
+ super().showEvent(event)
- def closeEvent(self, event):
- self.setParent(None)
+ def hideEvent(self, event):
+ self.timer.stop()
+ super().hideEvent(event)
- def lists(self):
- # Arrays for navigating pane tabs
- self.tab_types = (
- hou.paneTabType.ApexEditor,
- hou.paneTabType.CompositorViewer,
- hou.paneTabType.DetailsView,
- hou.paneTabType.NetworkEditor,
- hou.paneTabType.Parm,
- hou.paneTabType.PythonPanel,
- hou.paneTabType.PythonShell,
- hou.paneTabType.SceneViewer,
- hou.paneTabType.Textport
- )
- self.tab_type_names = (
- "ApexEditor",
- "CompositorViewer",
- "DetailsView",
- "NetworkEditor",
- "Parm",
- "PythonPanel",
- "PythonShell",
- "SceneViewer",
- "Textport"
- )
- self.tab_names = [tab.name() for tab in self.session.allTabs()]
+ def refresh(self):
+ self.desktop_label.setText(f"Desktop: {hou.ui.curDesktop().name()}")
+ self.hip_label.setText(f"Hip: {hou.hipFile.path()}")
- # Populate pane tab labels array
- self.tab_labels = []
- for tab in self.session.allTabs():
- index = self.tab_types.index(tab.type())
- label = self.tab_type_names[index]
- self.tab_labels.append(label)
+ pane = hou.ui.paneUnderCursor()
+ if pane is None:
+ self.pane_label.setText("Pane: —")
+ self.tab_type_label.setText("Tab Type: —")
+ self.tab_name_label.setText("Tab Name: —")
+ self.context_label.setText("Context: —")
+ self.path_label.setText("Path: —")
+ else:
+ self.pane_label.setText(f"Pane: #{pane.id()}")
+ tab = pane.currentTab()
+ if tab is None:
+ self.tab_type_label.setText("Tab Type: —")
+ self.tab_name_label.setText("Tab Name: —")
+ self.context_label.setText("Context: —")
+ self.path_label.setText("Path: —")
+ else:
+ tab_type = tab.type()
+ self.tab_type_label.setText(f"Tab Type: {tab_type.name()}")
+ self.tab_name_label.setText(f"Tab Name: {tab.name()}")
+ pwd = getattr(tab, "pwd", None)
+ if callable(pwd):
+ try:
+ node = pwd()
+ self.path_label.setText(f"Path: {node.path()}")
+ self.context_label.setText(f"Context: {node.childTypeCategory().name()}")
+ except Exception as e:
+ self.path_label.setText(f"Path: {e!r}")
+ self.context_label.setText("Context: —")
+ else:
+ self.path_label.setText("Path: —")
+ self.context_label.setText("Context: —")
- def update(self):
- # Labels
- self.project_path_label.setText("Project path: " + hou.session.projectPath)
- self.network_path_label.setText("Network Path: " + hou.session.networkPath)
- self.tab_type_label.setText("Tab type: " + str(hou.session.tabType))
+ screen = QCursor.pos()
+ self.cursor_screen_label.setText(f"Cursor (screen): ({screen.x()}, {screen.y()})")
+ rel = hou.qt.mainWindow().mapFromGlobal(screen)
+ self.cursor_main_label.setText(f"Cursor (main): ({rel.x()}, {rel.y()})")
diff --git a/python3.11libs/hc/hcstatusbar.py b/python3.11libs/hc/hcstatusbar.py
new file mode 100644
index 0000000..7824f99
--- /dev/null
+++ b/python3.11libs/hc/hcstatusbar.py
@@ -0,0 +1,202 @@
+import hou
+from PySide6.QtCore import QEvent, QObject, Qt, QTimer
+from PySide6.QtWidgets import QFrame, QHBoxLayout, QLabel, QPushButton, QWidget
+
+
+HCSTATUSBAR_HEIGHT = 48
+HCSTATUSBAR_WIDTH = 500
+HCSTATUSBAR_MARGIN = 4
+HCSTATUSBAR_RIGHT_MARGIN = 360
+HCSTATUSBAR_STYLE = """
+ QFrame#hc_status_overlay {
+ background: #2a2a2a;
+ border: 1px solid #444;
+ }
+ QFrame#hc_status_overlay QLabel {
+ color: #ccc;
+ background: transparent;
+ padding: 0 6px;
+ }
+ QFrame#hc_status_overlay QPushButton {
+ color: #eee;
+ background: #3a3a3a;
+ border: 1px solid #555;
+ padding: 0 10px;
+ }
+ QFrame#hc_status_overlay QPushButton:hover {
+ background: #4a4a4a;
+ }
+"""
+
+
+class _ResizeFilter(QObject):
+ def __init__(self, manager):
+ super().__init__(manager.overlay)
+ self.manager = manager
+
+ def eventFilter(self, obj, event):
+ if event.type() == QEvent.Resize:
+ self.manager._pin()
+ return False
+
+
+class HCStatusBar:
+ OVERLAY_NAME = "hc_status_overlay"
+ TAB_TYPE_LABEL = "hc_status_tab_type"
+ AUTOSAVE_LABEL = "hc_status_autosave"
+
+ def __init__(self):
+ self.main = hou.qt.mainWindow()
+ self.overlay = self._findOverlay()
+
+ def _findOverlay(self):
+ return self.main.findChild(QFrame, self.OVERLAY_NAME)
+
+ def _ensureOverlay(self):
+ if self.overlay is not None and self.overlay.parent() is self.main:
+ return self.overlay
+ frame = QFrame(self.main)
+ frame.setObjectName(self.OVERLAY_NAME)
+ frame.setStyleSheet(HCSTATUSBAR_STYLE)
+ layout = QHBoxLayout(frame)
+ layout.setContentsMargins(2, 0, 2, 0)
+ layout.setSpacing(4)
+ layout.addStretch()
+ frame.setLayout(layout)
+ filt = _ResizeFilter(self)
+ self.main.installEventFilter(filt)
+ frame._hc_filter = filt # keep reference alive
+ self.overlay = frame
+ self._pin()
+ frame.show()
+ frame.raise_()
+ self._installLabels()
+ return frame
+
+ def _installLabels(self):
+ frame = self.overlay
+ layout = frame.layout()
+
+ tab_lbl = QLabel("—", frame)
+ tab_lbl.setObjectName(self.TAB_TYPE_LABEL)
+ layout.insertWidget(0, tab_lbl)
+
+ autosave_lbl = QLabel("—", frame)
+ autosave_lbl.setObjectName(self.AUTOSAVE_LABEL)
+ layout.insertWidget(1, autosave_lbl)
+
+ timer = QTimer(frame)
+ timer.timeout.connect(self._refreshTabType)
+ timer.start(250)
+ frame._hc_tab_type_timer = timer
+
+ self._refreshTabType()
+ self.updateAutosave()
+
+ def _refreshTabType(self):
+ if self.overlay is None:
+ return
+ lbl = self.overlay.findChild(QLabel, self.TAB_TYPE_LABEL)
+ if lbl is None:
+ return
+ from .hcsession import HCSession
+ tab = HCSession().currentTab()
+ if tab is None:
+ lbl.setText("—")
+ return
+ lbl.setText(tab.hou_tab.type().name())
+
+ def updateAutosave(self):
+ if self.overlay is None:
+ return
+ lbl = self.overlay.findChild(QLabel, self.AUTOSAVE_LABEL)
+ if lbl is None:
+ return
+ autosave = hou.getPreference("autoSave")
+ lbl.setText("Autosave: On" if autosave == "1" else "Autosave: Off")
+
+ def _pin(self):
+ if self.overlay is None:
+ return
+ x = self.main.width() - HCSTATUSBAR_WIDTH - HCSTATUSBAR_RIGHT_MARGIN
+ y = self.main.height() - HCSTATUSBAR_HEIGHT - HCSTATUSBAR_MARGIN
+ self.overlay.setGeometry(x, y, HCSTATUSBAR_WIDTH, HCSTATUSBAR_HEIGHT)
+ self.overlay.raise_()
+
+
+ """ Show / Hide """
+
+
+ def show(self):
+ self._ensureOverlay()
+
+ def hide(self):
+ if self.overlay is not None:
+ self.overlay.hide()
+
+ def isVisible(self):
+ return self.overlay is not None and self.overlay.isVisible()
+
+
+ """ Add """
+
+
+ def addButton(self, name, label, callback):
+ frame = self._ensureOverlay()
+ self.removeWidget(name)
+ btn = QPushButton(label, frame)
+ btn.setObjectName(name)
+ btn.clicked.connect(callback)
+ frame.layout().addWidget(btn)
+ frame.raise_()
+ return btn
+
+ def addLabel(self, name, text):
+ frame = self._ensureOverlay()
+ self.removeWidget(name)
+ lbl = QLabel(text, frame)
+ lbl.setObjectName(name)
+ frame.layout().addWidget(lbl)
+ frame.raise_()
+ return lbl
+
+ def addWidget(self, name, widget):
+ frame = self._ensureOverlay()
+ self.removeWidget(name)
+ widget.setObjectName(name)
+ widget.setParent(frame)
+ frame.layout().addWidget(widget)
+ frame.raise_()
+ return widget
+
+
+ """ Remove """
+
+
+ def removeWidget(self, name):
+ if self.overlay is None:
+ return
+ existing = self.overlay.findChild(QWidget, name)
+ if existing is not None:
+ self.overlay.layout().removeWidget(existing)
+ existing.setParent(None)
+ existing.deleteLater()
+
+ def clear(self):
+ if self.overlay is None:
+ return
+ self.overlay.setParent(None)
+ self.overlay.deleteLater()
+ self.overlay = None
+
+
+ """ Query """
+
+
+ def exists(self):
+ return self._findOverlay() is not None
+
+ def widget(self, name):
+ if self.overlay is None:
+ return None
+ return self.overlay.findChild(QWidget, name)
diff --git a/python3.11libs/hc/hctab.py b/python3.11libs/hc/hctab.py
index 42971b9..f469771 100644
--- a/python3.11libs/hc/hctab.py
+++ b/python3.11libs/hc/hctab.py
@@ -5,6 +5,12 @@ class HCTab():
def __init__(self, hou_tab):
self.hou_tab = hou_tab
+ def initialize(self):
+ """Per-pane setup hook. Override in subclasses to apply pane-type
+ defaults, register hooks, set viewport prefs, etc. Called by factory
+ callers (e.g. HCLayout.launchFloating) after a pane tab is created."""
+ return
+
""" Status """
def close(self):
diff --git a/python3.11libs/hc/hcwindowwatcher.py b/python3.11libs/hc/hcwindowwatcher.py
new file mode 100644
index 0000000..34b42d4
--- /dev/null
+++ b/python3.11libs/hc/hcwindowwatcher.py
@@ -0,0 +1,98 @@
+import hou
+from PySide6.QtCore import QEvent, QObject
+
+
+class HCWindowWatcher(QObject):
+ EVENT_MAP = {
+ QEvent.Enter: "enter",
+ QEvent.Leave: "leave",
+ QEvent.WindowActivate: "focus",
+ QEvent.WindowDeactivate: "unfocus",
+ }
+
+ def __init__(self):
+ super().__init__(hou.qt.mainWindow())
+ self._installed = False
+ self._watched = []
+ self._in_window = False
+ self._counts = {name: 0 for name in self.EVENT_MAP.values()}
+ self._callbacks = {name: [] for name in self.EVENT_MAP.values()}
+
+ def _targets(self):
+ from PySide6.QtWidgets import QWidget
+ main = hou.qt.mainWindow()
+ targets = [main]
+ gl = main.findChild(QWidget, "RE_GLDrawableWrapper")
+ if gl is not None:
+ targets.append(gl)
+ return targets
+
+ def start(self):
+ if self._installed:
+ return
+ self._watched = self._targets()
+ for t in self._watched:
+ t.installEventFilter(self)
+ self._installed = True
+
+ def stop(self):
+ if not self._installed:
+ return
+ for t in self._watched:
+ t.removeEventFilter(self)
+ self._watched = []
+ self._installed = False
+
+ def toggle(self):
+ if self._installed:
+ self.stop()
+ else:
+ self.start()
+
+ def isRunning(self):
+ return self._installed
+
+ def counts(self):
+ return dict(self._counts)
+
+ def on(self, name, callback):
+ if name not in self._callbacks:
+ raise ValueError(f"unknown event: {name}")
+ if callback not in self._callbacks[name]:
+ self._callbacks[name].append(callback)
+
+ def off(self, name, callback):
+ if name in self._callbacks and callback in self._callbacks[name]:
+ self._callbacks[name].remove(callback)
+
+ def _fire(self, name):
+ for cb in self._callbacks[name]:
+ try:
+ cb()
+ except Exception as e:
+ print(f"[HCWindowWatcher] {name} callback error: {e!r}")
+
+ def _cursorInsideMain(self):
+ main = hou.qt.mainWindow()
+ pos = main.mapFromGlobal(main.cursor().pos())
+ return main.rect().contains(pos)
+
+ def eventFilter(self, obj, event):
+ et = event.type()
+ if et == QEvent.Enter:
+ if not self._in_window:
+ self._in_window = True
+ self._counts["enter"] += 1
+ self._fire("enter")
+ elif et == QEvent.Leave:
+ if self._in_window and not self._cursorInsideMain():
+ self._in_window = False
+ self._counts["leave"] += 1
+ self._fire("leave")
+ elif et == QEvent.WindowActivate:
+ self._counts["focus"] += 1
+ self._fire("focus")
+ elif et == QEvent.WindowDeactivate:
+ self._counts["unfocus"] += 1
+ self._fire("unfocus")
+ return False
diff --git a/python3.11libs/nodegraphhooks.py b/python3.11libs/nodegraphhooks.py
index 06fb81d..3390fcf 100755
--- a/python3.11libs/nodegraphhooks.py
+++ b/python3.11libs/nodegraphhooks.py
@@ -1,62 +1,17 @@
-import hou
-from canvaseventtypes import *
-import nodegraphdisplay as display
-from hc import HCSession, HCNetworkEditor
-# import nodegraphview as view
-
-
-def createEventHandler(uievent, pending_actions):
-
- if isinstance(uievent, ContextEvent):
- # network_editor = HCNetworkEditor(uievent.editor)
- # network_editor.showPathMessage()
- # return None, True
- return None, False
-
- elif isinstance(uievent, MouseEvent):
- return None, False
-
- elif isinstance(uievent, KeyboardEvent) and \
- uievent.eventtype == 'keyhit':
- network_editor = HCNetworkEditor(uievent.editor)
- session = HCSession()
- keymap = {
- # Zoom
- '=': lambda: network_editor.zoom('in'),
- '-': lambda: network_editor.zoom('out'),
- # Move view
- 'H': lambda: network_editor.translateView('left'),
- 'J': lambda: network_editor.translateView('down'),
- 'K': lambda: network_editor.translateView('up'),
- 'L': lambda: network_editor.translateView('right'),
- # Add to selection
- 'Ctrl+Shift+H': lambda: network_editor.addToSelection('left'),
- 'Ctrl+Shift+J': lambda: network_editor.addToSelection('down'),
- 'Ctrl+Shift+K': lambda: network_editor.addToSelection('up'),
- 'Ctrl+Shift+L': lambda: network_editor.addToSelection('right'),
- # Move node
- 'Alt+H': lambda: network_editor.translateSelectedNodes('left'),
- 'Alt+J': lambda: network_editor.translateSelectedNodes('down'),
- 'Alt+K': lambda: network_editor.translateSelectedNodes('up'),
- 'Alt+L': lambda: network_editor.translateSelectedNodes('right'),
- # Flags
- 'Q': network_editor.toggleBypassFlag,
- 'W': network_editor.toggleTemplateFlag,
- 'E': network_editor.setDisplayFlag,
- # Organize
- # Grid
- 'Shift+G': network_editor.toggleGridMode,
- # Selection
- 'Ctrl+A': network_editor.selectAllNodes,
- 'Ctrl+D': network_editor.deselectAllNodes,
- }
-
- key = uievent.key
- if key in keymap:
- func = keymap[key]()
- return None, True
- else:
- return None, False
-
- else:
- return None, False
+import hou
+from canvaseventtypes import *
+import nodegraphdisplay as display
+from hc import HCNetworkEditor
+
+
+def createEventHandler(uievent, pending_actions):
+
+ if isinstance(uievent, MouseEvent):
+ if uievent.modifierstate.ctrl and uievent.wheelvalue:
+ network_editor = HCNetworkEditor(uievent.editor)
+ pivot = uievent.editor.posFromScreen(uievent.mousepos)
+ network_editor.zoom('in' if uievent.wheelvalue > 0 else 'out', amount=0.03, pivot=pivot)
+ return None, True
+ return None, False
+
+ return None, False
diff --git a/python3.11libs/uiready.py b/python3.11libs/uiready.py
index f55b2fa..932ce90 100644
--- a/python3.11libs/uiready.py
+++ b/python3.11libs/uiready.py
@@ -1,21 +1,36 @@
import hou
-from hc import HCSession
-from PySide6.QtCore import Qt
+import hrpyc
+from hc import HCSession, HCSettings
hc_session = HCSession()
hc_session.reloadHotkeys()
-hc_session.toggleStowbars()
-## Set network grid points to on
-# networkeditors = desktop.getNetworkEditors()
-# for networkeditor in networkeditors:
- # networkeditor.setPref("gridmode", "1")
+def set_current_desktop(name):
+ for d in hou.ui.desktops():
+ if d.name() == name:
+ d.setAsCurrent()
+ return
+desktop_mode = HCSettings().desktopMode()
-## Qt
-# if qt_commands:
-# window = hou.qt.mainWindow()
-# window.setWindowFlags(Qt.FramelessWindowHint)
-# window.setFixedWidth(1710)
-# window.setFixedHeight(1100)
+if desktop_mode == "detached":
+ set_current_desktop("hc_detached")
+ hc_session.toggleStowbars()
+ hc_session.hideShelf()
+ hc_session.showMainMenu(0)
+ from hc import HCStatusBar
+ HCStatusBar().show()
+ from hc.hclayout import launchFloatingLayout
+ launchFloatingLayout()
+else:
+ set_current_desktop("hc_attached")
+ hc_session.toggleStowbars()
+ hc_session.splitHandles().show()
+ from hc import HCStatusBar
+ HCStatusBar().show()
+
+try:
+ hrpyc.start_server(port=18811)
+except OSError:
+ pass
diff --git a/scripts/OnCreated.py b/scripts/OnCreated.py
index e67c799..2d622e4 100644
--- a/scripts/OnCreated.py
+++ b/scripts/OnCreated.py
@@ -1,10 +1,10 @@
import hou
-from hc import HCPrefs
+from hc import HCSettings
from hc import HCNode
# import from hc.HCNetworkEditor import snapToGrid
node = HCNode(kwargs['node'])
-prefs = HCPrefs().prefs()
+prefs = HCSettings().prefs()
node.setUserData("nodeshape", "rect")
diff --git a/scripts/hotkeys_clear.json b/scripts/hotkeys_clear.json
deleted file mode 100644
index 57ecada..0000000
--- a/scripts/hotkeys_clear.json
+++ /dev/null
@@ -1,152 +0,0 @@
-{
- "commands": [
- "deskmgr.new",
- "deskmgr.save",
- "h.add_key",
- "h.aliases",
- "h.audio_panel",
- "h.auto_key",
- "h.cache_manager",
- "h.channeleditor",
- "h.clear_cop_caches",
- "h.comp_project_manager",
- "h.context_help",
- "h.desktop_mgr",
- "h.find",
- "h.global_animation_options",
- "h.mat_palette",
- "h.merge",
- "h.new",
- "h.next_key",
- "h.open",
- "h.perf_monitor",
- "h.python_shell",
- "h.textport",
- "h.pane.clone",
- "h.pane.closetab",
- "h.pane.copytab",
- "h.pane.edit_bookmark",
- "h.pane.editpathastext",
- "h.pane.link_minus",
- "h.pane.link_plus",
- "h.pane.nexttab",
- "h.pane.prevtab",
- "h.pane.to_cop_view",
- "h.pane.to_geosheet",
- "h.pane.to_ipr_viewer",
- "h.pane.to_mat_palette",
- "h.pane.to_net",
- "h.pane.to_old_net",
- "h.pane.to_parm",
- "h.pane.to_scene_view",
- "h.pane.to_textport",
- "h.pane.to_treeview",
- "h.pane.bundle.appbundle",
- "h.pane.bundle.duplicate",
- "h.pane.chedit.bsave",
- "h.pane.chedit.load",
- "h.pane.chedit.save",
- "h.pane.chedit.scroll_to_current_frame",
- "h.pane.chedit.show_groups",
- "h.pane.chedit.show_layers",
- "h.pane.chedit.show_parmbox",
- "h.pane.chlist.parmbox.show_filter",
- "h.pane.group.addgroup",
- "h.pane.group.equals",
- "h.pane.group.selectall",
- "h.pane.group.togpin",
- "h.pane.group.togpinall",
- "h.pane.gview.increase_subd",
- "h.pane.gview.decrease_subd",
- "h.pane.gview.delete",
- "h.pane.gview.dockhudhandles",
- "h.pane.gview.operator_menu_branch",
- "h.pane.gview.state.view.home_selection",
- "h.pane.gview.repeat_current",
- "h.pane.gview.repeat_current_branch",
- "h.pane.gview.restore_prevcam",
- "h.pane.gview.selectstylebox",
- "h.pane.gview.selectstylelaser",
- "h.pane.gview.selectstylelasso",
- "h.pane.gview.handle.remove_keyframe",
- "h.pane.gview.handle.set_keyframe",
- "h.pane.gview.handle.agentcollisionlayer.boxhandle",
- "h.pane.gview.handle.agentcollisionlayer.orientationhandle",
- "h.pane.gview.handle.agentconfigurejoints.conehandle",
- "h.pane.gview.handle.agentconfigurejoins.childrotationhandle",
- "h.pane.gview.state.volatile_chmodify",
- "h.pane.textport.editor",
- "h.pane.textport.selectall",
- "h.pane.textport.zoom_in",
- "h.pane.textport.zoom_out",
- "h.pane.textport.zoom_reset",
- "h.pane.wsheet.add_image",
- "h.pane.wsheet.add_postit",
- "h.pane.wsheet.addfileop",
- "h.pane.wsheet.allowdroponwire",
- "h.pane.wsheet.batch_rename",
- "h.pane.wsheet.bypass_mode",
- "h.pane.wsheet.change_wire_style",
- "h.pane.wsheet.collapse",
- "h.pane.wsheet.color_palette",
- "h.pane.wsheet.create_netbox",
- "h.pane.wsheet.cut_wires_mode",
- "h.pane.wsheet.del_netbox_and_contents",
- "h.pane.wsheet.dirty_cook_selected_node",
- "h.pane.wsheet.dirty_node",
- "h.pane.wsheet.doautomovenodes",
- "h.pane.wsheet.dosnapping",
- "h.pane.wsheet.down",
- "h.pane.wsheet.drop_on_wire_mode",
- "h.pane.wsheet.edit_images",
- "h.pane.wsheet.expose",
- "h.pane.wsheet.flag1_mode",
- "h.pane.wsheet.flag2_mode",
- "h.pane.wsheet.flag3_mode",
- "h.pane.wsheet.flag4_mode",
- "h.pane.wsheet.home",
- "h.pane.wsheet.home_selection",
- "h.pane.wsheet.jump_mark_undo",
- "h.pane.wsheet.jump_mark_1",
- "h.pane.wsheet.jump_mark_2",
- "h.pane.wsheet.jump_mark_3",
- "h.pane.wsheet.jump_mark_4",
- "h.pane.wsheet.jump_mark_5",
- "h.pane.wsheet.layout",
- "h.pane.wsheet.layout_all",
- "h.pane.wsheet.layout_mode",
- "h.pane.wsheet.left",
- "h.pane.wsheet.maximize",
- "h.pane.wsheet.maximize_adjust",
- "h.pane.wsheet.minimize",
- "h.pane.wsheet.minimize_adjust",
- "h.pane.wsheet.mode_switch",
- "h.pane.wsheet.next_wi",
- "h.pane.wsheet.node_quick_nav",
- "h.pane.wsheet.open_display_options",
- "h.pane.wsheet.open_groups",
- "h.pane.wsheet.open_treeview",
- "h.pane.wsheet.opmenu.flag_debug",
- "h.pane.wsheet.prev_wi",
- "h.pane.wsheet.rename_node",
- "h.pane.wsheet.right",
- "h.pane.wsheet.scope_chans",
- "h.pane.wsheet.select",
- "h.pane.wsheet.select_mode",
- "h.pane.wsheet.set_mark_1",
- "h.pane.wsheet.set_mark_2",
- "h.pane.wsheet.set_mark_3",
- "h.pane.wsheet.set_mark_4",
- "h.pane.wsheet.set_mark_5",
- "h.pane.wsheet.snap_netbox_bounds",
- "h.pane.wsheet.stitch_mode",
- "h.pane.wsheet.up",
- "h.pane.wsheet.zoom_in",
- "h.pane.wsheet.zoom_out",
- "h.playbar.next_bookmark",
- "h.playbar.prev_bookmark",
- "h.playbar.show_range",
- "inputfield.context_help",
- "inputfield.editor"
- ]
-}
diff --git a/viewer_states/keycam.py b/viewer_states/keycam.py
index a711a9f..0ed2f4a 100644
--- a/viewer_states/keycam.py
+++ b/viewer_states/keycam.py
@@ -1,5 +1,5 @@
import hou
-from hc import HCCam, HCPrefs, HCGeo, HCGuides, HCSceneViewer
+from hc import HCCam, HCSettings, HCGeo, HCGuides, HCSceneViewer
# Required to pass in hou.SceneViewer object
@@ -26,7 +26,7 @@ class State(object):
# Setup hc cam
self.cam = HCCam(self.cam_node, self.scene_viewer)
- prefs = HCPrefs().prefs().get('keycam')
+ prefs = HCSettings().prefs().get('keycam')
self.cam.delta_r = prefs.get('units').get('delta_r')
self.cam.delta_t = prefs.get('units').get('delta_t')