git.lucas.co / hou-control
git clone https://git.lucas.co/hou-control.git

commit7b64287d9e7a65c3c71579457029d633f1c79a41
parentc4e416c38a
authorLucas Galante <[email protected]>
date2026-04-16 20:43
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')