-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdialogs.py
More file actions
561 lines (501 loc) · 23.8 KB
/
Copy pathdialogs.py
File metadata and controls
561 lines (501 loc) · 23.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
"""Dialogs extracted from the app_qt god-object.
build_properties_dialog() is the Properties & Settings dialog, moved here verbatim
(window references via the `win` parameter) so the main window shrinks while
behavior stays identical. app_qt helpers are imported locally to avoid a cycle.
"""
from __future__ import annotations
import os
import shutil
import subprocess
import sys
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QCheckBox,
QComboBox,
QDialog,
QDialogButtonBox,
QFileDialog,
QFrame,
QGroupBox,
QHBoxLayout,
QLabel,
QLineEdit,
QListWidgetItem,
QPushButton,
QScrollArea,
QTabWidget,
QVBoxLayout,
QWidget,
)
from core.utils import find_deadlinecommand, subprocess_creation_flags
from media import _find_ffprobe, find_ffmpeg_tool
from ui_dialogs import error, inform, warn
from workers import DeadlineQueryThread
def build_properties_dialog(win, initial_tab: str | None = None) -> None:
"""The Properties & Settings dialog. Operates on the main window ``win``."""
from app_qt import (
APP_VERSION,
LOG_PATH,
PROFILE_PATH,
_blender_version_status,
_find_blender,
_find_c4dpy,
)
from core.runtime import BLENDER_RUNTIME_VERSION, _norm_blender
dlg = QDialog(win)
dlg.setWindowTitle("Properties & Settings")
dlg.setMinimumWidth(720)
dlg.setMinimumHeight(460)
# Open tall enough that the common tabs fit without scrolling; tabs that
# overflow (Watch & Auto-render) now scroll rather than squeeze.
dlg.resize(820, 760)
root = QVBoxLayout(dlg)
tabs = QTabWidget()
root.addWidget(tabs)
def group(title: str) -> tuple[QGroupBox, QVBoxLayout]:
"""A titled, framed section — the modern replacement for a bare all-caps
header. Returns the box (add it to the tab) and its content layout."""
box = QGroupBox(title)
gl = QVBoxLayout(box)
gl.setSpacing(8)
return box, gl
def disclose(checkbox: QCheckBox) -> tuple[QWidget, QVBoxLayout]:
"""A body whose visibility follows a master checkbox (progressive
disclosure) — advanced options stay hidden until the feature is on."""
body = QWidget()
bl = QVBoxLayout(body)
bl.setContentsMargins(18, 0, 0, 0)
bl.setSpacing(8)
body.setVisible(checkbox.isChecked())
checkbox.toggled.connect(body.setVisible)
return body, bl
def _tab(title: str) -> QVBoxLayout:
# Each tab scrolls instead of squeezing. Without this, a short window
# compresses the page and word-wrapped hints get clipped/overlapped
# (the tall Watch & Auto-render tab was unreadable). The page keeps its
# natural height and a scrollbar appears only when it doesn't fit.
content = QWidget()
v = QVBoxLayout(content)
v.setContentsMargins(4, 4, 4, 4)
v.setSpacing(10)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setFrameShape(QFrame.Shape.NoFrame)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
scroll.setWidget(content)
tabs.addTab(scroll, title)
return v
def _open_path(target) -> None:
target = Path(target)
try:
target.mkdir(parents=True, exist_ok=True)
if sys.platform == "darwin":
subprocess.Popen(["open", str(target)])
elif os.name == "nt":
os.startfile(str(target)) # type: ignore[attr-defined]
else:
subprocess.Popen(["xdg-open", str(target)])
except Exception:
pass
def hint(text: str) -> QLabel:
lbl = QLabel(text)
lbl.setWordWrap(True)
lbl.setStyleSheet(f"color:{win._palette.text_muted}; font-size:11px;")
return lbl
# ── General ──────────────────────────────────────────────────────
lay = _tab("General")
g, gl = group("When a render finishes")
behave_row = QHBoxLayout()
behave_row.addWidget(QLabel("Then:"))
when_combo = QComboBox()
_when_opts = [("Do nothing", "nothing"), ("Quit the app", "quit"), ("Sleep the computer", "sleep")]
when_combo.addItems([lbl for lbl, _ in _when_opts])
_vals = [v for _, v in _when_opts]
when_combo.setCurrentIndex(_vals.index(win._when_done) if win._when_done in _vals else 0)
behave_row.addWidget(when_combo)
behave_row.addStretch()
gl.addLayout(behave_row)
lay.addWidget(g)
g, gl = group("Preview")
preview_cb = QCheckBox("Show a live frame preview while rendering")
preview_cb.setChecked(win._preview_enabled)
gl.addWidget(preview_cb)
gl.addWidget(hint("Renders the current frame as it goes so you can watch progress. "
"Turn off for a small speed-up on heavy scenes."))
lay.addWidget(g)
g, gl = group("Startup")
restore_cb = QCheckBox("Reopen the last session on launch")
restore_cb.setChecked(getattr(win, "_restore_session_on_launch", False))
gl.addWidget(restore_cb)
gl.addWidget(hint("Off (default): the app opens to a clean, empty workspace — use "
"Profile → New (⌘N) to start fresh anytime, the Scene picker's "
"recents to reopen a scene, or Reopen Last Session to bring back "
"your last scene + queue. On: it restores your last scene, mappings "
"and queue automatically."))
lay.addWidget(g)
lay.addStretch()
# ── Render Engines ───────────────────────────────────────────────
lay = _tab("Render Engines")
lay.addWidget(hint("Scenes route to a renderer automatically by type — Blender for "
".blend / .fbx / .usd / .obj…, and Cinema 4D + Redshift for .c4d."))
g, gl = group("Blender")
blender_row = QHBoxLayout()
blender_edit = QLineEdit(win._blender_path)
blender_edit.setPlaceholderText("Path to the Blender executable")
blender_locate = QPushButton("Locate")
blender_row.addWidget(QLabel("Executable:"))
blender_row.addWidget(blender_edit, 1)
blender_row.addWidget(blender_locate)
gl.addLayout(blender_row)
def do_locate_blender() -> None:
if sys.platform == "darwin":
chosen = QFileDialog.getExistingDirectory(dlg, "Select Blender.app", "/Applications")
else:
chosen, _ = QFileDialog.getOpenFileName(dlg, "Select Blender executable")
if chosen:
resolved = _norm_blender(chosen)
if resolved:
blender_edit.setText(resolved)
else:
warn(
dlg, "Not a Blender App",
"That location doesn't contain Blender. Pick the Blender app itself "
"(e.g. /Applications/Blender.app) or its executable.")
blender_locate.clicked.connect(do_locate_blender)
detect_row = QHBoxLayout()
detect_btn = QPushButton("Auto-detect")
detect_btn.setToolTip("Search the usual install locations for Blender")
install_btn = QPushButton("Install Managed Blender…")
install_btn.setToolTip(f"Download a win-contained Blender {BLENDER_RUNTIME_VERSION} runtime")
detect_row.addWidget(detect_btn)
detect_row.addWidget(install_btn)
detect_row.addStretch()
gl.addLayout(detect_row)
blender_ver_lbl = hint("")
gl.addWidget(blender_ver_lbl)
def do_check_version() -> None:
exe = blender_edit.text().strip()
if not exe or not Path(exe).exists():
blender_ver_lbl.setText("No valid Blender path set.")
return
try:
out = subprocess.run([exe, "--version"], capture_output=True, text=True, timeout=15,
creationflags=subprocess_creation_flags())
text = (out.stdout or out.stderr).strip()
first = text.splitlines()[0] if text else ""
blender_ver_lbl.setText(
_blender_version_status(first) if first else "Could not read Blender version.")
except Exception as exc:
blender_ver_lbl.setText(f"Version check failed: {exc}")
def do_autodetect() -> None:
found = _find_blender(blender_edit.text().strip())
if found:
blender_edit.setText(found)
do_check_version()
else:
blender_ver_lbl.setText(
"No Blender found in the usual locations. Use “Locate” to pick it "
"manually, or set the BLENDER_PATH environment variable.")
detect_btn.clicked.connect(do_autodetect)
install_btn.clicked.connect(win._install_managed_runtime)
blender_edit.editingFinished.connect(do_check_version)
do_check_version()
lay.addWidget(g)
g, gl = group("Cinema 4D + Redshift")
c4d_row = QHBoxLayout()
c4dpy_edit = QLineEdit(win._c4dpy_path)
c4dpy_edit.setPlaceholderText("Path to c4dpy (Cinema 4D's headless Python)")
c4d_locate = QPushButton("Locate")
c4d_row.addWidget(QLabel("c4dpy:"))
c4d_row.addWidget(c4dpy_edit, 1)
c4d_row.addWidget(c4d_locate)
gl.addLayout(c4d_row)
c4d_detect_row = QHBoxLayout()
c4d_detect_btn = QPushButton("Auto-detect")
c4d_detect_btn.setToolTip("Search the usual install locations for Cinema 4D's c4dpy")
c4d_detect_row.addWidget(c4d_detect_btn)
c4d_detect_row.addStretch()
gl.addLayout(c4d_detect_row)
c4d_status_lbl = hint("")
def _c4d_refresh() -> None:
p = c4dpy_edit.text().strip()
if p and Path(p).exists():
c4d_status_lbl.setText(f"✓ Cinema 4D detected — {Path(p).name}")
else:
c4d_status_lbl.setText("Not set — only needed for Cinema 4D (.c4d) scenes. Redshift renders use this.")
def do_locate_c4d() -> None:
chosen, _ = QFileDialog.getOpenFileName(dlg, "Select c4dpy executable", c4dpy_edit.text() or "")
if chosen:
c4dpy_edit.setText(chosen)
_c4d_refresh()
def do_detect_c4d() -> None:
found = _find_c4dpy()
if found:
c4dpy_edit.setText(found)
_c4d_refresh()
c4d_locate.clicked.connect(do_locate_c4d)
c4d_detect_btn.clicked.connect(do_detect_c4d)
c4dpy_edit.editingFinished.connect(_c4d_refresh)
gl.addWidget(c4d_status_lbl)
_c4d_refresh()
lay.addWidget(g)
lay.addStretch()
# ── Watch & Auto-render ──────────────────────────────────────────
lay = _tab("Watch && Auto-render")
g, gl = group("Watch & Auto-render")
gl.addWidget(hint("Watch a folder and render dropped clips automatically. The setup now "
"lives in its own panel — choose a folder, pick auto-map or previz "
"assembly, set the naming, and watch progress live."))
open_watch_btn = QPushButton("Open the Watch & Auto-render panel →")
open_watch_btn.setObjectName("PrimaryButton")
def _open_watch_panel() -> None:
win._open_watch_render_panel()
dlg.accept()
open_watch_btn.clicked.connect(_open_watch_panel)
gl.addWidget(open_watch_btn)
lay.addWidget(g)
lay.addStretch()
# ── Updates ──────────────────────────────────────────────────────
lay = _tab("Updates")
g, gl = group("Software updates")
gl.addWidget(hint("When a newer version is released, the app shows a popup with the "
"release notes so you can download it — nothing installs without "
"your say-so."))
upd_status = QLabel(f"This build is v{APP_VERSION}.")
upd_status.setStyleSheet(f"color:{win._palette.text_muted}; font-size:12px;")
gl.addWidget(upd_status)
launch_check_cb = QCheckBox("Check for updates on launch")
launch_check_cb.setChecked(getattr(win, "_check_updates_on_launch", True))
gl.addWidget(launch_check_cb)
gl.addWidget(hint("Looks for a newer release a few seconds after the app starts and "
"pops the update notice if one is found. Turn off to only check "
"manually with the button below."))
upd_check_btn = QPushButton("Check for Updates Now")
upd_check_btn.clicked.connect(lambda: win._check_for_updates(manual=True))
upd_row = QHBoxLayout()
upd_row.addWidget(upd_check_btn)
upd_row.addStretch()
gl.addLayout(upd_row)
lay.addWidget(g)
lay.addStretch()
# ── Deadline ─────────────────────────────────────────────────────
lay = _tab("Deadline")
lay.addWidget(hint("Submit Blender and Cinema 4D jobs to a Thinkbox Deadline farm. "
"Leave blank to render locally."))
g, gl = group("Configuration")
# Repo Path
repo_row = QHBoxLayout()
repo_edit = QLineEdit(win._deadline_repo_path)
repo_edit.setPlaceholderText("Default repository (or browse path)")
repo_locate = QPushButton("Locate")
repo_row.addWidget(QLabel("Repository Path:"))
repo_row.addWidget(repo_edit, 1)
repo_row.addWidget(repo_locate)
gl.addLayout(repo_row)
def do_locate_repo() -> None:
chosen = QFileDialog.getExistingDirectory(dlg, "Select Deadline Repository Path", repo_edit.text() or "")
if chosen:
repo_edit.setText(chosen)
repo_locate.clicked.connect(do_locate_repo)
# Command Path
cmd_row = QHBoxLayout()
cmd_edit = QLineEdit(win._deadline_command_path)
cmd_edit.setPlaceholderText("Path to deadlinecommand executable (optional)")
cmd_locate = QPushButton("Locate")
cmd_row.addWidget(QLabel("Command Path: "))
cmd_row.addWidget(cmd_edit, 1)
cmd_row.addWidget(cmd_locate)
gl.addLayout(cmd_row)
def do_locate_cmd() -> None:
chosen, _ = QFileDialog.getOpenFileName(dlg, "Select deadlinecommand executable", cmd_edit.text() or "")
if chosen:
cmd_edit.setText(chosen)
cmd_locate.clicked.connect(do_locate_cmd)
repo_help = QLabel(
"Repository Path is your Deadline repository folder — e.g. "
"/opt/Thinkbox/Deadline/repository, or \\\\server\\DeadlineRepository on "
"Windows. Leave it blank to use this machine's configured default. "
"Command Path is auto-detected; set it only if deadlinecommand isn't found. "
"Use Test Connection to verify — any failure shows full details in Live Logs.")
repo_help.setWordWrap(True)
repo_help.setStyleSheet(f"color:{win._palette.text_muted}; font-size:11px;")
gl.addWidget(repo_help)
# Name Template
template_row = QHBoxLayout()
template_edit = QLineEdit(win._deadline_job_name_template)
template_edit.setPlaceholderText("e.g. Render Mapper Pro Job - {scene_name}")
template_row.addWidget(QLabel("Name Template: "))
template_row.addWidget(template_edit, 1)
gl.addLayout(template_row)
# Comment
comment_row = QHBoxLayout()
comment_edit = QLineEdit(win._deadline_comment)
comment_edit.setPlaceholderText("Optional job comment")
comment_row.addWidget(QLabel("Job Comment: "))
comment_row.addWidget(comment_edit, 1)
gl.addLayout(comment_row)
lay.addWidget(g)
g, gl = group("Connection")
status_lbl = QLabel("Connection status: Not tested")
status_lbl.setStyleSheet(f"color: {win._palette.text_faint}; font-size: 11px; font-weight: bold;")
gl.addWidget(status_lbl)
# Buttons Row
diag_btn_layout = QHBoxLayout()
test_conn_btn = QPushButton("Test Connection")
export_files_btn = QPushButton("Export Job Files")
diag_btn_layout.addWidget(test_conn_btn)
diag_btn_layout.addWidget(export_files_btn)
diag_btn_layout.addStretch()
gl.addLayout(diag_btn_layout)
lay.addWidget(g)
lay.addStretch()
# ── Diagnostics ──────────────────────────────────────────────────
lay = _tab("Diagnostics")
g, gl = group("Bundled tools")
ff_lbl = QLabel(f"ffmpeg: {find_ffmpeg_tool('ffmpeg') or 'not found'}\n"
f"ffprobe: {_find_ffprobe() or 'not found'}")
ff_lbl.setStyleSheet(f"color:{win._palette.text_muted}; font-size:11px;")
ff_lbl.setWordWrap(True)
gl.addWidget(ff_lbl)
lay.addWidget(g)
g, gl = group("Files & logs")
data_dir = PROFILE_PATH.parent
data_row = QHBoxLayout()
data_row.addWidget(QLabel("App data folder:"))
data_path = QLineEdit(str(data_dir))
data_path.setReadOnly(True)
data_row.addWidget(data_path, 1)
open_data_btn = QPushButton("Open")
open_data_btn.clicked.connect(lambda: _open_path(data_dir))
data_row.addWidget(open_data_btn)
gl.addLayout(data_row)
diag_tools = QHBoxLayout()
open_log_btn = QPushButton("Open Logs Folder")
open_log_btn.clicked.connect(lambda: _open_path(LOG_PATH.parent))
copy_diag_btn = QPushButton("Copy Diagnostics")
copy_diag_btn.clicked.connect(win._copy_diagnostics)
diag_tools.addWidget(open_log_btn)
diag_tools.addWidget(copy_diag_btn)
diag_tools.addStretch()
gl.addLayout(diag_tools)
lay.addWidget(g)
lay.addStretch()
# Dialog buttons live below the tabs.
btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
root.addWidget(btns)
# Handle Ok/Cancel
def on_accept() -> None:
win._blender_path = blender_edit.text().strip()
win._c4dpy_path = c4dpy_edit.text().strip()
win._deadline_repo_path = repo_edit.text().strip()
win._deadline_command_path = cmd_edit.text().strip()
win._deadline_job_name_template = template_edit.text().strip()
win._deadline_comment = comment_edit.text().strip()
# Sync hidden widgets in panel if needed
win.deadline_panel.dl_cmd_edit.setText(win._deadline_command_path)
win.deadline_panel.dl_repo_edit.setText(win._deadline_repo_path)
win.deadline_panel.dl_name_template_edit.setText(win._deadline_job_name_template)
win.deadline_panel.dl_comment_edit.setText(win._deadline_comment)
# Behaviour
win._restore_session_on_launch = restore_cb.isChecked()
win._when_done = _vals[when_combo.currentIndex()]
_menu_label = {"nothing": "Do Nothing", "quit": "Quit App", "sleep": "Sleep Computer"}.get(win._when_done)
if _menu_label and hasattr(win, "_when_actions") and _menu_label in win._when_actions:
win._when_actions[_menu_label].setChecked(True)
if hasattr(win, "preview_action"):
win.preview_action.setChecked(preview_cb.isChecked())
else:
win._preview_enabled = preview_cb.isChecked()
# Watch / Auto-render / Previz settings now live in the Watch & Auto-render
# dock panel (which owns + persists them); the Properties tab is just a
# shortcut into it, so there is nothing to read back here.
# Updates
win._check_updates_on_launch = launch_check_cb.isChecked()
win._save_profile() # persist immediately so settings survive a quick quit
dlg.accept()
btns.accepted.connect(on_accept)
btns.rejected.connect(dlg.reject)
# Handle testing connection and exporting files inside dialog. The query
# runs off-thread (DeadlineQueryThread) so the dialog never freezes — the
# modal event loop still delivers the result signal while it's open.
def _apply_props_test_result(res: dict) -> None:
ok = bool(res.get("ok"))
dp = win.deadline_panel
if ok:
pools = res.get("pools", [])
current_pool = dp.dl_pool_combo.currentText()
current_sec_pool = dp.dl_sec_pool_combo.currentText()
dp.dl_pool_combo.clear()
dp.dl_sec_pool_combo.clear()
dp.dl_pool_combo.addItems(pools)
dp.dl_sec_pool_combo.addItems(["", *pools])
if current_pool:
dp.dl_pool_combo.setCurrentText(current_pool)
if current_sec_pool:
dp.dl_sec_pool_combo.setCurrentText(current_sec_pool)
groups = res.get("groups", [])
current_group = dp.dl_group_combo.currentText()
dp.dl_group_combo.clear()
dp.dl_group_combo.addItems(["", *groups])
if current_group:
dp.dl_group_combo.setCurrentText(current_group)
machines = res.get("machines", [])
dp.dl_machines_list.blockSignals(True)
currently_checked = {
dp.dl_machines_list.item(i).text().strip()
for i in range(dp.dl_machines_list.count())
if dp.dl_machines_list.item(i).checkState() == Qt.CheckState.Checked
}
dp.dl_machines_list.clear()
for m in machines:
item = QListWidgetItem(m)
item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
item.setCheckState(
Qt.CheckState.Checked if (m in currently_checked or not currently_checked)
else Qt.CheckState.Unchecked)
dp.dl_machines_list.addItem(item)
dp.dl_machines_list.blockSignals(False)
# Dialog feedback — guarded, since the dialog may be closed mid-test.
try:
test_conn_btn.setEnabled(True)
if ok:
status_lbl.setText("Connection status: Connected")
status_lbl.setStyleSheet(f"color: {win._palette.success}; font-size: 11px; font-weight: bold;")
inform(dlg, "Deadline Connection", "Successfully connected to Deadline repository and updated pools, groups, and machine list!")
else:
status_lbl.setText("Connection status: Connection failed")
status_lbl.setStyleSheet(f"color: {win._palette.danger}; font-size: 11px; font-weight: bold;")
warn(dlg, "Deadline Warning",
res.get("error", "") or "deadlinecommand failed.")
except RuntimeError:
pass # dialog already closed
def run_test_connection() -> None:
cmd = cmd_edit.text().strip() or find_deadlinecommand() or "deadlinecommand"
status_lbl.setText("Connection status: Testing...")
status_lbl.setStyleSheet(f"color: {win._palette.warning}; font-size: 11px; font-weight: bold;")
if not Path(cmd).exists() and not shutil.which(cmd):
status_lbl.setText("Connection status: deadlinecommand not found")
status_lbl.setStyleSheet(f"color: {win._palette.danger}; font-size: 11px; font-weight: bold;")
error(dlg, "Deadline Connection Error", f"deadlinecommand not found at {cmd}.\nPlease check your Thinkbox Deadline installation.")
return
if win._props_deadline_thread is not None and win._props_deadline_thread.isRunning():
return
test_conn_btn.setEnabled(False)
win._props_deadline_thread = DeadlineQueryThread(cmd, repo_edit.text().strip())
win._props_deadline_thread.result.connect(_apply_props_test_result)
win._props_deadline_thread.start()
test_conn_btn.clicked.connect(run_test_connection)
export_files_btn.clicked.connect(win._export_deadline_files)
if initial_tab:
# Tolerant match so callers (incl. in-app help links) can pass a clean
# name: ignore the '&' mnemonic markers, case, and allow a prefix match.
def _norm(s: str) -> str:
return s.replace("&", "").strip().lower()
want = _norm(initial_tab)
for i in range(tabs.count()):
label = _norm(tabs.tabText(i))
if label == want or label.startswith(want):
tabs.setCurrentIndex(i)
break
dlg.exec()