Files
86Box/src/qt/qt_vmmanager_addmachine.cpp
2025-07-01 11:33:18 +00:00

368 lines
13 KiB
C++

/*
* 86Box A hypervisor and IBM PC system emulator that specializes in
* running old operating systems and software designed for IBM
* PC systems and compatibles from 1981 through fairly recent
* system designs based on the PCI bus.
*
* This file is part of the 86Box distribution.
*
* 86Box VM manager add machine wizard
*
*
*
* Authors: cold-brewed
*
* Copyright 2024 cold-brewed
*/
#include <QApplication>
#include <QDebug>
#include <QFileDialog>
#include <QMessageBox>
#include <QPushButton>
#include <QStyle>
#include <QVBoxLayout>
#include "qt_vmmanager_addmachine.hpp"
extern "C" {
#include <86box/86box.h>
}
// Implementation note: There are several classes in this file:
// One for the main Wizard class and one for each page of the wizard
VMManagerAddMachine::
VMManagerAddMachine(QWidget *parent) : QWizard(parent)
{
setPage(Page_Intro, new IntroPage);
setPage(Page_WithExistingConfig, new WithExistingConfigPage);
setPage(Page_NameAndLocation, new NameAndLocationPage);
setPage(Page_Conclusion, new ConclusionPage);
// Need to create a better image
// QPixmap originalPixmap(":/assets/86box.png");
// QPixmap scaledPixmap = originalPixmap.scaled(150, 150, Qt::KeepAspectRatio);
QPixmap wizardPixmap(":/assets/86box-wizard.png");
#ifndef Q_OS_MACOS
setWizardStyle(ModernStyle);
// setPixmap(LogoPixmap, scaledPixmap);
// setPixmap(LogoPixmap, wizardPixmap);
// setPixmap(WatermarkPixmap, scaledPixmap);
setPixmap(WatermarkPixmap, wizardPixmap);
#else
// macos
// setPixmap(BackgroundPixmap, scaledPixmap);
setPixmap(BackgroundPixmap, wizardPixmap);
#endif
// Wizard wants to resize based on image. This keeps the size
setMinimumSize(size());
setOption(HaveHelpButton, true);
// setPixmap(LogoPixmap, QPixmap(":/settings/qt/icons/86Box-gray.ico"));
connect(this, &QWizard::helpRequested, this, &VMManagerAddMachine::showHelp);
setWindowTitle(tr("Add new system wizard"));
}
void
VMManagerAddMachine::showHelp()
{
// TBD
static QString lastHelpMessage;
QString message;
// Help will depend on the current page
switch (currentId()) {
case Page_Intro:
message = tr("This is the into page.");
break;
default:
message = tr("No help has been added yet, you're on your own.");
break;
}
if (lastHelpMessage == message) {
message = tr("Did you click help twice?");
}
QMessageBox::information(this, tr("Add new system wizard help"), message);
lastHelpMessage = message;
}
IntroPage::
IntroPage(QWidget *parent)
{
setTitle(tr("Introduction"));
setPixmap(QWizard::WatermarkPixmap, QPixmap(":/assets/qt/assets/86box.png"));
topLabel = new QLabel(tr("This will help you add a new system to 86Box."));
// topLabel = new QLabel(tr("This will help you add a new system to 86Box.\n\n Choose \"New configuration\" if you'd like to create a new machine.\n\nChoose \"Use existing configuration\" if you'd like to paste in an existing configuration from elsewhere."));
topLabel->setWordWrap(true);
newConfigRadioButton = new QRadioButton(tr("New configuration"));
// auto newDescription = new QLabel(tr("Choose this option to start with a fresh configuration."));
existingConfigRadioButton = new QRadioButton(tr("Use existing configuraion"));
// auto existingDescription = new QLabel(tr("Use this option if you'd like to paste in the configuration file from an existing system."));
newConfigRadioButton->setChecked(true);
const auto layout = new QVBoxLayout();
layout->addWidget(topLabel);
layout->addWidget(newConfigRadioButton);
// layout->addWidget(newDescription);
layout->addWidget(existingConfigRadioButton);
// layout->addWidget(existingDescription);
setLayout(layout);
}
int
IntroPage::nextId() const
{
if (newConfigRadioButton->isChecked()) {
return VMManagerAddMachine::Page_NameAndLocation;
} else {
return VMManagerAddMachine::Page_WithExistingConfig;
}
}
WithExistingConfigPage::
WithExistingConfigPage(QWidget *parent)
{
setTitle(tr("Use existing configuration"));
const auto topLabel = new QLabel(tr("Paste the contents of the existing configuration file into the box below."));
topLabel->setWordWrap(true);
existingConfiguration = new QPlainTextEdit();
connect(existingConfiguration, &QPlainTextEdit::textChanged, this, &WithExistingConfigPage::completeChanged);
registerField("existingConfiguration*", this, "configuration");
const auto layout = new QVBoxLayout();
layout->addWidget(topLabel);
layout->addWidget(existingConfiguration);
const auto loadFileButton = new QPushButton();
const auto loadFileLabel = new QLabel(tr("Load configuration from file"));
const auto hLayout = new QHBoxLayout();
loadFileButton->setIcon(QApplication::style()->standardIcon(QStyle::SP_FileIcon));
loadFileButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
connect(loadFileButton, &QPushButton::clicked, this, &WithExistingConfigPage::chooseExistingConfigFile);
hLayout->addWidget(loadFileButton);
hLayout->addWidget(loadFileLabel);
layout->addLayout(hLayout);
setLayout(layout);
}
void
WithExistingConfigPage::chooseExistingConfigFile()
{
// TODO: FIXME: This is using the CLI arg and needs to instead use a proper variable
const auto startDirectory = QString(vmm_path);
const auto selectedConfigFile = QFileDialog::getOpenFileName(this, tr("Choose configuration file"),
startDirectory,
tr("86Box configuration files (86box.cfg)"));
// Empty value means the dialog was canceled
if (!selectedConfigFile.isEmpty()) {
QFile configFile(selectedConfigFile);
if (!configFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QMessageBox::critical(this, tr("Configuration read failed"), tr("Unable to open the selected configuration file for reading: %1").arg(configFile.errorString()));
return;
}
const QString configFileContents = configFile.readAll();
existingConfiguration->setPlainText(configFileContents);
configFile.close();
emit completeChanged();
}
}
QString
WithExistingConfigPage::configuration() const
{
return existingConfiguration->toPlainText();
}
void
WithExistingConfigPage::setConfiguration(const QString &configuration)
{
if (configuration != existingConfiguration->toPlainText()) {
existingConfiguration->setPlainText(configuration);
emit configurationChanged(configuration);
}
}
int
WithExistingConfigPage::nextId() const
{
return VMManagerAddMachine::Page_NameAndLocation;
}
bool
WithExistingConfigPage::isComplete() const
{
return !existingConfiguration->toPlainText().isEmpty();
}
NameAndLocationPage::
NameAndLocationPage(QWidget *parent)
{
setTitle(tr("System name and location"));
#if defined(_WIN32)
dirValidate = QRegularExpression(R"(^[^\\/:*?"<>|]+$)");
#elif defined(__APPLE__)
dirValidate = QRegularExpression(R"(^[^/:]+$)");
#else
dirValidate = QRegularExpression(R"(^[^/]+$)");
#endif
const auto topLabel = new QLabel(tr("Enter the name of the system and choose the location"));
topLabel->setWordWrap(true);
const auto chooseDirectoryButton = new QPushButton();
chooseDirectoryButton->setIcon(QApplication::style()->standardIcon(QStyle::SP_DirIcon));
const auto systemNameLabel = new QLabel(tr("System Name"));
systemName = new QLineEdit();
// Special event filter to override enter key
systemName->installEventFilter(this);
registerField("systemName*", systemName);
systemNameValidation = new QLabel();
const auto systemLocationLabel = new QLabel(tr("System Location"));
systemLocation = new QLineEdit();
// TODO: FIXME: This is using the CLI arg and needs to instead use a proper variable
systemLocation->setText(QDir::toNativeSeparators(vmm_path));
registerField("systemLocation*", systemLocation);
systemLocationValidation = new QLabel();
systemLocationValidation->setWordWrap(true);
const auto layout = new QGridLayout();
layout->addWidget(topLabel, 0, 0, 1, -1);
// Spacer row
layout->setRowMinimumHeight(1, 20);
layout->addWidget(systemNameLabel, 2, 0);
layout->addWidget(systemName, 2, 1);
// Validation text, appears only as necessary
layout->addWidget(systemNameValidation, 3, 0, 1, -1);
// Set height on validation because it may not always be present
layout->setRowMinimumHeight(3, 20);
// Another spacer
layout->setRowMinimumHeight(4, 20);
layout->addWidget(systemLocationLabel, 5, 0);
layout->addWidget(systemLocation, 5, 1);
layout->addWidget(chooseDirectoryButton, 5, 2);
// Validation text
layout->addWidget(systemLocationValidation, 6, 0, 1, -1);
layout->setRowMinimumHeight(6, 20);
setLayout(layout);
connect(chooseDirectoryButton, &QPushButton::clicked, this, &NameAndLocationPage::chooseDirectoryLocation);
}
int
NameAndLocationPage::nextId() const
{
return VMManagerAddMachine::Page_Conclusion;
}
void
NameAndLocationPage::chooseDirectoryLocation()
{
// TODO: FIXME: This is pulling in the CLI directory! Needs to be set properly elsewhere
const auto directory = QFileDialog::getExistingDirectory(this, "Choose directory", QDir(vmm_path).path());
systemLocation->setText(QDir::toNativeSeparators(directory));
emit completeChanged();
}
bool
NameAndLocationPage::isComplete() const
{
bool nameValid = false;
bool locationValid = false;
// return true if complete
if (systemName->text().isEmpty()) {
systemNameValidation->setText(tr("Please enter a system name"));
} else if (!systemName->text().contains(dirValidate)) {
systemNameValidation->setText(tr("System name cannot contain certain characters"));
} else if (const QDir newDir = QDir::cleanPath(systemLocation->text() + "/" + systemName->text()); newDir.exists()) {
systemNameValidation->setText(tr("System name already exists"));
} else {
systemNameValidation->clear();
nameValid = true;
}
if (systemLocation->text().isEmpty()) {
systemLocationValidation->setText(tr("Please enter a directory for the system"));
} else if (const auto dir = QDir(systemLocation->text()); !dir.exists()) {
systemLocationValidation->setText(tr("Directory does not exist"));
} else {
systemLocationValidation->setText("A new directory for the system will be created in the selected directory above");
locationValid = true;
}
return nameValid && locationValid;
}
bool
NameAndLocationPage::eventFilter(QObject *watched, QEvent *event)
{
// Override the enter key to hit the next wizard button
// if the validator (isComplete) is satisfied
if (event->type() == QEvent::KeyPress) {
const auto keyEvent = dynamic_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Enter || keyEvent->key() == Qt::Key_Return) {
// Only advance if the validator is satisfied (isComplete)
if(const auto wizard = qobject_cast<QWizard*>(this->wizard())) {
if (wizard->currentPage()->isComplete()) {
wizard->next();
}
}
// Discard the key event
return true;
}
}
return QWizardPage::eventFilter(watched, event);
}
ConclusionPage::
ConclusionPage(QWidget *parent)
{
setTitle(tr("Complete"));
topLabel = new QLabel(tr("The wizard will now launch the configuration for the new system."));
topLabel->setWordWrap(true);
const auto systemNameLabel = new QLabel(tr("System name:"));
systemNameLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
systemName = new QLabel();
const auto systemLocationLabel = new QLabel(tr("System location:"));
systemLocationLabel->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
systemLocation = new QLabel();
const auto layout = new QGridLayout();
layout->addWidget(topLabel, 0, 0, 1, -1);
layout->setRowMinimumHeight(1, 20);
layout->addWidget(systemNameLabel, 2, 0);
layout->addWidget(systemName, 2, 1);
layout->addWidget(systemLocationLabel, 3, 0);
layout->addWidget(systemLocation, 3, 1);
setLayout(layout);
}
// initializePage() runs after the page has been created with the constructor
void
ConclusionPage::initializePage()
{
const auto finalPath = QDir::cleanPath(field("systemLocation").toString() + "/" + field("systemName").toString());
const auto nativePath = QDir::toNativeSeparators(finalPath);
const auto systemNameDisplay = field("systemName").toString();
systemName->setText(systemNameDisplay);
systemLocation->setText(nativePath);
}