Repository navigation
Expand file tree
/
Copy pathconfigwindow.cpp
More file actions
1715 lines (1464 loc) · 70.5 KB
/
Copy pathconfigwindow.cpp
File metadata and controls
1715 lines (1464 loc) · 70.5 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
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#include "configwindow.h"
#include <QApplication>
#include <QClipboard>
#include <QDir>
#include <QFile>
#include <QTextStream>
#include <QFont>
#include <QPalette>
#include <QProcess>
#include <QScreen>
#include <QStyle>
#include <QThread>
#include <QScrollArea>
#include <QGroupBox>
#include <QTimer>
#include <QSizePolicy>
#include <QShowEvent>
#include <QResizeEvent>
#include <QMoveEvent>
#include <QGridLayout>
#include <QScrollBar>
#include <QRegularExpression>
#include <QPointer>
#include <tuple>
#include "appsettings.h"
#include "dialogs.h"
#include "dialogstyle.h"
#include "lgaregistry.h"
#include "logger.h"
#include "nukebridge.h"
#include "qflowlayout.h"
#ifdef Q_OS_MACOS
#include "macintegration.h"
#endif
#ifdef Q_OS_WIN
#include <qt_windows.h>
#include <dwmapi.h>
#endif
namespace {
/// Ancho fijo de la ventana. Antes había DOS: 900 en el constructor y 800 en el resize, así
/// que la ventana cambiaba de ancho sola al terminar el escaneo.
constexpr int kWindowWidth = 800;
/// Alto mínimo de la ventana.
constexpr int kMinWindowHeight = 400;
// ============================================================================
// 🔲🔲🔲 SEPARACION ENTRE CARDS — CENTRALIZADA ACA 🔲🔲🔲
// ----------------------------------------------------------------------------
// Espacio vertical, en píxeles, entre los tres bloques de la ventana (File
// Association, Preferred Nuke Version, Nuke Bridge). Cambiar este valor los
// separa o los junta a TODOS de una.
// ============================================================================
constexpr int kCardSpacing = 12;
/// La ventana nunca ocupa más que esta fracción del alto USABLE de la pantalla (o sea ya
/// descontados la barra de menú y el Dock). Con el panel manual desplegado el contenido pide
/// más de lo que entra en un portátil, y una ventana pegada a los dos bordes se lee como si
/// estuviera rota; el resto lo resuelve el scroll.
constexpr double kMaxScreenFraction = 0.8;
#ifdef Q_OS_MACOS
/// Cuanto se espera la respuesta al cartel de permiso de macOS antes de volver a habilitar
/// APPLY. Generoso a proposito: el cartel lo contesta una persona, y reactivar el boton no
/// invalida una respuesta que llegue despues.
constexpr int kAssocWatchdogMs = 90000;
#endif
#ifdef Q_OS_WIN
void applyDarkTitleBar(QWidget *window)
{
if (!window) {
return;
}
const HWND hwnd = reinterpret_cast<HWND>(window->winId());
const BOOL enableDarkMode = TRUE;
// Windows 10 20H1+ usa 20; versiones anteriores compatibles usan 19.
HRESULT result = DwmSetWindowAttribute(
hwnd, 20, &enableDarkMode, sizeof(enableDarkMode));
if (FAILED(result)) {
DwmSetWindowAttribute(
hwnd, 19, &enableDarkMode, sizeof(enableDarkMode));
}
// Windows 11 permite fijar caption y texto sin depender del tema global.
const COLORREF captionColor = RGB(22, 22, 22);
const COLORREF textColor = RGB(242, 242, 242);
DwmSetWindowAttribute(hwnd, 35, &captionColor, sizeof(captionColor));
DwmSetWindowAttribute(hwnd, 36, &textColor, sizeof(textColor));
}
#endif
}
ConfigWindow::ConfigWindow(QWidget *parent)
: QWidget(parent)
, scrollArea(nullptr)
, nukePathEdit(nullptr)
, browseButton(nullptr)
, saveButton(nullptr)
, applyButton(nullptr)
, descriptionLabel(nullptr)
, nukeVersionDescLabel(nullptr)
, nukeScanner(nullptr)
, versionsContainer(nullptr)
, scanningLabel(nullptr)
, foundVersionsLabel(nullptr)
, versionsButtonsWidget(nullptr)
, versionsLayout(nullptr)
, foundVersionsCount(0)
, scanFinished(false)
, bridgeDescLabel(nullptr)
, bridgeChipLabel(nullptr)
, bridgeDirEdit(nullptr)
, bridgeBrowseButton(nullptr)
, bridgeInstallButton(nullptr)
, bridgeHintLabel(nullptr)
, manualToggleButton(nullptr)
, manualPanel(nullptr)
, manualStepNumbers{nullptr, nullptr, nullptr}
, manualStepTexts{nullptr, nullptr, nullptr}
, manualCodeLabel(nullptr)
, exportButton(nullptr)
, copyLineButton(nullptr)
, copyFeedbackTimer(nullptr)
, langEnButton(nullptr)
, langEsButton(nullptr)
, versionLabel(nullptr)
, userResizedHeight(false)
, programmaticResize(false)
, lastAppliedHeight(0)
, userMovedWindow(false)
, programmaticMove(false)
{
Logger::logInfo("=== CONSTRUCTOR ConfigWindow INICIADO ===");
Logger::logInfo("Llamando a setupUI()...");
setupUI();
Logger::logInfo("✓ setupUI() completado");
Logger::logInfo("Llamando a loadCurrentPath()...");
loadCurrentPath();
Logger::logInfo("✓ loadCurrentPath() completado");
// Configurar ventana
Logger::logInfo("Configurando ventana...");
setWindowTitle("OpenInNukeX Config");
// Ancho fijo, alto libre: el contenido es una columna de bloques de ancho fijo, así que
// estirar en X solo agrega fondo a los costados. En Y sí hay algo que ganar, porque el
// contenido puede no entrar en la pantalla.
setFixedWidth(kWindowWidth);
// El techo se fija YA, y no recién cuando termina el escaneo de versiones: hasta ese
// momento la ventana se podía estirar más allá del 80% de la pantalla, y ese estirón
// además contaba como un gesto del usuario y le desactivaba el auto-alto por el resto de
// la sesión. Se usa el tope de pantalla y no el del contenido porque acá el layout todavía
// no corrió: su sizeHint mediría de menos y dejaría la ventana corta hasta el primer
// recalculo.
const int cap = screenHeightCap();
applyWindowHeight(cap, qMin(600, cap));
Logger::logInfo("✓ Ventana configurada");
// Cargar estilo QSS
Logger::logInfo("Cargando estilo QSS...");
loadStyleSheet();
Logger::logInfo("✓ Estilo QSS cargado");
// Una sola pasada de texto para toda la ventana, DESPUES del QSS: `retranslateUi()` es
// tambien el lugar donde se arman los textos que dependen del estado (el chip del bridge,
// el hint, el label del escaner), y `repolish()` necesita la hoja ya aplicada para que el
// selector del chip valga.
Logger::logInfo("Aplicando textos e inspeccionando el Nuke Bridge...");
retranslateUi();
Logger::logInfo("✓ Textos aplicados");
// Inicializar y comenzar el escaneo de versiones
Logger::logInfo("Iniciando proceso de escáner...");
initializeScanner();
Logger::logInfo("✓ initializeScanner() llamado");
Logger::logInfo("=== CONSTRUCTOR ConfigWindow COMPLETADO ===");
}
void ConfigWindow::showEvent(QShowEvent *event)
{
QWidget::showEvent(event);
#ifdef Q_OS_WIN
applyDarkTitleBar(this);
#endif
// Centrado también acá, y no solo desde `applyWindowHeight()`: el del constructor corre
// con la ventana todavía sin marco, así que no puede descontar la barra de título. Este es
// el primero que la ve, y es el que decide dónde APARECE.
centerOnScreen();
}
void ConfigWindow::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
if (programmaticResize || !isVisible()) {
return;
}
// `oldSize().height() > 0`: durante el armado de la ventana llegan resizes que no son del
// usuario y que tampoco pasan por el flag (los del propio layout al mostrarse).
if (event->oldSize().height() <= 0
|| event->size().height() == event->oldSize().height()) {
return;
}
// Segunda línea de defensa: si el alto al que se llegó es exactamente el último que impuso
// la app, el resize es suyo aunque haya llegado con el flag apagado. Pasa de verdad: el
// `processEvents()` del recálculo despacha resizes pendientes fuera de esa ventana.
if (event->size().height() == lastAppliedHeight) {
return;
}
if (!userResizedHeight) {
Logger::logInfo("El usuario redimensionó la ventana: la app deja de imponer el alto");
}
userResizedHeight = true;
}
void ConfigWindow::moveEvent(QMoveEvent *event)
{
QWidget::moveEvent(event);
if (programmaticMove || !isVisible()) {
return;
}
if (!userMovedWindow) {
Logger::logInfo("El usuario movió la ventana: la app deja de re-centrarla");
}
userMovedWindow = true;
}
void ConfigWindow::centerOnScreen()
{
// Una vez que el usuario la movió o la redimensionó, la geometría es suya: re-centrarla
// seria arrancarle la ventana de donde la dejó.
if (userMovedWindow || userResizedHeight) {
return;
}
QScreen *screen = this->screen();
if (!screen) {
return;
}
// Se centra el FRAME y no el cliente: `frameGeometry()` incluye la barra de título, y
// centrar el cliente deja la ventana un poco baja, que es justo el síntoma que esto viene
// a corregir. El alto del marco solo se conoce una vez que la ventana existe, así que si
// todavía no hay marco se usa el alto del cliente y se recentra en la próxima pasada.
const QRect available = screen->availableGeometry();
const QRect frame = frameGeometry();
const int frameHeight = frame.height() > 0 ? frame.height() : height();
const int titleBar = qMax(0, frameHeight - height());
const int x = available.x() + (available.width() - width()) / 2;
const int y = available.y() + (available.height() - frameHeight) / 2 + titleBar;
programmaticMove = true;
move(x, y);
programmaticMove = false;
}
void ConfigWindow::setupUI()
{
// Crear el layout principal
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(0);
// Crear un QScrollArea para permitir desplazamiento
scrollArea = new QScrollArea(this);
scrollArea->setObjectName("darkScrollArea");
scrollArea->setWidgetResizable(true);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
scrollArea->setFrameShape(QFrame::NoFrame);
// Asegurar que el QScrollArea ocupe todo el espacio disponible
scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// Contenedor central para limitar el ancho y centrar el contenido
QWidget *centralWidget = new QWidget();
centralWidget->setObjectName("centralSettingsWidget");
// Permitir que se ajuste al contenido
centralWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
// Crear un layout para centrar el contenido horizontalmente
QHBoxLayout *horizontalCenterLayout = new QHBoxLayout(centralWidget);
horizontalCenterLayout->setContentsMargins(0, 20, 0, 20);
horizontalCenterLayout->setSpacing(0);
// Crear un widget contenedor para el contenido real
QWidget *contentWidget = new QWidget();
contentWidget->setObjectName("contentSettingsWidget");
contentWidget->setFixedWidth(700); // Ancho para todas las cajas principales
// Layout para el contenido
QVBoxLayout *centralLayout = new QVBoxLayout(contentWidget);
centralLayout->setContentsMargins(20, 20, 20, 20);
// Ver `kCardSpacing` al tope del archivo: es la perilla para separarlos o juntarlos.
centralLayout->setSpacing(kCardSpacing);
// Agregar el widget de contenido al layout horizontal centrado
horizontalCenterLayout->addStretch();
horizontalCenterLayout->addWidget(contentWidget);
horizontalCenterLayout->addStretch();
// Asignar el widget central al área de desplazamiento
scrollArea->setWidget(centralWidget);
// Agregar el área de desplazamiento al layout principal
mainLayout->addWidget(scrollArea, 1);
// ------------------------------------------------------------------------------------------------
// Grupo de configuración de File Association
QGroupBox *fileAssociationGroup = new QGroupBox(contentWidget);
fileAssociationGroup->setObjectName("settingsGroup");
fileAssociationGroup->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
QVBoxLayout *fileAssociationLayout = new QVBoxLayout(fileAssociationGroup);
fileAssociationLayout->setSizeConstraint(QLayout::SetDefaultConstraint);
fileAssociationLayout->setContentsMargins(20, 10, 20, 10);
fileAssociationLayout->setSpacing(10);
QLabel *fileAssociationTitle = new QLabel("File Association", fileAssociationGroup);
fileAssociationTitle->setObjectName("sectionTitle");
fileAssociationLayout->addWidget(fileAssociationTitle);
// APPLY va a la derecha del TEXTO, no del título, y los dos centrados verticalmente entre
// sí: alineado con el título quedaba a dos renglones de distancia de la explicación de lo
// que hace, con un hueco vacío en el medio que no significaba nada.
QHBoxLayout *fileAssociationBody = new QHBoxLayout();
fileAssociationBody->setContentsMargins(0, 0, 0, 0);
fileAssociationBody->setSpacing(20);
descriptionLabel = new QLabel(TR(DescFileAssociation), fileAssociationGroup);
descriptionLabel->setObjectName("sectionDescription");
descriptionLabel->setWordWrap(true);
// El centrado vertical se hace DENTRO del label y no con un flag de alineación en el
// layout: con el flag, la caja se encoge a su sizeHint y un label con wordWrap mide mal
// (Qt no le consulta el heightForWidth). Así el label ocupa el alto de la fila —que lo
// fija el botón— y es el texto el que se centra adentro.
descriptionLabel->setAlignment(Qt::AlignLeft | Qt::AlignVCenter);
applyButton = new QPushButton(TR(BtnApply), fileAssociationGroup);
applyButton->setFixedHeight(40);
applyButton->setMinimumWidth(158);
applyButton->setProperty("class", "action");
fileAssociationBody->addWidget(descriptionLabel, 1);
fileAssociationBody->addWidget(applyButton, 0, Qt::AlignVCenter);
fileAssociationLayout->addLayout(fileAssociationBody);
// Agregar el grupo de File Association al layout central
centralLayout->addWidget(fileAssociationGroup);
// ------------------------------------------------------------------------------------------------
// Grupo de configuración de Preferred Nuke Version
QGroupBox *nukeVersionGroup = new QGroupBox(contentWidget);
nukeVersionGroup->setObjectName("settingsGroup");
nukeVersionGroup->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
QVBoxLayout *nukeVersionLayout = new QVBoxLayout(nukeVersionGroup);
nukeVersionLayout->setSizeConstraint(QLayout::SetDefaultConstraint);
nukeVersionLayout->setContentsMargins(20, 0, 20, 10);
// Título principal de Preferred Nuke Version
QLabel *nukeVersionTitle = new QLabel("Preferred Nuke Version", nukeVersionGroup);
nukeVersionTitle->setObjectName("sectionTitle");
nukeVersionLayout->addWidget(nukeVersionTitle);
nukeVersionLayout->addSpacing(4);
nukeVersionDescLabel = new QLabel(TR(DescNukeVersion), nukeVersionGroup);
nukeVersionDescLabel->setObjectName("sectionDescription");
nukeVersionDescLabel->setWordWrap(true);
nukeVersionLayout->addWidget(nukeVersionDescLabel);
nukeVersionLayout->addSpacing(10);
// ===== SECCIÓN DE VERSIONES ENCONTRADAS (ARRIBA) =====
Logger::logInfo("=== CREANDO SECCIÓN DE VERSIONES ENCONTRADAS ===");
// Crear contenedor para las versiones encontradas
versionsContainer = new QWidget(nukeVersionGroup);
versionsContainer->setObjectName("versionsMainContainer");
// NO aplicar setStyleSheet aquí porque sobrescribe los estilos de los botones hijos
Logger::logInfo("✓ versionsContainer creado");
versionsLayout = new QVBoxLayout(versionsContainer);
versionsLayout->setContentsMargins(0, 0, 0, 0);
versionsLayout->setSpacing(10);
Logger::logInfo("✓ versionsLayout creado y configurado");
// Etiqueta descriptiva
foundVersionsLabel = new QLabel(TR(ScanChoose), versionsContainer);
foundVersionsLabel->setObjectName("sectionDescription");
foundVersionsLabel->setWordWrap(true);
versionsLayout->addWidget(foundVersionsLabel);
Logger::logInfo("✓ foundVersionsLabel creado y agregado");
// Etiqueta de scanning (inicialmente visible)
scanningLabel = new QLabel(TR(ScanScanning), versionsContainer);
scanningLabel->setObjectName("scanStatus");
versionsLayout->addWidget(scanningLabel);
Logger::logInfo("✓ scanningLabel creado y agregado");
// Widget contenedor para los botones de versiones (inicialmente oculto)
versionsButtonsWidget = new QWidget(versionsContainer);
versionsButtonsWidget->setObjectName("versionsButtonsContainer");
versionsButtonsWidget->setVisible(false);
// NO establecer fondo transparente para que los botones mantengan sus estilos
Logger::logInfo("✓ versionsButtonsWidget creado y ocultado");
// Layout de flujo para organizar los botones en filas
QFlowLayout *flowLayout = new QFlowLayout(versionsButtonsWidget, 0, 10, 10);
Logger::logInfo("✓ QFlowLayout creado para versionsButtonsWidget");
versionsLayout->addWidget(versionsButtonsWidget);
Logger::logInfo("✓ versionsButtonsWidget agregado a versionsLayout");
// Agregar el contenedor de versiones al grupo
nukeVersionLayout->addWidget(versionsContainer);
nukeVersionLayout->addSpacing(15);
Logger::logInfo("✓ versionsContainer agregado a nukeVersionLayout");
Logger::logInfo("=== SECCIÓN DE VERSIONES COMPLETADA ===");
// Crear layout horizontal para el campo de path y el botón browse
QHBoxLayout *nukePathLayout = new QHBoxLayout();
nukePathLayout->setContentsMargins(0, 0, 0, 0);
nukePathLayout->setSpacing(20);
// Campo de entrada para la ruta de NukeX
nukePathEdit = new QLineEdit(nukeVersionGroup);
nukePathEdit->setPlaceholderText(TR(PlaceholderNukePath));
// Botón Browse para seleccionar la ruta
browseButton = new QPushButton(TR(BtnBrowse), nukeVersionGroup);
browseButton->setFixedHeight(40);
browseButton->setProperty("class", "secondary");
// Agregar los elementos al layout horizontal
// SAVE va en la MISMA fila, a la derecha de BROWSE: los dos actuan sobre el campo de al
// lado, asi que separarlos en dos filas obligaba a barrer la vista de vuelta para arriba.
saveButton = new QPushButton(TR(BtnSave), nukeVersionGroup);
saveButton->setFixedHeight(40);
saveButton->setMinimumWidth(70);
saveButton->setProperty("class", "action");
nukePathLayout->addWidget(nukePathEdit);
nukePathLayout->addWidget(browseButton);
nukePathLayout->addWidget(saveButton);
// Agregar el layout horizontal al layout vertical de Nuke Version
nukeVersionLayout->addLayout(nukePathLayout);
nukeVersionLayout->addSpacing(10);
// Agregar el grupo de Nuke Version al layout central
centralLayout->addWidget(nukeVersionGroup);
// ------------------------------------------------------------------------------------------------
// Grupo del Nuke Bridge
setupBridgeGroup(contentWidget, centralLayout);
// ------------------------------------------------------------------------------------------------
// Pie: selector de idioma a la izquierda, version a la derecha.
QHBoxLayout *footerLayout = new QHBoxLayout();
footerLayout->setContentsMargins(4, 0, 4, 0);
footerLayout->setSpacing(0);
langEnButton = new QPushButton("EN", contentWidget);
langEsButton = new QPushButton("ES", contentWidget);
for (QPushButton *button : {langEnButton, langEsButton}) {
button->setProperty("class", "lang");
button->setFixedHeight(24);
button->setFixedWidth(38);
button->setCursor(Qt::PointingHandCursor);
}
footerLayout->addWidget(langEnButton);
footerLayout->addWidget(langEsButton);
footerLayout->addStretch(1);
// La version sale de la macro del CMake, nunca escrita a mano (convencion LGA: la fuente
// unica de verdad es el `project(... VERSION ...)`).
versionLabel = new QLabel(QString("v%1 | Lega").arg(OPENINNUKEX_VERSION), contentWidget);
versionLabel->setObjectName("versionLabel");
versionLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
footerLayout->addWidget(versionLabel);
centralLayout->addLayout(footerLayout);
// Conectar señales y slots
connect(browseButton, &QPushButton::clicked, this, &ConfigWindow::browseNukePath);
connect(saveButton, &QPushButton::clicked, this, &ConfigWindow::saveConfiguration);
connect(applyButton, &QPushButton::clicked, this, &ConfigWindow::applyFileAssociation);
connect(langEnButton, &QPushButton::clicked, this,
[this]() { setLanguage(I18n::Lang::En); });
connect(langEsButton, &QPushButton::clicked, this,
[this]() { setLanguage(I18n::Lang::Es); });
// Marca el idioma activo al arrancar (el que quedo persistido).
langEnButton->setProperty("selected", I18n::lang() == I18n::Lang::En);
langEsButton->setProperty("selected", I18n::lang() == I18n::Lang::Es);
}
// ─────────────────────────────────────────────────────────────────────────────
// Nuke Bridge
// ─────────────────────────────────────────────────────────────────────────────
void ConfigWindow::setupBridgeGroup(QWidget *parent, QVBoxLayout *centralLayout)
{
QGroupBox *bridgeGroup = new QGroupBox(parent);
bridgeGroup->setObjectName("settingsGroup");
bridgeGroup->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
QVBoxLayout *bridgeLayout = new QVBoxLayout(bridgeGroup);
bridgeLayout->setSizeConstraint(QLayout::SetDefaultConstraint);
bridgeLayout->setContentsMargins(20, 10, 20, 10);
bridgeLayout->setSpacing(10);
QLabel *bridgeTitle = new QLabel("Nuke Bridge", bridgeGroup);
bridgeTitle->setObjectName("sectionTitle");
bridgeLayout->addWidget(bridgeTitle);
bridgeLayout->addSpacing(4);
// El chip de estado NO va acá arriba junto al título: se crea ahora porque
// `refreshBridgeStatus()` lo necesita existiendo, pero se ubica más abajo, en la fila del
// toggle de instalación manual. Ver el comentario de esa fila.
bridgeChipLabel = new QLabel(TR(ChipNotInstalled), bridgeGroup);
bridgeChipLabel->setObjectName("bridgeChip");
bridgeChipLabel->setProperty("state", "off");
bridgeDescLabel = new QLabel(TR(DescNukeBridge), bridgeGroup);
bridgeDescLabel->setObjectName("sectionDescription");
bridgeDescLabel->setWordWrap(true);
bridgeLayout->addWidget(bridgeDescLabel);
bridgeLayout->addSpacing(6);
// Carpeta .nuke + acciones.
QHBoxLayout *bridgeDirLayout = new QHBoxLayout();
bridgeDirLayout->setContentsMargins(0, 0, 0, 0);
bridgeDirLayout->setSpacing(20);
bridgeDirEdit = new QLineEdit(bridgeGroup);
bridgeDirEdit->setPlaceholderText(TR(PlaceholderNukeDir));
bridgeBrowseButton = new QPushButton(TR(BtnBrowse), bridgeGroup);
bridgeBrowseButton->setFixedHeight(40);
bridgeBrowseButton->setProperty("class", "secondary");
bridgeInstallButton = new QPushButton(TR(BtnInstall), bridgeGroup);
bridgeInstallButton->setFixedHeight(40);
bridgeInstallButton->setMinimumWidth(70);
bridgeInstallButton->setProperty("class", "action");
bridgeDirLayout->addWidget(bridgeDirEdit);
bridgeDirLayout->addWidget(bridgeBrowseButton);
bridgeDirLayout->addWidget(bridgeInstallButton);
bridgeLayout->addLayout(bridgeDirLayout);
bridgeHintLabel = new QLabel(bridgeGroup);
bridgeHintLabel->setObjectName("bridgeHint");
bridgeHintLabel->setWordWrap(true);
bridgeHintLabel->setTextFormat(Qt::RichText);
bridgeLayout->addWidget(bridgeHintLabel);
// Es un QPushButton y no un QLabel con <a href> para heredar foco y teclado sin manejar
// linkActivated a mano. Y se ve COMO un botón: pintado de azul y sin caja parecía un
// enlace de texto, o sea algo que abre otra cosa, cuando en realidad despliega contenido
// acá mismo.
manualToggleButton = new QPushButton(TR(LinkManualShow), bridgeGroup);
manualToggleButton->setProperty("class", "toggle");
manualToggleButton->setFixedHeight(32);
manualToggleButton->setCursor(Qt::PointingHandCursor);
// El chip de estado comparte esta fila, pegado al borde derecho. Arriba, junto al título,
// quedaba lejos del control que lo cambia: el botón de instalar está a media caja de
// distancia, y el usuario lo apretaba sin ver que el chip se actualizaba. Acá queda
// inmediatamente debajo de ese botón y en el mismo renglón que la salida manual, que es la
// otra forma de cambiar lo que el chip informa.
QHBoxLayout *manualToggleLayout = new QHBoxLayout();
manualToggleLayout->setContentsMargins(0, 0, 0, 0);
manualToggleLayout->setSpacing(10);
manualToggleLayout->addWidget(manualToggleButton);
manualToggleLayout->addStretch(1);
manualToggleLayout->addWidget(bridgeChipLabel, 0, Qt::AlignVCenter);
bridgeLayout->addLayout(manualToggleLayout);
// ── panel manual, colapsado por defecto ──────────────────────────────────
manualPanel = new QWidget(bridgeGroup);
manualPanel->setObjectName("manualPanel");
manualPanel->setVisible(false);
QVBoxLayout *manualLayout = new QVBoxLayout(manualPanel);
manualLayout->setContentsMargins(0, 10, 0, 0);
manualLayout->setSpacing(14);
// Grilla de dos columnas: los números en la primera y el contenido en la segunda. Lo que
// esto compra es que la caja de código pueda ir en la columna del contenido, o sea con la
// misma sangría que el texto del paso 3 y por lo tanto leyéndose como parte de ese paso.
QGridLayout *stepsGrid = new QGridLayout();
stepsGrid->setContentsMargins(0, 0, 0, 0);
stepsGrid->setHorizontalSpacing(6);
stepsGrid->setVerticalSpacing(4);
stepsGrid->setColumnStretch(1, 1);
for (int i = 0; i < 3; ++i) {
manualStepNumbers[i] = new QLabel(QString("%1.").arg(i + 1), manualPanel);
manualStepNumbers[i]->setObjectName("manualSteps");
manualStepNumbers[i]->setAlignment(Qt::AlignRight | Qt::AlignTop);
manualStepTexts[i] = new QLabel(manualPanel);
manualStepTexts[i]->setObjectName("manualSteps");
manualStepTexts[i]->setTextFormat(Qt::RichText);
manualStepTexts[i]->setWordWrap(true);
stepsGrid->addWidget(manualStepNumbers[i], i, 0, Qt::AlignTop);
stepsGrid->addWidget(manualStepTexts[i], i, 1);
}
// La línea a pegar en el `init.py` y su botón de copiar, en la fila siguiente al paso 3 y
// en su misma columna. El botón va a la derecha de la línea, que es lo que copia: abajo,
// junto al de exportar, no se sabía cuál de las dos cosas copiaba. La caja se queda con el
// sobrante de la fila (stretch 1) en vez de ajustarse al texto, así el botón queda siempre
// en la misma posición y no se corre de lugar si algún día cambia el largo de la línea.
QHBoxLayout *codeRow = new QHBoxLayout();
codeRow->setContentsMargins(0, 0, 0, 0);
codeRow->setSpacing(8);
manualCodeLabel = new QLabel(NukeBridge::pluginAddPathLine(), manualPanel);
manualCodeLabel->setObjectName("manualCode");
manualCodeLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
copyLineButton = new QPushButton(TR(BtnCopyLine), manualPanel);
copyLineButton->setFixedHeight(34);
copyLineButton->setMinimumWidth(120);
copyLineButton->setProperty("class", "secondary");
codeRow->addWidget(manualCodeLabel, 1);
codeRow->addWidget(copyLineButton, 0);
stepsGrid->addLayout(codeRow, 3, 1);
manualLayout->addLayout(stepsGrid);
QHBoxLayout *manualButtons = new QHBoxLayout();
manualButtons->setContentsMargins(0, 0, 0, 0);
manualButtons->setSpacing(10);
exportButton = new QPushButton(TR(BtnExport), manualPanel);
exportButton->setFixedHeight(34);
exportButton->setProperty("class", "action");
manualButtons->addWidget(exportButton);
manualButtons->addStretch(1);
manualLayout->addLayout(manualButtons);
bridgeLayout->addWidget(manualPanel);
centralLayout->addWidget(bridgeGroup);
// El estado se recalcula también al editar el campo a mano: sin esto, pegar otra `.nuke`
// que ya tiene el bridge dejaba el chip en "Not installed" hasta apretar INSTALL.
// editingFinished y no textChanged: inspeccionar el disco en cada tecla es trabajo de I/O
// en el hilo de UI por cada caracter tipeado.
connect(bridgeDirEdit, &QLineEdit::editingFinished, this, &ConfigWindow::refreshBridgeStatus);
connect(bridgeBrowseButton, &QPushButton::clicked, this, &ConfigWindow::browseNukeDirectory);
connect(bridgeInstallButton, &QPushButton::clicked, this, &ConfigWindow::installBridge);
connect(manualToggleButton, &QPushButton::clicked, this, &ConfigWindow::toggleManualPanel);
connect(exportButton, &QPushButton::clicked, this, &ConfigWindow::exportBridgeFiles);
connect(copyLineButton, &QPushButton::clicked, this, &ConfigWindow::copyPluginLine);
// El campo arranca con la carpeta que ya conocemos: la registrada por otra app LGA, o la
// detectada en el home. Vacío solo si no hay ninguna.
bridgeDirEdit->setText(QDir::toNativeSeparators(NukeBridge::currentNukeDirectory()));
}
void ConfigWindow::repolish(QWidget *widget)
{
if (!widget) {
return;
}
widget->style()->unpolish(widget);
widget->style()->polish(widget);
widget->update();
}
void ConfigWindow::refreshBridgeStatus()
{
if (!bridgeChipLabel) {
return;
}
const QString nukeDir = bridgeDirEdit ? bridgeDirEdit->text().trimmed() : QString();
const NukeBridge::Status status = NukeBridge::inspect(nukeDir);
const QString bundled = NukeBridge::bundledVersion();
if (!status.installed()) {
bridgeChipLabel->setText(TR(ChipNotInstalled));
bridgeChipLabel->setProperty("state", "off");
bridgeInstallButton->setText(TR(BtnInstall));
bridgeInstallButton->setProperty("class", "action");
} else if (status.installedVersion.isEmpty()) {
// Instalado pero sin `VERSION` legible: pasa con un bridge puesto a mano, con uno de
// una versión anterior de la app, o con el archivo sin permiso de lectura.
//
// Antes este caso caía en el `else` y el chip mostraba en verde la versión que la app
// TRAERÍA, no la que hay: el usuario veía "al día" sobre un bridge viejo, que es
// exactamente lo que este chip existe para detectar. Se muestra como pendiente.
bridgeChipLabel->setText(TR(ChipInstalledUnknown));
bridgeChipLabel->setProperty("state", "stale");
bridgeInstallButton->setText(TR(BtnReinstall));
bridgeInstallButton->setProperty("class", "action");
} else if (status.installedVersion != bundled) {
// Instalado pero de otra version: el botón vuelve a ser la acción destacada, porque
// acá sí hay algo que hacer. Con "Installed" a secas el usuario no se entera.
bridgeChipLabel->setText(TR(ChipNeedsUpdate).arg(status.installedVersion));
bridgeChipLabel->setProperty("state", "stale");
bridgeInstallButton->setText(TR(BtnReinstall));
bridgeInstallButton->setProperty("class", "action");
} else {
bridgeChipLabel->setText(TR(ChipInstalled).arg(status.installedVersion));
bridgeChipLabel->setProperty("state", "on");
bridgeInstallButton->setText(TR(BtnReinstall));
bridgeInstallButton->setProperty("class", "secondary");
}
repolish(bridgeChipLabel);
repolish(bridgeInstallButton);
// La condición mira que la carpeta EXISTA, no sólo que el campo tenga texto: con una ruta
// tipeada a mano que no existe, el hint afirmaba haberla encontrado mientras el chip decía
// "Not installed" y INSTALL fallaba con "esa carpeta no existe". Tres mensajes, dos de
// ellos falsos.
if (!QFileInfo(nukeDir).isDir()) {
bridgeHintLabel->setText(TR(HintFolderNotFound));
bridgeHintLabel->setVisible(true);
} else if (!status.installed()) {
// toHtmlEscaped: el label es RichText y un path puede traer `&` o `<` (por ejemplo
// /Users/x/R&D/.nuke), que sin escapar se renderiza mal o se come el resto del texto.
bridgeHintLabel->setText(TR(HintFolderFound).arg(
QDir::toNativeSeparators(nukeDir).toHtmlEscaped()));
bridgeHintLabel->setVisible(true);
} else {
// Ya instalado: el hint explicaba qué carpeta usar, y eso ya no aporta nada.
bridgeHintLabel->setVisible(false);
}
}
void ConfigWindow::browseNukeDirectory()
{
const QString current = bridgeDirEdit->text().trimmed();
const QString startDir = QFileInfo(current).isDir() ? current : QDir::homePath();
// La `.nuke` está oculta: sin DontUseNativeDialog el panel de macOS no la muestra y el
// usuario no puede llegar a ella salvo con Cmd+Shift+. , que casi nadie conoce.
QFileDialog dialog(this, TR(PickNukeFolder), startDir);
dialog.setFileMode(QFileDialog::Directory);
dialog.setOption(QFileDialog::ShowDirsOnly, true);
dialog.setOption(QFileDialog::DontUseNativeDialog, true);
dialog.setFilter(dialog.filter() | QDir::Hidden);
if (dialog.exec() != QDialog::Accepted) {
return;
}
const QStringList selected = dialog.selectedFiles();
if (selected.isEmpty()) {
return;
}
bridgeDirEdit->setText(QDir::toNativeSeparators(QDir::cleanPath(selected.first())));
refreshBridgeStatus();
}
namespace {
/// El motor devuelve un CODIGO; el texto que ve el usuario sale de la tabla del idioma
/// activo. Ver el comentario de NukeBridge::Error sobre por que estan separados.
QString bridgeErrorMessage(NukeBridge::Error error)
{
switch (error) {
case NukeBridge::Error::None: return QString();
case NukeBridge::Error::DirMissing: return TR(MsgBridgeDirMissing);
case NukeBridge::Error::SourceRepo: return TR(MsgBridgeSourceIsTarget);
case NukeBridge::Error::PayloadMissing: return TR(MsgBridgePayloadMissing);
case NukeBridge::Error::WriteFailed: return TR(MsgBridgeWriteFailed);
}
return TR(MsgBridgeWriteFailed);
}
} // namespace
void ConfigWindow::installBridge()
{
const QString nukeDir = bridgeDirEdit->text().trimmed();
QString detail;
const NukeBridge::Error error = NukeBridge::install(nukeDir, &detail);
if (error == NukeBridge::Error::None) {
refreshBridgeStatus();
Dialogs::info(this, TR(TitleBridgeInstalled),
TR(MsgBridgeInstalled).arg(Dialogs::colorizePath(
QDir::toNativeSeparators(QDir(nukeDir).filePath(
NukeBridge::pluginFolderName())))));
return;
}
// El detalle tecnico va al log y NO al cartel: al usuario le sirve saber que hacer, no
// que ruta fallo. Al que diagnostica le sirve la ruta, y esa la tiene en el log.
Logger::logError(QString("NukeBridge: instalacion fallida en '%1': %2").arg(nukeDir, detail));
Dialogs::error(this, TR(TitleBridgeFailed), bridgeErrorMessage(error));
}
void ConfigWindow::toggleManualPanel()
{
const bool nowVisible = !manualPanel->isVisible();
manualPanel->setVisible(nowVisible);
manualToggleButton->setText(nowVisible ? TR(LinkManualHide) : TR(LinkManualShow));
// Agranda la ventana para que las instrucciones entren — salvo que el usuario ya la haya
// dimensionado a mano, en cuyo caso `calculateAndResizeWindow()` no le toca el alto y lo
// único que pasa es que aparece contenido nuevo más abajo.
calculateAndResizeWindow();
if (nowVisible && scrollArea) {
// Diferido: recién después de que el layout acomodó el panel recién mostrado tiene
// sentido preguntarle dónde quedó. En la misma pasada, su geometría todavía es la que
// tenía estando oculto.
QTimer::singleShot(0, this, [this]() {
if (!scrollArea || !manualPanel || !manualPanel->isVisible()) {
return;
}
QWidget *content = scrollArea->widget();
if (!content) {
return;
}
// Se desplaza al TOPE del panel y no con `ensureWidgetVisible()`: ese, cuando el
// widget es más alto que el viewport —que es justo lo que pasa con la ventana
// achicada a mano—, lo centra, y entonces el paso 1 queda arriba del borde visible.
const int top = manualPanel->mapTo(content, QPoint(0, 0)).y();
scrollArea->verticalScrollBar()->setValue(top - 20);
});
}
}
void ConfigWindow::exportBridgeFiles()
{
const QString destDir = QFileDialog::getExistingDirectory(
this, TR(PickExportFolder), QDir::homePath());
if (destDir.isEmpty()) {
return;
}
QString detail;
const NukeBridge::Error error = NukeBridge::exportPayload(destDir, &detail);
if (error == NukeBridge::Error::None) {
Dialogs::info(this, TR(TitleBridgeExported),
TR(MsgBridgeExported).arg(Dialogs::colorizePath(
QDir::toNativeSeparators(QDir(destDir).filePath(
NukeBridge::pluginFolderName())))));
return;
}
Logger::logError(QString("NukeBridge: exportacion fallida a '%1': %2").arg(destDir, detail));
Dialogs::error(this, TR(TitleBridgeFailed), bridgeErrorMessage(error));
}
void ConfigWindow::copyPluginLine()
{
QApplication::clipboard()->setText(NukeBridge::pluginAddPathLine());
// El feedback va en el propio botón y no en un cartel: es una confirmación de un gesto
// trivial, y un modal para esto obliga a un click extra para volver a lo que se estaba
// haciendo.
copyLineButton->setText(TR(BtnCopied));
if (!copyFeedbackTimer) {
copyFeedbackTimer = new QTimer(this);
copyFeedbackTimer->setSingleShot(true);
connect(copyFeedbackTimer, &QTimer::timeout, this,
[this]() { copyLineButton->setText(TR(BtnCopyLine)); });
}
copyFeedbackTimer->start(1500);
}
// ─────────────────────────────────────────────────────────────────────────────
// Idioma
// ─────────────────────────────────────────────────────────────────────────────
void ConfigWindow::setLanguage(I18n::Lang lang)
{
if (I18n::lang() == lang) {
return;
}
I18n::setLang(lang);
langEnButton->setProperty("selected", lang == I18n::Lang::En);
langEsButton->setProperty("selected", lang == I18n::Lang::Es);
repolish(langEnButton);
repolish(langEsButton);
retranslateUi();
calculateAndResizeWindow();
}
void ConfigWindow::refreshApplyButtonText()
{
if (!applyButton)
return;
// El boton dice lo que va a HACER. Si esta app ya es la que abre los `.nk`, apretarlo no
// asocia nada nuevo: rehace el registro, que es lo que sirve cuando la asociacion quedo
// apuntando a una copia vieja o el Finder dejo de reconocerla.
//
// En Windows la consulta equivalente lee UserChoice/UserChoiceLatest y compara el ProgID.
#ifdef Q_OS_MACOS
applyButton->setText(MacIntegration::isDefaultNkHandler() ? TR(BtnReapply) : TR(BtnApply));
#else
applyButton->setText(WinFileAssociation::isNkAssociatedWithUs() ? TR(BtnReapply)
: TR(BtnApply));
#endif
}
void ConfigWindow::changeEvent(QEvent *event)
{
QWidget::changeEvent(event);
// Al volver al frente se relee: el usuario pudo haber cambiado la asociacion en Finder, o
// haber contestado el cartel del sistema con la ventana atras.
if (event->type() == QEvent::ActivationChange && isActiveWindow())
refreshApplyButtonText();
}
void ConfigWindow::retranslateUi()
{
refreshApplyButtonText();
descriptionLabel->setText(TR(DescFileAssociation));
nukeVersionDescLabel->setText(TR(DescNukeVersion));
nukePathEdit->setPlaceholderText(TR(PlaceholderNukePath));
browseButton->setText(TR(BtnBrowse));
saveButton->setText(TR(BtnSave));
// El label del escáner dice cosas distintas según en qué punto esté: reconstruirlo pide
// saber en cuál, y por eso el resultado del último escaneo queda guardado.
if (!scanFinished) {
foundVersionsLabel->setText(TR(ScanChoose));
foundVersionsLabel->setVisible(true);
scanningLabel->setText(TR(ScanScanning));
} else if (foundVersionsCount == 0) {
// Sin versiones encontradas, "Elegí una de las encontradas" es falso y además queda
// arriba del mensaje que dice que no hay ninguna. Antes tampoco se reescribía al
// cambiar de idioma, así que los dos renglones quedaban en idiomas distintos.
foundVersionsLabel->setVisible(false);
scanningLabel->setText(TR(ScanNone));
} else {
foundVersionsLabel->setText(TR(ScanFound).arg(foundVersionsCount));
foundVersionsLabel->setVisible(true);
}
bridgeDescLabel->setText(TR(DescNukeBridge));
bridgeDirEdit->setPlaceholderText(TR(PlaceholderNukeDir));
bridgeBrowseButton->setText(TR(BtnBrowse));
manualToggleButton->setText(manualPanel->isVisible() ? TR(LinkManualHide)
: TR(LinkManualShow));
manualStepTexts[0]->setText(TR(ManualStep1));
manualStepTexts[1]->setText(TR(ManualStep2));
manualStepTexts[2]->setText(TR(ManualStep3));
exportButton->setText(TR(BtnExport));
copyLineButton->setText(TR(BtnCopyLine));
// El chip, el hint y el texto del botón de instalar dependen del estado, no solo del
// idioma: los reconstruye el mismo lugar que los calcula.
refreshBridgeStatus();
}
void ConfigWindow::loadCurrentPath()
{
QString currentPath = getNukePathFromFile();
if (!currentPath.isEmpty())
{
nukePathEdit->setText(currentPath);
}
}
QString ConfigWindow::getNukePathFromFile()
{
QString configDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
QDir().mkpath(configDir); // Crear directorio si no existe
QString filepath = QDir(configDir).filePath("nukeXpath.txt");
QFile file(filepath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
{
return QString();
}