SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
scripts/123.py (6.2K)
1 from pathlib import Path
2 import json
3
4 import hou
5 from PySide6 import QtWidgets
6 from PySide6.QtCore import Qt, QTimer
7
8
9 def _houdini_font():
10 main_window = hou.qt.mainWindow()
11 if main_window is not None:
12 return main_window.font()
13
14 app = QtWidgets.QApplication.instance()
15 return app.font() if app is not None else None
16
17
18 def _read_settings():
19 hc_path = hou.getenv("HC_PATH")
20 if not hc_path:
21 return {}
22 settings_path = Path(hc_path) / "hc_settings.json"
23 try:
24 return json.loads(settings_path.read_text())
25 except Exception:
26 return {}
27
28
29 def _write_settings(data):
30 hc_path = hou.getenv("HC_PATH")
31 if not hc_path:
32 return
33 settings_path = Path(hc_path) / "hc_settings.json"
34 settings_path.write_text(json.dumps(data, indent=4) + "\n")
35
36
37 # Fallbacks for the settings this script reads. It deliberately does not
38 # import hc -- this runs before the package is needed, and an exception in a
39 # startup script costs the whole launch -- so it cannot use HCSettings and its
40 # DEFAULTS merge. tools/check.py asserts these still agree with hcschema.
41 _DEFAULTS = {
42 "show_prompt": True,
43 "default_autosave_state": True,
44 }
45
46
47 def _startup_setting(key):
48 value = _read_settings().get("startup", {}).get(key, _DEFAULTS[key])
49 return bool(value)
50
51
52 def _current_desktop_mode():
53 return _read_settings().get("desktop_mode", "attached")
54
55
56 class StartupPrompt(QtWidgets.QDialog):
57 def __init__(self, last_file, parent=None):
58 super().__init__(parent or hou.qt.mainWindow())
59 self._open_last_file = False
60
61 self.setWindowTitle("Startup")
62 self.setWindowFlags(self.windowFlags() | Qt.Tool)
63 self.resize(820, 240)
64
65 font = _houdini_font()
66 if font is not None:
67 self.setFont(font)
68
69 layout = QtWidgets.QVBoxLayout(self)
70
71 label = QtWidgets.QLabel(f"Open last file?\n{last_file}")
72 label.setWordWrap(True)
73 label.setTextInteractionFlags(Qt.TextSelectableByMouse)
74 label.setFocusPolicy(Qt.NoFocus)
75 layout.addWidget(label)
76
77 self.autosave_checkbox = QtWidgets.QCheckBox("Enable autosave")
78 self.autosave_checkbox.setChecked(_startup_setting("default_autosave_state"))
79 self.autosave_checkbox.setFocusPolicy(Qt.NoFocus)
80 layout.addWidget(self.autosave_checkbox)
81
82 mode_layout = QtWidgets.QHBoxLayout()
83 mode_label = QtWidgets.QLabel("Layout mode:")
84 mode_label.setFocusPolicy(Qt.NoFocus)
85 self.mode_combo = QtWidgets.QComboBox()
86 self.mode_combo.addItem("Attached", "attached")
87 self.mode_combo.addItem("Detached", "detached")
88 current = _current_desktop_mode()
89 idx = self.mode_combo.findData(current)
90 if idx >= 0:
91 self.mode_combo.setCurrentIndex(idx)
92 mode_layout.addWidget(mode_label)
93 mode_layout.addWidget(self.mode_combo)
94 mode_layout.addStretch()
95 layout.addLayout(mode_layout)
96
97 buttons = QtWidgets.QDialogButtonBox()
98 yes_button = buttons.addButton("Yes", QtWidgets.QDialogButtonBox.AcceptRole)
99 no_button = buttons.addButton("No", QtWidgets.QDialogButtonBox.RejectRole)
100 yes_button.clicked.connect(self._acceptOpen)
101 no_button.clicked.connect(self.reject)
102 layout.addWidget(buttons)
103
104 QTimer.singleShot(0, self._clearInitialFocus)
105
106 def _clearInitialFocus(self):
107 widget = QtWidgets.QApplication.focusWidget()
108 if widget is not None and self.isAncestorOf(widget):
109 widget.clearFocus()
110
111 def _acceptOpen(self):
112 self._open_last_file = True
113 self.accept()
114
115 @property
116 def open_last_file(self):
117 return self._open_last_file
118
119 @property
120 def desktop_mode(self):
121 data = self.mode_combo.currentData()
122 return data if data is not None else "attached"
123
124
125 # This file runs when Houdini is opened without a .hip file.
126
127 data_dir = Path(hou.getenv("HOUDINI_USER_PREF_DIR")) / "st_data"
128 data_dir.mkdir(parents=True, exist_ok=True)
129
130 state_file = data_dir / "state.json"
131 if not state_file.exists():
132 state_file.write_text(json.dumps({"last_file": ""}, indent=4))
133 else:
134 try:
135 loaded_state = json.loads(state_file.read_text())
136 except (OSError, ValueError):
137 loaded_state = {}
138 last_file = loaded_state.get("last_file", "")
139
140 # A recorded path goes stale whenever the scene is moved, renamed or
141 # deleted, and a never-saved scene reports a phantom $HOME/untitled.hip
142 # that was never written to disk. Offering either one made hou.hipFile.load
143 # raise hou.OperationFailed out of this startup script. Only offer a file
144 # that is really there.
145 if last_file and not Path(last_file).is_file():
146 last_file = ""
147
148 # Also runs under hython and batch, where there is no hou.qt and
149 # StartupPrompt raises AttributeError before the rest of the package
150 # has loaded. Skip the prompt and leave the session file-less.
151 show_prompt = (last_file and hou.isUIAvailable()
152 and _startup_setting("show_prompt"))
153
154 if hou.isUIAvailable() and not show_prompt:
155 # Nothing asked, so nothing is opened -- not asking is the same answer
156 # as clicking No. The autosave preference the dialog would have set is
157 # still applied, or turning the prompt off would quietly take autosave
158 # with it.
159 hou.setPreference(
160 "autoSave", "1" if _startup_setting("default_autosave_state") else "0")
161
162 if show_prompt:
163 dialog = StartupPrompt(last_file)
164 dialog.exec()
165
166 hou.setPreference("autoSave", "1" if dialog.autosave_checkbox.isChecked() else "0")
167
168 settings = _read_settings()
169 settings["desktop_mode"] = dialog.desktop_mode
170 _write_settings(settings)
171
172 if dialog.open_last_file:
173 # Still guard the load: the file can be unreadable or corrupt even
174 # when it exists, and an exception here aborts the rest of startup.
175 try:
176 hou.hipFile.load(last_file, suppress_save_prompt=True)
177 except hou.OperationFailed as exc:
178 hou.ui.displayMessage(
179 "Could not open last file:\n%s\n\n%s" % (last_file, exc),
180 severity=hou.severityType.Warning,
181 )