MXVK Vulkan Framework 0.24.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
model_view.cpp
Go to the documentation of this file.
1
2/**
3 * @file model_view.cpp
4 * @brief Implements the Qt launcher window for the MXVK command-line viewer.
5 */
6
7#include "model_view.hpp"
8#include <QCoreApplication>
9#include <QDateTime>
10#include <QDebug>
11#include <QDir>
12#include <QFileInfo>
13#include <QInputDialog>
14
15MainWindow::MainWindow(QWidget *parent)
16 : QMainWindow(parent), activeProcess(nullptr) {
17 settings = new QSettings("MXModel", "ModelViewer", this);
18 initWindow();
19 loadSettings();
20 loadRecentFiles();
21}
22
24 saveSettings();
25 if (activeProcess && activeProcess->state() != QProcess::NotRunning) {
26 activeProcess->kill();
27 activeProcess->waitForFinished();
28 }
29}
30
31void MainWindow::initWindow() {
32 setWindowTitle("MXMOD Model Viewer");
33 setGeometry(100, 100, 1200, 800);
34 setMinimumSize(900, 600);
35 setAcceptDrops(true);
36
37 setupMenuBar();
38 setupToolBar();
39 setupCentralWidget();
40 setupStatusBar();
41 createConnections();
42 updateUIState(false);
43}
44
45void MainWindow::setupMenuBar() {
46 QMenu *fileMenu = menuBar()->addMenu(tr("&File"));
47
48 openModelAction = new QAction(style()->standardIcon(QStyle::SP_FileIcon), tr("Open &Model..."), this);
49 openModelAction->setShortcut(QKeySequence::Open);
50 openModelAction->setStatusTip(tr("Open a model file"));
51 fileMenu->addAction(openModelAction);
52
53 openTextureAction = new QAction(tr("Open &Texture..."), this);
54 openTextureAction->setStatusTip(tr("Open a texture file"));
55 fileMenu->addAction(openTextureAction);
56
57 openTextureDirAction = new QAction(tr("Open Texture &Directory..."), this);
58 openTextureDirAction->setStatusTip(tr("Open a texture directory"));
59 fileMenu->addAction(openTextureDirAction);
60
61 fileMenu->addSeparator();
62
63 clearConsoleAction = new QAction(tr("&Clear Console"), this);
64 clearConsoleAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_L));
65 fileMenu->addAction(clearConsoleAction);
66
67 fileMenu->addSeparator();
68
69 exitAction = new QAction(tr("E&xit"), this);
70 exitAction->setShortcut(QKeySequence::Quit);
71 fileMenu->addAction(exitAction);
72
73 QMenu *helpMenu = menuBar()->addMenu(tr("&Help"));
74
75 aboutAction = new QAction(tr("&About"), this);
76 helpMenu->addAction(aboutAction);
77
78 settingsAction = new QAction(tr("&Settings"), this);
79 settingsAction->setShortcut(QKeySequence::Preferences);
80 helpMenu->addAction(settingsAction);
81}
82
83void MainWindow::setupToolBar() {
84 QToolBar *toolBar = addToolBar(tr("Main"));
85 toolBar->setMovable(false);
86 toolBar->setIconSize(QSize(24, 24));
87
88 toolBar->addAction(openModelAction);
89 toolBar->addSeparator();
90
91 openButton = new QPushButton(style()->standardIcon(QStyle::SP_MediaPlay), tr("Launch Viewer"), this);
92 openButton->setMinimumHeight(32);
93 openButton->setStyleSheet("QPushButton { font-weight: bold; padding: 5px 15px; }");
94 toolBar->addWidget(openButton);
95
96 stopButton = new QPushButton(style()->standardIcon(QStyle::SP_MediaStop), tr("Stop"), this);
97 stopButton->setMinimumHeight(32);
98 stopButton->setEnabled(false);
99 toolBar->addWidget(stopButton);
100
101 toolBar->addSeparator();
102
103 clearButton = new QPushButton(style()->standardIcon(QStyle::SP_DialogResetButton), tr("Clear"), this);
104 clearButton->setMinimumHeight(32);
105 toolBar->addWidget(clearButton);
106}
107
108void MainWindow::setupCentralWidget() {
109 QWidget *centralWidget = new QWidget(this);
110 setCentralWidget(centralWidget);
111
112 QVBoxLayout *mainLayout = new QVBoxLayout(centralWidget);
113 mainLayout->setSpacing(10);
114 mainLayout->setContentsMargins(10, 10, 10, 10);
115
116 QGroupBox *fileGroup = new QGroupBox(tr("Model and Texture Selection"), this);
117 QVBoxLayout *fileLayout = new QVBoxLayout(fileGroup);
118
119 QLabel *modelLabel = new QLabel(tr("Model File:"), this);
120 modelLabel->setStyleSheet("font-weight: bold;");
121 fileLayout->addWidget(modelLabel);
122
123 QHBoxLayout *modelLayout = new QHBoxLayout();
124 modelLineEdit = new QLineEdit(this);
125 modelLineEdit->setPlaceholderText(tr("Select a .mxmod/.mxmod.z file/.obj file..."));
126 QPushButton *modelBrowseButton = new QPushButton(style()->standardIcon(QStyle::SP_DirIcon), tr("Browse..."), this);
127 modelLayout->addWidget(modelLineEdit, 1);
128 modelLayout->addWidget(modelBrowseButton);
129 fileLayout->addLayout(modelLayout);
130
131 QLabel *textureLabel = new QLabel(tr("Texture File:"), this);
132 textureLabel->setStyleSheet("font-weight: bold;");
133 fileLayout->addWidget(textureLabel);
134
135 QHBoxLayout *textureLayout = new QHBoxLayout();
136 textureLineEdit = new QLineEdit(this);
137 textureLineEdit->setPlaceholderText(tr("Select a .tex or .png or .mtl file..."));
138 QPushButton *textureBrowseButton = new QPushButton(style()->standardIcon(QStyle::SP_DirIcon), tr("Browse..."), this);
139 textureLayout->addWidget(textureLineEdit, 1);
140 textureLayout->addWidget(textureBrowseButton);
141 fileLayout->addLayout(textureLayout);
142
143 QLabel *dirLabel = new QLabel(tr("Texture Directory:"), this);
144 dirLabel->setStyleSheet("font-weight: bold;");
145 fileLayout->addWidget(dirLabel);
146
147 QHBoxLayout *dirLayout = new QHBoxLayout();
148 textureDirLineEdit = new QLineEdit(this);
149 textureDirLineEdit->setPlaceholderText(tr("Select texture directory..."));
150 QPushButton *dirBrowseButton = new QPushButton(style()->standardIcon(QStyle::SP_DirIcon), tr("Browse..."), this);
151 dirLayout->addWidget(textureDirLineEdit, 1);
152 dirLayout->addWidget(dirBrowseButton);
153 fileLayout->addLayout(dirLayout);
154
155 mainLayout->addWidget(fileGroup);
156
157 QGroupBox *optionsGroup = new QGroupBox(tr("Viewer Options"), this);
158 QGridLayout *optionsLayout = new QGridLayout(optionsGroup);
159 optionsLayout->setSpacing(8);
160
161 QLabel *recentLabel = new QLabel(tr("Recent:"), this);
162 recentFilesCombo = new QComboBox(this);
163 recentFilesCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
164 recentFilesCombo->setMinimumWidth(150);
165 recentFilesCombo->addItem(tr("-- Select Recent --"));
166 QPushButton *removeRecentBtn = new QPushButton(tr("Remove"), this);
167 removeRecentBtn->setMaximumWidth(70);
168 removeRecentBtn->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);
169
170 QLabel *resLabel = new QLabel(tr("Resolution:"), this);
171 resolutionCombo = new QComboBox(this);
172 resolutionCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
173 populateResolutionCombo();
174
175 optionsLayout->addWidget(recentLabel, 0, 0);
176 optionsLayout->addWidget(recentFilesCombo, 0, 1);
177 optionsLayout->addWidget(removeRecentBtn, 0, 2);
178 optionsLayout->addWidget(resLabel, 0, 3);
179 optionsLayout->addWidget(resolutionCombo, 0, 4);
180
181 optionsLayout->setColumnStretch(1, 2);
182 optionsLayout->setColumnStretch(4, 1);
183
184 mainLayout->addWidget(optionsGroup);
185
186 QGroupBox *outputGroup = new QGroupBox(tr("Process Output"), this);
187 QVBoxLayout *outputLayout = new QVBoxLayout(outputGroup);
188
189 consoleOutput = new QPlainTextEdit(this);
190 consoleOutput->setReadOnly(true);
191 consoleOutput->setLineWrapMode(QPlainTextEdit::NoWrap);
192 consoleOutput->setMaximumBlockCount(2000);
193 consoleOutput->setFont(QFont("Consolas", 9));
194 consoleOutput->setStyleSheet("QPlainTextEdit { background-color: #1e1e1e; color: #39ff14; }");
195
196 outputLayout->addWidget(consoleOutput);
197 mainLayout->addWidget(outputGroup, 1);
198
199 connect(modelBrowseButton, &QPushButton::clicked, this, &MainWindow::browseModelFile);
200 connect(textureBrowseButton, &QPushButton::clicked, this, &MainWindow::browseTextureFile);
201 connect(dirBrowseButton, &QPushButton::clicked, this, &MainWindow::browseTextureDirectory);
202 connect(removeRecentBtn, &QPushButton::clicked, this, &MainWindow::removeRecentFile);
203}
204
205void MainWindow::setupStatusBar() {
206 statusLabel = new QLabel(tr("Ready"), this);
207 statusBar()->addWidget(statusLabel);
208}
209
210void MainWindow::createConnections() {
211 connect(openButton, &QPushButton::clicked, this, &MainWindow::openModelViewer);
212 connect(stopButton, &QPushButton::clicked, this, &MainWindow::stopProcess);
213 connect(clearButton, &QPushButton::clicked, this, &MainWindow::clearConsole);
214
215 connect(openModelAction, &QAction::triggered, this, &MainWindow::browseModelFile);
216 connect(openTextureAction, &QAction::triggered, this, &MainWindow::browseTextureFile);
217 connect(openTextureDirAction, &QAction::triggered, this, &MainWindow::browseTextureDirectory);
218 connect(clearConsoleAction, &QAction::triggered, this, &MainWindow::clearConsole);
219 connect(exitAction, &QAction::triggered, this, &QMainWindow::close);
220 connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutDialog);
221 connect(settingsAction, &QAction::triggered, this, &MainWindow::showSettingsDialog);
222
223 connect(recentFilesCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
224 this, &MainWindow::openRecentModel);
225
226 connect(modelLineEdit, &QLineEdit::textChanged, this, &MainWindow::validatePaths);
227 connect(textureLineEdit, &QLineEdit::textChanged, this, &MainWindow::validatePaths);
228 connect(textureDirLineEdit, &QLineEdit::textChanged, this, &MainWindow::validatePaths);
229}
230
231void MainWindow::populateResolutionCombo() {
232 resolutionCombo->addItem("1280x720 (HD)");
233 resolutionCombo->addItem("1920x1080 (Full HD)");
234 resolutionCombo->addItem("2560x1440 (2K)");
235 resolutionCombo->addItem("3840x2160 (4K)");
236 resolutionCombo->setCurrentIndex(1);
237}
238
239void MainWindow::closeEvent(QCloseEvent *event) {
240 if (activeProcess && activeProcess->state() != QProcess::NotRunning) {
241 auto reply = QMessageBox::question(this, tr("Viewer Running"),
242 tr("The model viewer is still running. Do you want to close it and exit?"),
243 QMessageBox::Yes | QMessageBox::No);
244
245 if (reply == QMessageBox::Yes) {
246 activeProcess->kill();
247 activeProcess->waitForFinished(3000);
248 event->accept();
249 } else {
250 event->ignore();
251 }
252 } else {
253 event->accept();
254 }
255}
256
257void MainWindow::dragEnterEvent(QDragEnterEvent *event) {
258 if (event->mimeData()->hasUrls()) {
259 event->acceptProposedAction();
260 }
261}
262
263void MainWindow::dropEvent(QDropEvent *event) {
264 const QMimeData *mimeData = event->mimeData();
265
266 if (mimeData->hasUrls()) {
267 QList<QUrl> urls = mimeData->urls();
268 if (!urls.isEmpty()) {
269 QString filePath = urls.first().toLocalFile();
270 QFileInfo fileInfo(filePath);
271
272 const QString suffix = fileInfo.suffix().toLower();
273 if (suffix == "mxmod" || filePath.endsWith(".mxmod.z") || suffix == "obj") {
274 modelLineEdit->setText(filePath);
275 } else if (suffix == "tex" || suffix == "png" || suffix == "jpg" || suffix == "jpeg" || suffix == "bmp" || suffix == "mtl") {
276 textureLineEdit->setText(filePath);
277 } else if (fileInfo.isDir()) {
278 textureDirLineEdit->setText(filePath);
279 }
280 }
281 event->acceptProposedAction();
282 }
283}
284
285void MainWindow::browseModelFile() {
286 QString lastDir = settings->value("lastModelDir", QDir::homePath()).toString();
287 QString fileName = QFileDialog::getOpenFileName(this,
288 tr("Open Model File"), lastDir,
289 tr("Model Files (*.mxmod *.mxmod.z *.obj);;All Files (*)"));
290
291 if (!fileName.isEmpty()) {
292 modelLineEdit->setText(fileName);
293 settings->setValue("lastModelDir", QFileInfo(fileName).absolutePath());
294 updateRecentFiles(fileName);
295 statusLabel->setText(tr("Model loaded: %1").arg(QFileInfo(fileName).fileName()));
296 }
297}
298
299void MainWindow::browseTextureFile() {
300 QString lastDir = settings->value("lastTextureDir", QDir::homePath()).toString();
301 QString fileName = QFileDialog::getOpenFileName(this,
302 tr("Open Texture File"), lastDir,
303 tr("Texture Files (*.tex *.png *.jpg *.bmp *.mtl);;All Files (*)"));
304
305 if (!fileName.isEmpty()) {
306 textureLineEdit->setText(fileName);
307 settings->setValue("lastTextureDir", QFileInfo(fileName).absolutePath());
308 statusLabel->setText(tr("Texture loaded: %1").arg(QFileInfo(fileName).fileName()));
309 }
310}
311
312void MainWindow::browseTextureDirectory() {
313 QString lastDir = settings->value("lastTextureDirPath", QDir::homePath()).toString();
314 QString dirName = QFileDialog::getExistingDirectory(this,
315 tr("Select Texture Directory"), lastDir,
316 QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
317
318 if (!dirName.isEmpty()) {
319 textureDirLineEdit->setText(dirName);
320 settings->setValue("lastTextureDirPath", dirName);
321 statusLabel->setText(tr("Texture directory: %1").arg(QDir(dirName).dirName()));
322 }
323}
324
325void MainWindow::validatePaths() {
326 QString modelPath = modelLineEdit->text();
327 QString texturePath = textureLineEdit->text();
328 QString textureDir = textureDirLineEdit->text();
329
330 bool valid = !modelPath.isEmpty();
331
332 if (valid) {
333 valid = QFileInfo(modelPath).exists();
334 if (valid && !texturePath.isEmpty()) {
335 valid = QFileInfo(texturePath).exists();
336 }
337 if (valid && !textureDir.isEmpty()) {
338 valid = QDir(textureDir).exists();
339 }
340 }
341
342 openButton->setEnabled(valid && !activeProcess);
343}
344
345QString MainWindow::resolveExecutablePath() const {
346 const QString configuredPath = executablePath.trimmed();
347 if (!configuredPath.isEmpty() && configuredPath != "viewer") {
348 return configuredPath;
349 }
350
351 const QString appPath = QCoreApplication::applicationDirPath();
352 const QStringList candidates = {
353 QDir(appPath).filePath("viewer"),
354 QDir(appPath).filePath("examples/viewer/viewer"),
355 QDir(appPath).filePath("../examples/viewer/viewer"),
356 QDir(appPath).filePath("../../examples/viewer/viewer"),
357 QDir(appPath).filePath("../build/examples/viewer/viewer"),
358 QDir(appPath).filePath("../../build/examples/viewer/viewer"),
359 "viewer",
360 };
361
362 for (const QString &candidate : candidates) {
363 const QFileInfo info(candidate);
364 if ((info.exists() && info.isExecutable()) || candidate == "viewer") {
365 return QDir::cleanPath(candidate);
366 }
367 }
368
369 return "viewer";
370}
371
372void MainWindow::openModelViewer() {
373 QString modelPath = modelLineEdit->text();
374 QString texturePath = textureLineEdit->text();
375 QString textureDir = textureDirLineEdit->text();
376
377 QFileInfo modelInfo(modelPath);
378 QFileInfo textureInfo(texturePath);
379 QDir dir(textureDir);
380
381 if (!modelInfo.exists() || !modelInfo.isFile()) {
382 QMessageBox::warning(this, tr("Invalid Path"),
383 tr("The model file does not exist or is invalid."));
384 return;
385 }
386
387 if (!texturePath.isEmpty() && (!textureInfo.exists() || !textureInfo.isFile())) {
388 QMessageBox::warning(this, tr("Invalid Path"),
389 tr("The texture file does not exist or is invalid."));
390 return;
391 }
392
393 if (!textureDir.isEmpty() && !dir.exists()) {
394 QMessageBox::warning(this, tr("Invalid Path"),
395 tr("The texture directory does not exist."));
396 return;
397 }
398
399 activeProcess = new QProcess(this);
400 QProcess *process = activeProcess;
401
402 consoleOutput->clear();
403 consoleOutput->appendPlainText(QString("[%1] Starting model viewer...")
404 .arg(QDateTime::currentDateTime().toString("hh:mm:ss")));
405
406 // Mirror renderer stdout into the GUI console with wall-clock timestamps.
407 connect(process, &QProcess::readyReadStandardOutput, [this, process]() {
408 QByteArray output = process->readAllStandardOutput();
409 QString text = QString::fromLocal8Bit(output).trimmed();
410 if (!text.isEmpty()) {
411 consoleOutput->appendPlainText(QString("[%1] %2")
412 .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
413 .arg(text));
414 }
415 QScrollBar *scrollBar = consoleOutput->verticalScrollBar();
416 scrollBar->setValue(scrollBar->maximum());
417 });
418
419 // Keep stderr visible in the same console so launch failures are not hidden.
420 connect(process, &QProcess::readyReadStandardError, [this, process]() {
421 QByteArray error = process->readAllStandardError();
422 QString text = QString::fromLocal8Bit(error).trimmed();
423 if (!text.isEmpty()) {
424 QTextCharFormat originalFormat = consoleOutput->currentCharFormat();
425 QTextCharFormat errorFormat = originalFormat;
426 errorFormat.setForeground(QColor("#39ff14"));
427
428 consoleOutput->setCurrentCharFormat(errorFormat);
429 consoleOutput->appendPlainText(QString("[%1] ERROR: %2")
430 .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
431 .arg(text));
432 consoleOutput->setCurrentCharFormat(originalFormat);
433 }
434 QScrollBar *scrollBar = consoleOutput->verticalScrollBar();
435 scrollBar->setValue(scrollBar->maximum());
436 });
437
438 // QProcess owns the renderer lifetime from launch until finished/error cleanup.
439 connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
440 [this, process](int exitCode, QProcess::ExitStatus) {
441 QString exitMsg;
442 if (exitCode != 0) {
443 exitMsg = QString("[%1] Process exited with error code %2")
444 .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
445 .arg(exitCode);
446
447 QTextCharFormat originalFormat = consoleOutput->currentCharFormat();
448 QTextCharFormat errorFormat = originalFormat;
449 errorFormat.setForeground(QColor("#39ff14"));
450 consoleOutput->setCurrentCharFormat(errorFormat);
451 consoleOutput->appendPlainText(exitMsg);
452 consoleOutput->setCurrentCharFormat(originalFormat);
453
454 statusLabel->setText(tr("Viewer exited with error"));
455 } else {
456 exitMsg = QString("[%1] Process completed successfully")
457 .arg(QDateTime::currentDateTime().toString("hh:mm:ss"));
458 consoleOutput->appendPlainText(exitMsg);
459 statusLabel->setText(tr("Viewer closed"));
460 }
461
462 if (activeProcess == process) {
463 activeProcess = nullptr;
464 }
465 process->deleteLater();
466 updateUIState(false);
467 });
468
469 connect(process, &QProcess::errorOccurred, [this, process](QProcess::ProcessError error) {
470 QString errorMsg;
471 switch (error) {
472 case QProcess::FailedToStart:
473 errorMsg = "Failed to start. Is the selected viewer executable installed or configured?";
474 break;
475 case QProcess::Crashed:
476 errorMsg = "Process crashed";
477 break;
478 default:
479 errorMsg = process->errorString();
480 }
481
482 QMessageBox::critical(this, tr("Process Error"),
483 tr("Model viewer error: %1").arg(errorMsg));
484
485 consoleOutput->appendPlainText(QString("[%1] ERROR: %2")
486 .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
487 .arg(errorMsg));
488
489 if (activeProcess == process) {
490 activeProcess = nullptr;
491 }
492 process->deleteLater();
493 updateUIState(false);
494 statusLabel->setText(tr("Error: %1").arg(errorMsg));
495 });
496
497 const QString selectedExecutable = resolveExecutablePath();
498
499 QStringList arguments;
500 arguments << "--filename" << modelPath;
501 if (!texturePath.isEmpty()) {
502 arguments << "--texture" << texturePath;
503 }
504 if (!textureDir.isEmpty()) {
505 arguments << "--resource_path" << textureDir;
506 }
507 QString resolution = resolutionCombo->currentText().split(" ").first();
508 arguments << "-r" << resolution;
509 QString rendererAssetPath = QCoreApplication::applicationDirPath();
510 const QFileInfo selectedExecutableInfo(selectedExecutable);
511 if (selectedExecutableInfo.exists()) {
512 rendererAssetPath = selectedExecutableInfo.absolutePath();
513 }
514 arguments << "-p" << rendererAssetPath;
515 consoleOutput->appendPlainText(QString("[%1] Command: %2 %3")
516 .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
517 .arg(selectedExecutable)
518 .arg(arguments.join(" ")));
519 consoleOutput->appendPlainText(QString("").leftJustified(60, '-'));
520
521 process->start(selectedExecutable, arguments);
522
523 if (!process->waitForStarted(3000)) {
524 QString errorMsg = tr("Failed to start %1. Please check:\n"
525 "1. The executable exists and is in your PATH, or\n"
526 "2. Set the correct path in Settings (Help → Settings)")
527 .arg(selectedExecutable);
528
529 QMessageBox::critical(this, tr("Startup Error"), errorMsg);
530
531 consoleOutput->appendPlainText(QString("[%1] FATAL: Failed to start process: %2")
532 .arg(QDateTime::currentDateTime().toString("hh:mm:ss"))
533 .arg(selectedExecutable));
534
535 activeProcess = nullptr;
536 process->deleteLater();
537 statusLabel->setText(tr("Failed to start viewer"));
538 return;
539 }
540
541 updateUIState(true);
542 statusLabel->setText(tr("Viewer running..."));
543}
544
545void MainWindow::stopProcess() {
546 if (activeProcess && activeProcess->state() != QProcess::NotRunning) {
547 auto reply = QMessageBox::question(this, tr("Stop Viewer"),
548 tr("Are you sure you want to stop the model viewer?"),
549 QMessageBox::Yes | QMessageBox::No);
550
551 if (reply == QMessageBox::Yes) {
552 consoleOutput->appendPlainText(QString("[%1] Stopping process...")
553 .arg(QDateTime::currentDateTime().toString("hh:mm:ss")));
554 activeProcess->terminate();
555
556 if (!activeProcess->waitForFinished(2000)) {
557 activeProcess->kill();
558 }
559 statusLabel->setText(tr("Viewer stopped"));
560 }
561 }
562}
563
564void MainWindow::clearConsole() {
565 consoleOutput->clear();
566 statusLabel->setText(tr("Console cleared"));
567}
568
569void MainWindow::updateUIState(bool processRunning) {
570 openButton->setEnabled(!processRunning);
571 stopButton->setEnabled(processRunning);
572 modelLineEdit->setEnabled(!processRunning);
573 textureLineEdit->setEnabled(!processRunning);
574 textureDirLineEdit->setEnabled(!processRunning);
575 resolutionCombo->setEnabled(!processRunning);
576 recentFilesCombo->setEnabled(!processRunning);
577 openModelAction->setEnabled(!processRunning);
578 openTextureAction->setEnabled(!processRunning);
579 openTextureDirAction->setEnabled(!processRunning);
580}
581
582void MainWindow::loadSettings() {
583 modelLineEdit->setText(settings->value("lastModel", "").toString());
584 textureLineEdit->setText(settings->value("lastTexture", "").toString());
585 textureDirLineEdit->setText(settings->value("lastTextureDir", "").toString());
586
587 executablePath = settings->value("executablePath", "").toString();
588
589 int resIndex = settings->value("resolutionIndex", 1).toInt();
590 if (resIndex >= 0 && resIndex < resolutionCombo->count()) {
591 resolutionCombo->setCurrentIndex(resIndex);
592 }
593
594 QSize size = settings->value("windowSize", QSize(1200, 800)).toSize();
595 resize(size);
596
597 QPoint pos = settings->value("windowPos", QPoint(100, 100)).toPoint();
598 move(pos);
599}
600
601void MainWindow::saveSettings() {
602 settings->setValue("lastModel", modelLineEdit->text());
603 settings->setValue("lastTexture", textureLineEdit->text());
604 settings->setValue("lastTextureDir", textureDirLineEdit->text());
605 settings->setValue("executablePath", executablePath);
606 settings->setValue("resolutionIndex", resolutionCombo->currentIndex());
607 settings->setValue("windowSize", size());
608 settings->setValue("windowPos", pos());
609 settings->setValue("recentModels", recentModels);
610}
611
612void MainWindow::loadRecentFiles() {
613 recentModels = settings->value("recentModels", QStringList()).toStringList();
614 recentFilesCombo->clear();
615 recentFilesCombo->addItem(tr("-- Select Recent --"));
616
617 for (const QString &path : recentModels) {
618 QFileInfo info(path);
619 if (info.exists()) {
620 recentFilesCombo->addItem(info.fileName(), path);
621 }
622 }
623}
624
625void MainWindow::updateRecentFiles(const QString &modelPath) {
626 recentModels.removeAll(modelPath);
627 recentModels.prepend(modelPath);
628
629 while (recentModels.size() > MAX_RECENT_FILES) {
630 recentModels.removeLast();
631 }
632
633 settings->setValue("recentModels", recentModels);
634 settings->sync();
635
636 loadRecentFiles();
637}
638
639void MainWindow::openRecentModel(int index) {
640 if (index > 0 && index <= recentModels.size()) {
641 QString modelPath = recentFilesCombo->itemData(index).toString();
642 if (QFileInfo(modelPath).exists()) {
643 modelLineEdit->setText(modelPath);
644 statusLabel->setText(tr("Loaded recent: %1")
645 .arg(QFileInfo(modelPath).fileName()));
646 } else {
647 QMessageBox::warning(this, tr("File Not Found"),
648 tr("The selected recent file no longer exists."));
649 recentModels.removeAll(modelPath);
650 loadRecentFiles();
651 }
652 }
653 recentFilesCombo->setCurrentIndex(0);
654}
655
656void MainWindow::removeRecentFile() {
657 if (recentFilesCombo->currentIndex() > 0) {
658 QString modelPath = recentFilesCombo->currentData().toString();
659 recentModels.removeAll(modelPath);
660 loadRecentFiles();
661 statusLabel->setText(tr("Removed from recent files"));
662 }
663}
664
665void MainWindow::showAboutDialog() {
666 QMessageBox::about(this, tr("MXMOD Model Viewer"),
667 tr("<h3>MXMOD Model Viewer v1.0</h3>"
668 "<p>A modern Qt-based model viewer application for .mxmod files.</p>"
669 "<p><b>Features:</b></p>"
670 "<ul>"
671 "<li>Drag and drop support</li>"
672 "<li>Recent files history</li>"
673 "<li>Multiple resolution options</li>"
674 "<li>Real-time console output</li>"
675 "</ul>"
676 "<p>© 2026 LostSideDead</p>"));
677}
678
679void MainWindow::showSettingsDialog() {
680 QDialog settingsDialog(this);
681 settingsDialog.setWindowTitle(tr("Settings"));
682 settingsDialog.setMinimumWidth(600);
683
684 QVBoxLayout *mainLayout = new QVBoxLayout(&settingsDialog);
685
686 QGroupBox *execGroup = new QGroupBox(tr("Executable Configuration"), &settingsDialog);
687 QVBoxLayout *execLayout = new QVBoxLayout(execGroup);
688
689 QLabel *infoLabel = new QLabel(tr("Specify a custom MXVK viewer path. Leave blank to auto-detect the viewer or use viewer from PATH."), &settingsDialog);
690 infoLabel->setWordWrap(true);
691 infoLabel->setStyleSheet("color: #aaa; font-size: 10pt;");
692 execLayout->addWidget(infoLabel);
693
694 execLayout->addSpacing(10);
695
696 QLabel *pathLabel = new QLabel(tr("Executable Path:"), &settingsDialog);
697 pathLabel->setStyleSheet("font-weight: bold;");
698 execLayout->addWidget(pathLabel);
699
700 QHBoxLayout *pathLayout = new QHBoxLayout();
701 QLineEdit *execPathEdit = new QLineEdit(&settingsDialog);
702 execPathEdit->setText(executablePath);
703 execPathEdit->setPlaceholderText(tr("viewer or /full/path/to/viewer"));
704
705 QPushButton *browseExecBtn = new QPushButton(tr("Browse..."), &settingsDialog);
706 browseExecBtn->setIcon(style()->standardIcon(QStyle::SP_DirIcon));
707
708 pathLayout->addWidget(execPathEdit, 1);
709 pathLayout->addWidget(browseExecBtn);
710 execLayout->addLayout(pathLayout);
711
712 QLabel *noteLabel = new QLabel(tr("Note: Leave blank for auto-detection. Use a full path if the executable is in a custom location."), &settingsDialog);
713 noteLabel->setWordWrap(true);
714 noteLabel->setStyleSheet("color: #888; font-size: 9pt; font-style: italic;");
715 execLayout->addWidget(noteLabel);
716
717 mainLayout->addWidget(execGroup);
718
719 QGroupBox *generalGroup = new QGroupBox(tr("General Settings"), &settingsDialog);
720 QVBoxLayout *generalLayout = new QVBoxLayout(generalGroup);
721
722 QLabel *recentLabel = new QLabel(tr("Maximum recent files: %1").arg(MAX_RECENT_FILES), &settingsDialog);
723 generalLayout->addWidget(recentLabel);
724
725 QPushButton *clearRecentBtn = new QPushButton(tr("Clear Recent Files History"), &settingsDialog);
726 generalLayout->addWidget(clearRecentBtn);
727
728 mainLayout->addWidget(generalGroup);
729
730 mainLayout->addStretch();
731
732 QHBoxLayout *buttonLayout = new QHBoxLayout();
733 buttonLayout->addStretch();
734
735 QPushButton *saveBtn = new QPushButton(tr("Save"), &settingsDialog);
736 saveBtn->setDefault(true);
737 saveBtn->setMinimumWidth(100);
738
739 QPushButton *cancelBtn = new QPushButton(tr("Cancel"), &settingsDialog);
740 cancelBtn->setMinimumWidth(100);
741
742 buttonLayout->addWidget(saveBtn);
743 buttonLayout->addWidget(cancelBtn);
744 mainLayout->addLayout(buttonLayout);
745
746 connect(browseExecBtn, &QPushButton::clicked, [this, execPathEdit]() {
747 QString fileName = QFileDialog::getOpenFileName(this,
748 tr("Select Viewer Executable"),
749 QFileInfo(execPathEdit->text()).absolutePath(),
750 tr("Executables (viewer*);;All Files (*)"));
751
752 if (!fileName.isEmpty()) {
753 execPathEdit->setText(fileName);
754 }
755 });
756
757 connect(clearRecentBtn, &QPushButton::clicked, [this, &settingsDialog]() {
758 auto reply = QMessageBox::question(&settingsDialog, tr("Clear Recent Files"),
759 tr("Are you sure you want to clear all recent files?"),
760 QMessageBox::Yes | QMessageBox::No);
761
762 if (reply == QMessageBox::Yes) {
763 recentModels.clear();
764 loadRecentFiles();
765 QMessageBox::information(&settingsDialog, tr("Cleared"),
766 tr("Recent files history has been cleared."));
767 }
768 });
769
770 connect(saveBtn, &QPushButton::clicked, [this, execPathEdit, &settingsDialog]() {
771 QString newPath = execPathEdit->text().trimmed();
772
773 if (newPath.isEmpty()) {
774 executablePath.clear();
775 saveSettings();
776 statusLabel->setText(tr("Settings saved successfully"));
777 settingsDialog.accept();
778 return;
779 }
780
781 if (newPath != "viewer" && !QFileInfo(newPath).isExecutable() && !QFileInfo(newPath).exists()) {
782 auto reply = QMessageBox::question(&settingsDialog, tr("File Not Found"),
783 tr("The specified file does not exist or is not executable. Save anyway?"),
784 QMessageBox::Yes | QMessageBox::No);
785
786 if (reply == QMessageBox::No) {
787 return;
788 }
789 }
790
791 executablePath = newPath;
792 saveSettings();
793 statusLabel->setText(tr("Settings saved successfully"));
794 settingsDialog.accept();
795 });
796
797 connect(cancelBtn, &QPushButton::clicked, &settingsDialog, &QDialog::reject);
798
799 settingsDialog.exec();
800}
void closeEvent(QCloseEvent *event) override
Confirms shutdown when the viewer process is still running.
~MainWindow() override
Persists settings and stops any running renderer process.
void dragEnterEvent(QDragEnterEvent *event) override
Accepts drag events that contain file or directory URLs.
MainWindow(QWidget *parent=nullptr)
Constructs the launcher window and restores persisted settings.
void dropEvent(QDropEvent *event) override
Assigns dropped model, texture, or directory paths to the matching input.
Declares the Qt front end used to launch the MXVK model viewer renderer.