8#include <QCoreApplication>
13#include <QInputDialog>
16 : QMainWindow(parent), activeProcess(nullptr) {
17 settings =
new QSettings(
"MXModel",
"ModelViewer",
this);
25 if (activeProcess && activeProcess->state() != QProcess::NotRunning) {
26 activeProcess->kill();
27 activeProcess->waitForFinished();
31void MainWindow::initWindow() {
32 setWindowTitle(
"MXMOD Model Viewer");
33 setGeometry(100, 100, 1200, 800);
34 setMinimumSize(900, 600);
45void MainWindow::setupMenuBar() {
46 QMenu *fileMenu = menuBar()->addMenu(tr(
"&File"));
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);
53 openTextureAction =
new QAction(tr(
"Open &Texture..."),
this);
54 openTextureAction->setStatusTip(tr(
"Open a texture file"));
55 fileMenu->addAction(openTextureAction);
57 openTextureDirAction =
new QAction(tr(
"Open Texture &Directory..."),
this);
58 openTextureDirAction->setStatusTip(tr(
"Open a texture directory"));
59 fileMenu->addAction(openTextureDirAction);
61 fileMenu->addSeparator();
63 clearConsoleAction =
new QAction(tr(
"&Clear Console"),
this);
64 clearConsoleAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_L));
65 fileMenu->addAction(clearConsoleAction);
67 fileMenu->addSeparator();
69 exitAction =
new QAction(tr(
"E&xit"),
this);
70 exitAction->setShortcut(QKeySequence::Quit);
71 fileMenu->addAction(exitAction);
73 QMenu *helpMenu = menuBar()->addMenu(tr(
"&Help"));
75 aboutAction =
new QAction(tr(
"&About"),
this);
76 helpMenu->addAction(aboutAction);
78 settingsAction =
new QAction(tr(
"&Settings"),
this);
79 settingsAction->setShortcut(QKeySequence::Preferences);
80 helpMenu->addAction(settingsAction);
83void MainWindow::setupToolBar() {
84 QToolBar *toolBar = addToolBar(tr(
"Main"));
85 toolBar->setMovable(
false);
86 toolBar->setIconSize(QSize(24, 24));
88 toolBar->addAction(openModelAction);
89 toolBar->addSeparator();
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);
96 stopButton =
new QPushButton(style()->standardIcon(QStyle::SP_MediaStop), tr(
"Stop"),
this);
97 stopButton->setMinimumHeight(32);
98 stopButton->setEnabled(
false);
99 toolBar->addWidget(stopButton);
101 toolBar->addSeparator();
103 clearButton =
new QPushButton(style()->standardIcon(QStyle::SP_DialogResetButton), tr(
"Clear"),
this);
104 clearButton->setMinimumHeight(32);
105 toolBar->addWidget(clearButton);
108void MainWindow::setupCentralWidget() {
109 QWidget *centralWidget =
new QWidget(
this);
110 setCentralWidget(centralWidget);
112 QVBoxLayout *mainLayout =
new QVBoxLayout(centralWidget);
113 mainLayout->setSpacing(10);
114 mainLayout->setContentsMargins(10, 10, 10, 10);
116 QGroupBox *fileGroup =
new QGroupBox(tr(
"Model and Texture Selection"),
this);
117 QVBoxLayout *fileLayout =
new QVBoxLayout(fileGroup);
119 QLabel *modelLabel =
new QLabel(tr(
"Model File:"),
this);
120 modelLabel->setStyleSheet(
"font-weight: bold;");
121 fileLayout->addWidget(modelLabel);
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);
131 QLabel *textureLabel =
new QLabel(tr(
"Texture File:"),
this);
132 textureLabel->setStyleSheet(
"font-weight: bold;");
133 fileLayout->addWidget(textureLabel);
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);
143 QLabel *dirLabel =
new QLabel(tr(
"Texture Directory:"),
this);
144 dirLabel->setStyleSheet(
"font-weight: bold;");
145 fileLayout->addWidget(dirLabel);
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);
155 mainLayout->addWidget(fileGroup);
157 QGroupBox *optionsGroup =
new QGroupBox(tr(
"Viewer Options"),
this);
158 QGridLayout *optionsLayout =
new QGridLayout(optionsGroup);
159 optionsLayout->setSpacing(8);
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);
170 QLabel *resLabel =
new QLabel(tr(
"Resolution:"),
this);
171 resolutionCombo =
new QComboBox(
this);
172 resolutionCombo->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
173 populateResolutionCombo();
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);
181 optionsLayout->setColumnStretch(1, 2);
182 optionsLayout->setColumnStretch(4, 1);
184 mainLayout->addWidget(optionsGroup);
186 QGroupBox *outputGroup =
new QGroupBox(tr(
"Process Output"),
this);
187 QVBoxLayout *outputLayout =
new QVBoxLayout(outputGroup);
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; }");
196 outputLayout->addWidget(consoleOutput);
197 mainLayout->addWidget(outputGroup, 1);
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);
205void MainWindow::setupStatusBar() {
206 statusLabel =
new QLabel(tr(
"Ready"),
this);
207 statusBar()->addWidget(statusLabel);
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);
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);
223 connect(recentFilesCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
224 this, &MainWindow::openRecentModel);
226 connect(modelLineEdit, &QLineEdit::textChanged,
this, &MainWindow::validatePaths);
227 connect(textureLineEdit, &QLineEdit::textChanged,
this, &MainWindow::validatePaths);
228 connect(textureDirLineEdit, &QLineEdit::textChanged,
this, &MainWindow::validatePaths);
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);
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);
245 if (reply == QMessageBox::Yes) {
246 activeProcess->kill();
247 activeProcess->waitForFinished(3000);
258 if (event->mimeData()->hasUrls()) {
259 event->acceptProposedAction();
264 const QMimeData *mimeData =
event->mimeData();
266 if (mimeData->hasUrls()) {
267 QList<QUrl> urls = mimeData->urls();
268 if (!urls.isEmpty()) {
269 QString filePath = urls.first().toLocalFile();
270 QFileInfo fileInfo(filePath);
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);
281 event->acceptProposedAction();
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 (*)"));
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()));
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 (*)"));
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()));
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);
318 if (!dirName.isEmpty()) {
319 textureDirLineEdit->setText(dirName);
320 settings->setValue(
"lastTextureDirPath", dirName);
321 statusLabel->setText(tr(
"Texture directory: %1").arg(QDir(dirName).dirName()));
325void MainWindow::validatePaths() {
326 QString modelPath = modelLineEdit->text();
327 QString texturePath = textureLineEdit->text();
328 QString textureDir = textureDirLineEdit->text();
330 bool valid = !modelPath.isEmpty();
333 valid = QFileInfo(modelPath).exists();
334 if (valid && !texturePath.isEmpty()) {
335 valid = QFileInfo(texturePath).exists();
337 if (valid && !textureDir.isEmpty()) {
338 valid = QDir(textureDir).exists();
342 openButton->setEnabled(valid && !activeProcess);
345QString MainWindow::resolveExecutablePath()
const {
346 const QString configuredPath = executablePath.trimmed();
347 if (!configuredPath.isEmpty() && configuredPath !=
"viewer") {
348 return configuredPath;
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"),
362 for (
const QString &candidate : candidates) {
363 const QFileInfo info(candidate);
364 if ((info.exists() && info.isExecutable()) || candidate ==
"viewer") {
365 return QDir::cleanPath(candidate);
372void MainWindow::openModelViewer() {
373 QString modelPath = modelLineEdit->text();
374 QString texturePath = textureLineEdit->text();
375 QString textureDir = textureDirLineEdit->text();
377 QFileInfo modelInfo(modelPath);
378 QFileInfo textureInfo(texturePath);
379 QDir dir(textureDir);
381 if (!modelInfo.exists() || !modelInfo.isFile()) {
382 QMessageBox::warning(
this, tr(
"Invalid Path"),
383 tr(
"The model file does not exist or is invalid."));
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."));
393 if (!textureDir.isEmpty() && !dir.exists()) {
394 QMessageBox::warning(
this, tr(
"Invalid Path"),
395 tr(
"The texture directory does not exist."));
399 activeProcess =
new QProcess(
this);
400 QProcess *process = activeProcess;
402 consoleOutput->clear();
403 consoleOutput->appendPlainText(QString(
"[%1] Starting model viewer...")
404 .arg(QDateTime::currentDateTime().toString(
"hh:mm:ss")));
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"))
415 QScrollBar *scrollBar = consoleOutput->verticalScrollBar();
416 scrollBar->setValue(scrollBar->maximum());
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"));
428 consoleOutput->setCurrentCharFormat(errorFormat);
429 consoleOutput->appendPlainText(QString(
"[%1] ERROR: %2")
430 .arg(QDateTime::currentDateTime().toString(
"hh:mm:ss"))
432 consoleOutput->setCurrentCharFormat(originalFormat);
434 QScrollBar *scrollBar = consoleOutput->verticalScrollBar();
435 scrollBar->setValue(scrollBar->maximum());
439 connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
440 [
this, process](
int exitCode, QProcess::ExitStatus) {
443 exitMsg = QString(
"[%1] Process exited with error code %2")
444 .arg(QDateTime::currentDateTime().toString(
"hh:mm:ss"))
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);
454 statusLabel->setText(tr(
"Viewer exited with error"));
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"));
462 if (activeProcess == process) {
463 activeProcess =
nullptr;
465 process->deleteLater();
466 updateUIState(
false);
469 connect(process, &QProcess::errorOccurred, [
this, process](QProcess::ProcessError error) {
472 case QProcess::FailedToStart:
473 errorMsg =
"Failed to start. Is the selected viewer executable installed or configured?";
475 case QProcess::Crashed:
476 errorMsg =
"Process crashed";
479 errorMsg = process->errorString();
482 QMessageBox::critical(
this, tr(
"Process Error"),
483 tr(
"Model viewer error: %1").arg(errorMsg));
485 consoleOutput->appendPlainText(QString(
"[%1] ERROR: %2")
486 .arg(QDateTime::currentDateTime().toString(
"hh:mm:ss"))
489 if (activeProcess == process) {
490 activeProcess =
nullptr;
492 process->deleteLater();
493 updateUIState(
false);
494 statusLabel->setText(tr(
"Error: %1").arg(errorMsg));
497 const QString selectedExecutable = resolveExecutablePath();
499 QStringList arguments;
500 arguments <<
"--filename" << modelPath;
501 if (!texturePath.isEmpty()) {
502 arguments <<
"--texture" << texturePath;
504 if (!textureDir.isEmpty()) {
505 arguments <<
"--resource_path" << textureDir;
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();
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,
'-'));
521 process->start(selectedExecutable, arguments);
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);
529 QMessageBox::critical(
this, tr(
"Startup Error"), errorMsg);
531 consoleOutput->appendPlainText(QString(
"[%1] FATAL: Failed to start process: %2")
532 .arg(QDateTime::currentDateTime().toString(
"hh:mm:ss"))
533 .arg(selectedExecutable));
535 activeProcess =
nullptr;
536 process->deleteLater();
537 statusLabel->setText(tr(
"Failed to start viewer"));
542 statusLabel->setText(tr(
"Viewer running..."));
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);
551 if (reply == QMessageBox::Yes) {
552 consoleOutput->appendPlainText(QString(
"[%1] Stopping process...")
553 .arg(QDateTime::currentDateTime().toString(
"hh:mm:ss")));
554 activeProcess->terminate();
556 if (!activeProcess->waitForFinished(2000)) {
557 activeProcess->kill();
559 statusLabel->setText(tr(
"Viewer stopped"));
564void MainWindow::clearConsole() {
565 consoleOutput->clear();
566 statusLabel->setText(tr(
"Console cleared"));
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);
582void MainWindow::loadSettings() {
583 modelLineEdit->setText(settings->value(
"lastModel",
"").toString());
584 textureLineEdit->setText(settings->value(
"lastTexture",
"").toString());
585 textureDirLineEdit->setText(settings->value(
"lastTextureDir",
"").toString());
587 executablePath = settings->value(
"executablePath",
"").toString();
589 int resIndex = settings->value(
"resolutionIndex", 1).toInt();
590 if (resIndex >= 0 && resIndex < resolutionCombo->count()) {
591 resolutionCombo->setCurrentIndex(resIndex);
594 QSize size = settings->value(
"windowSize", QSize(1200, 800)).toSize();
597 QPoint pos = settings->value(
"windowPos", QPoint(100, 100)).toPoint();
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);
612void MainWindow::loadRecentFiles() {
613 recentModels = settings->value(
"recentModels", QStringList()).toStringList();
614 recentFilesCombo->clear();
615 recentFilesCombo->addItem(tr(
"-- Select Recent --"));
617 for (
const QString &path : recentModels) {
618 QFileInfo info(path);
620 recentFilesCombo->addItem(info.fileName(), path);
625void MainWindow::updateRecentFiles(
const QString &modelPath) {
626 recentModels.removeAll(modelPath);
627 recentModels.prepend(modelPath);
629 while (recentModels.size() > MAX_RECENT_FILES) {
630 recentModels.removeLast();
633 settings->setValue(
"recentModels", recentModels);
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()));
647 QMessageBox::warning(
this, tr(
"File Not Found"),
648 tr(
"The selected recent file no longer exists."));
649 recentModels.removeAll(modelPath);
653 recentFilesCombo->setCurrentIndex(0);
656void MainWindow::removeRecentFile() {
657 if (recentFilesCombo->currentIndex() > 0) {
658 QString modelPath = recentFilesCombo->currentData().toString();
659 recentModels.removeAll(modelPath);
661 statusLabel->setText(tr(
"Removed from recent files"));
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>"
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>"
676 "<p>© 2026 LostSideDead</p>"));
679void MainWindow::showSettingsDialog() {
680 QDialog settingsDialog(
this);
681 settingsDialog.setWindowTitle(tr(
"Settings"));
682 settingsDialog.setMinimumWidth(600);
684 QVBoxLayout *mainLayout =
new QVBoxLayout(&settingsDialog);
686 QGroupBox *execGroup =
new QGroupBox(tr(
"Executable Configuration"), &settingsDialog);
687 QVBoxLayout *execLayout =
new QVBoxLayout(execGroup);
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);
694 execLayout->addSpacing(10);
696 QLabel *pathLabel =
new QLabel(tr(
"Executable Path:"), &settingsDialog);
697 pathLabel->setStyleSheet(
"font-weight: bold;");
698 execLayout->addWidget(pathLabel);
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"));
705 QPushButton *browseExecBtn =
new QPushButton(tr(
"Browse..."), &settingsDialog);
706 browseExecBtn->setIcon(style()->standardIcon(QStyle::SP_DirIcon));
708 pathLayout->addWidget(execPathEdit, 1);
709 pathLayout->addWidget(browseExecBtn);
710 execLayout->addLayout(pathLayout);
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);
717 mainLayout->addWidget(execGroup);
719 QGroupBox *generalGroup =
new QGroupBox(tr(
"General Settings"), &settingsDialog);
720 QVBoxLayout *generalLayout =
new QVBoxLayout(generalGroup);
722 QLabel *recentLabel =
new QLabel(tr(
"Maximum recent files: %1").arg(MAX_RECENT_FILES), &settingsDialog);
723 generalLayout->addWidget(recentLabel);
725 QPushButton *clearRecentBtn =
new QPushButton(tr(
"Clear Recent Files History"), &settingsDialog);
726 generalLayout->addWidget(clearRecentBtn);
728 mainLayout->addWidget(generalGroup);
730 mainLayout->addStretch();
732 QHBoxLayout *buttonLayout =
new QHBoxLayout();
733 buttonLayout->addStretch();
735 QPushButton *saveBtn =
new QPushButton(tr(
"Save"), &settingsDialog);
736 saveBtn->setDefault(
true);
737 saveBtn->setMinimumWidth(100);
739 QPushButton *cancelBtn =
new QPushButton(tr(
"Cancel"), &settingsDialog);
740 cancelBtn->setMinimumWidth(100);
742 buttonLayout->addWidget(saveBtn);
743 buttonLayout->addWidget(cancelBtn);
744 mainLayout->addLayout(buttonLayout);
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 (*)"));
752 if (!fileName.isEmpty()) {
753 execPathEdit->setText(fileName);
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);
762 if (reply == QMessageBox::Yes) {
763 recentModels.clear();
765 QMessageBox::information(&settingsDialog, tr(
"Cleared"),
766 tr(
"Recent files history has been cleared."));
770 connect(saveBtn, &QPushButton::clicked, [
this, execPathEdit, &settingsDialog]() {
771 QString newPath = execPathEdit->text().trimmed();
773 if (newPath.isEmpty()) {
774 executablePath.clear();
776 statusLabel->setText(tr(
"Settings saved successfully"));
777 settingsDialog.accept();
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);
786 if (reply == QMessageBox::No) {
791 executablePath = newPath;
793 statusLabel->setText(tr(
"Settings saved successfully"));
794 settingsDialog.accept();
797 connect(cancelBtn, &QPushButton::clicked, &settingsDialog, &QDialog::reject);
799 settingsDialog.exec();
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.