feat: shelf button opens PySide6 dialog with port spinner, toggle button, copy — per SideFX HOM cookbook
This commit is contained in:
+144
-54
@@ -1,80 +1,170 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<shelfDocument>
|
<shelfDocument>
|
||||||
<toolshelf name="hermes_bridge" label="Hermes Bridge">
|
<toolshelf name="hermes_bridge" label="Hermes Bridge">
|
||||||
<memberTool name="hermes_bridge_toggle"/>
|
<memberTool name="hermes_bridge_dialog"/>
|
||||||
</toolshelf>
|
</toolshelf>
|
||||||
<tool name="hermes_bridge_toggle" label="Toggle Bridge" icon="MISC_python">
|
<tool name="hermes_bridge_dialog" label="Hermes Bridge" icon="MISC_python">
|
||||||
<helpText><![CDATA[Toggle the Hermes-Houdini Bridge.
|
<helpText><![CDATA[Open the Hermes Bridge control panel.
|
||||||
Right-click: Set port.]]></helpText>
|
Manage port, open/close the bridge, copy connection info.]]></helpText>
|
||||||
<script scriptType="python"><![CDATA[
|
<script scriptType="python"><![CDATA[
|
||||||
|
from PySide6 import QtWidgets, QtCore, QtGui
|
||||||
import hou
|
import hou
|
||||||
import socket
|
import socket
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# ── Config ──────────────────────────────────────────
|
|
||||||
CONFIG_FILE = os.path.join(hou.homeHoudiniDirectory(), "hermes_bridge.json")
|
CONFIG_FILE = os.path.join(hou.homeHoudiniDirectory(), "hermes_bridge.json")
|
||||||
DEFAULT_PORT = 12345
|
DEFAULT_PORT = 12345
|
||||||
|
|
||||||
def load_port():
|
def load_config():
|
||||||
try:
|
try:
|
||||||
with open(CONFIG_FILE) as f:
|
with open(CONFIG_FILE) as f:
|
||||||
return json.load(f).get("port", DEFAULT_PORT)
|
return json.load(f)
|
||||||
except Exception:
|
except Exception:
|
||||||
return DEFAULT_PORT
|
return {"port": DEFAULT_PORT}
|
||||||
|
|
||||||
def save_port(p):
|
def save_config(cfg):
|
||||||
with open(CONFIG_FILE, "w") as f:
|
with open(CONFIG_FILE, "w") as f:
|
||||||
json.dump({"port": p}, f, indent=2)
|
json.dump(cfg, f, indent=2)
|
||||||
|
|
||||||
def is_open(port):
|
def bridge_is_open(port):
|
||||||
result = hou.hscript("openport")[0]
|
return f"Port {port} is open" in hou.hscript("openport")[0]
|
||||||
return f"Port {port} is open" in result
|
|
||||||
|
|
||||||
def toggle(port):
|
# ── Dialog ──────────────────────────────────────────
|
||||||
if is_open(port):
|
class HermesBridgeDialog(QtWidgets.QDialog):
|
||||||
hou.hscript(f"closeport {port}")
|
def __init__(self, parent=None):
|
||||||
hou.ui.displayMessage(
|
super().__init__(parent)
|
||||||
f"Bridge CLOSED on port {port}",
|
self.cfg = load_config()
|
||||||
severity=hou.severityType.ImportantMessage,
|
self.port = self.cfg.get("port", DEFAULT_PORT)
|
||||||
title="Hermes Bridge"
|
self.hostname = socket.gethostname()
|
||||||
)
|
self._build_ui()
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
def _build_ui(self):
|
||||||
|
self.setWindowTitle("Hermes Bridge")
|
||||||
|
self.setMinimumWidth(420)
|
||||||
|
self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
|
||||||
|
|
||||||
|
# Dark theme
|
||||||
|
self.setStyleSheet("""
|
||||||
|
QDialog { background: #1e1e1e; color: #e0e0e0; }
|
||||||
|
QLabel { color: #ccc; }
|
||||||
|
QLabel#title { font-size: 16px; font-weight: bold; color: #fff; }
|
||||||
|
QLabel#status { font-size: 14px; font-weight: bold; }
|
||||||
|
QPushButton { border: none; border-radius: 4px; padding: 8px 16px;
|
||||||
|
font-weight: bold; font-size: 13px; }
|
||||||
|
QPushButton#btnToggle { padding: 12px; font-size: 15px; }
|
||||||
|
QPushButton#btnToggle[active="true"] {
|
||||||
|
background: #5c1a1a; color: #ff7777; border: 1px solid #a44; }
|
||||||
|
QPushButton#btnToggle[active="false"] {
|
||||||
|
background: #1a4a1a; color: #77cc77; border: 1px solid #4a4; }
|
||||||
|
QPushButton#btnToggle:hover { opacity: 0.9; }
|
||||||
|
QPushButton#btnCopy { background: #1565c0; color: #fff; }
|
||||||
|
QPushButton#btnCopy:hover { background: #1976d2; }
|
||||||
|
QSpinBox { background: #2d2d2d; border: 1px solid #555; color: #e0e0e0;
|
||||||
|
padding: 5px 8px; border-radius: 3px; }
|
||||||
|
QLineEdit { background: #2d2d2d; border: 1px solid #555; color: #aaa;
|
||||||
|
padding: 5px 8px; border-radius: 3px; }
|
||||||
|
""")
|
||||||
|
|
||||||
|
layout = QtWidgets.QVBoxLayout(self)
|
||||||
|
layout.setSpacing(12)
|
||||||
|
layout.setContentsMargins(20, 20, 20, 20)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = QtWidgets.QLabel("\U0001f916 Hermes Bridge")
|
||||||
|
title.setObjectName("title")
|
||||||
|
layout.addWidget(title)
|
||||||
|
|
||||||
|
# Status
|
||||||
|
self.status_label = QtWidgets.QLabel()
|
||||||
|
self.status_label.setObjectName("status")
|
||||||
|
layout.addWidget(self.status_label)
|
||||||
|
|
||||||
|
# Port
|
||||||
|
port_row = QtWidgets.QHBoxLayout()
|
||||||
|
port_row.addWidget(QtWidgets.QLabel("Port:"))
|
||||||
|
self.port_spin = QtWidgets.QSpinBox()
|
||||||
|
self.port_spin.setRange(1024, 65535)
|
||||||
|
self.port_spin.setValue(self.port)
|
||||||
|
self.port_spin.setFixedWidth(100)
|
||||||
|
self.port_spin.valueChanged.connect(self._on_port_changed)
|
||||||
|
port_row.addWidget(self.port_spin)
|
||||||
|
port_row.addStretch()
|
||||||
|
layout.addLayout(port_row)
|
||||||
|
|
||||||
|
# Toggle button
|
||||||
|
self.btn_toggle = QtWidgets.QPushButton()
|
||||||
|
self.btn_toggle.setObjectName("btnToggle")
|
||||||
|
self.btn_toggle.clicked.connect(self._toggle)
|
||||||
|
layout.addWidget(self.btn_toggle)
|
||||||
|
|
||||||
|
# Prompt text
|
||||||
|
layout.addWidget(QtWidgets.QLabel("Tell Hermes:"))
|
||||||
|
prompt_row = QtWidgets.QHBoxLayout()
|
||||||
|
self.prompt_field = QtWidgets.QLineEdit()
|
||||||
|
self.prompt_field.setReadOnly(True)
|
||||||
|
prompt_row.addWidget(self.prompt_field)
|
||||||
|
self.btn_copy = QtWidgets.QPushButton("Copy")
|
||||||
|
self.btn_copy.setObjectName("btnCopy")
|
||||||
|
self.btn_copy.clicked.connect(self._copy)
|
||||||
|
prompt_row.addWidget(self.btn_copy)
|
||||||
|
layout.addLayout(prompt_row)
|
||||||
|
|
||||||
|
layout.addStretch()
|
||||||
|
|
||||||
|
def _refresh(self):
|
||||||
|
is_open = bridge_is_open(self.port)
|
||||||
|
if is_open:
|
||||||
|
self.status_label.setText(
|
||||||
|
f"\u25cf Connected \u2014 {self.hostname}:{self.port}")
|
||||||
|
self.status_label.setStyleSheet(
|
||||||
|
"color: #4caf50; font-size: 14px; font-weight: bold;")
|
||||||
|
self.btn_toggle.setText("Close Bridge")
|
||||||
|
self.btn_toggle.setProperty("active", "true")
|
||||||
else:
|
else:
|
||||||
hou.hscript(f"closeport {port}")
|
self.status_label.setText("\u25cf Not connected")
|
||||||
hou.hscript(f"openport {port} 0.0.0.0")
|
self.status_label.setStyleSheet(
|
||||||
host = socket.gethostname()
|
"color: #ff5555; font-size: 14px; font-weight: bold;")
|
||||||
hou.ui.displayMessage(
|
self.btn_toggle.setText("Open Bridge")
|
||||||
f"Bridge OPEN \u2014 {host}:{port}\n\n"
|
self.btn_toggle.setProperty("active", "false")
|
||||||
f"Tell Hermes:\n"
|
# Force style refresh
|
||||||
f'"I have Houdini open on {host} (port {port})"',
|
self.btn_toggle.style().unpolish(self.btn_toggle)
|
||||||
severity=hou.severityType.ImportantMessage,
|
self.btn_toggle.style().polish(self.btn_toggle)
|
||||||
title="Hermes Bridge"
|
self.prompt_field.setText(
|
||||||
)
|
f"Hey Hermes, I have a Houdini session running "
|
||||||
|
f"on {self.hostname} (port {self.port}).")
|
||||||
|
|
||||||
# ── Right-click: set port ───────────────────────────
|
def _toggle(self):
|
||||||
kwargs = kwargs if "kwargs" in dir() else {}
|
if bridge_is_open(self.port):
|
||||||
if kwargs.get("altclick") or kwargs.get("ctrlclick"):
|
hou.hscript(f"closeport {self.port}")
|
||||||
port = load_port()
|
|
||||||
new = hou.ui.readInput(
|
|
||||||
"Bridge port number",
|
|
||||||
buttons=("OK", "Cancel"),
|
|
||||||
default_choice=0,
|
|
||||||
title="Hermes Bridge \u2014 Port",
|
|
||||||
initial_contents=str(port)
|
|
||||||
)
|
|
||||||
if new and new[0]:
|
|
||||||
try:
|
|
||||||
new_port = int(new[1].strip())
|
|
||||||
save_port(new_port)
|
|
||||||
hou.ui.displayMessage(
|
|
||||||
f"Port set to {new_port}",
|
|
||||||
severity=hou.severityType.Message,
|
|
||||||
title="Hermes Bridge"
|
|
||||||
)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
else:
|
||||||
toggle(load_port())
|
hou.hscript(f"closeport {self.port}")
|
||||||
|
hou.hscript(f"openport {self.port} 0.0.0.0")
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
def _on_port_changed(self, value):
|
||||||
|
was_open = bridge_is_open(self.port)
|
||||||
|
if was_open:
|
||||||
|
hou.hscript(f"closeport {self.port}")
|
||||||
|
self.port = value
|
||||||
|
self.cfg["port"] = value
|
||||||
|
save_config(self.cfg)
|
||||||
|
if was_open:
|
||||||
|
hou.hscript(f"closeport {self.port}")
|
||||||
|
hou.hscript(f"openport {self.port} 0.0.0.0")
|
||||||
|
self._refresh()
|
||||||
|
|
||||||
|
def _copy(self):
|
||||||
|
QtWidgets.QApplication.clipboard().setText(
|
||||||
|
self.prompt_field.text())
|
||||||
|
self.btn_copy.setText("\u2713 Copied!")
|
||||||
|
QtCore.QTimer.singleShot(2000,
|
||||||
|
lambda: self.btn_copy.setText("Copy"))
|
||||||
|
|
||||||
|
# ── Show dialog ─────────────────────────────────────
|
||||||
|
dialog = HermesBridgeDialog(hou.ui.mainQtWindow())
|
||||||
|
dialog.show()
|
||||||
]]></script>
|
]]></script>
|
||||||
</tool>
|
</tool>
|
||||||
</shelfDocument>
|
</shelfDocument>
|
||||||
|
|||||||
Reference in New Issue
Block a user