Files
86Box/src/qt/qt_vmmanager_main.cpp
Nelson Kerber Hennemann Filho 00677015b7 Fix untranslated string
2025-09-13 19:04:12 -03:00

955 lines
39 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 main module
*
*
*
* Authors: cold-brewed
*
* Copyright 2024 cold-brewed
*/
#include <QDirIterator>
#include <QLabel>
#include <QAbstractListModel>
#include <QCompleter>
#include <QDebug>
#include <QDesktopServices>
#include <QMenu>
#include <QMessageBox>
#include <QStringListModel>
#include <QTimer>
#include <QProgressDialog>
#include <thread>
#include <atomic>
#include "qt_vmmanager_main.hpp"
#include "qt_vmmanager_mainwindow.hpp"
#include "ui_qt_vmmanager_main.h"
#include "qt_vmmanager_model.hpp"
#include "qt_vmmanager_addmachine.hpp"
extern VMManagerMainWindow* vmm_main_window;
// https://stackoverflow.com/a/36460740
bool copyPath(QString sourceDir, QString destinationDir, bool overWriteDirectory)
{
QDir originDirectory(sourceDir);
if (! originDirectory.exists())
{
return false;
}
QDir destinationDirectory(destinationDir);
if(destinationDirectory.exists() && !overWriteDirectory)
{
return false;
}
else if(destinationDirectory.exists() && overWriteDirectory)
{
destinationDirectory.removeRecursively();
}
originDirectory.mkpath(destinationDir);
foreach (QString directoryName, originDirectory.entryList(QDir::Dirs | \
QDir::NoDotAndDotDot))
{
QString destinationPath = destinationDir + "/" + directoryName;
originDirectory.mkpath(destinationPath);
copyPath(sourceDir + "/" + directoryName, destinationPath, overWriteDirectory);
}
foreach (QString fileName, originDirectory.entryList(QDir::Files))
{
QFile::copy(sourceDir + "/" + fileName, destinationDir + "/" + fileName);
}
/*! Possible race-condition mitigation? */
QDir finalDestination(destinationDir);
finalDestination.refresh();
if(finalDestination.exists())
{
return true;
}
return false;
}
VMManagerMain::VMManagerMain(QWidget *parent) :
QWidget(parent), ui(new Ui::VMManagerMain), selected_sysconfig(new VMManagerSystem) {
ui->setupUi(this);
// Set up the main listView
ui->listView->setItemDelegate(new VMManagerListViewDelegate);
vm_model = new VMManagerModel;
proxy_model = new StringListProxyModel(this);
proxy_model->setSourceModel(vm_model);
ui->listView->setModel(proxy_model);
proxy_model->setSortCaseSensitivity(Qt::CaseInsensitive);
ui->listView->model()->sort(0, Qt::AscendingOrder);
// Connect the model signal
connect(vm_model, &VMManagerModel::systemDataChanged, this, &VMManagerMain::modelDataChange);
// Set up the context menu for the list view
ui->listView->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->listView, &QListView::customContextMenuRequested, [this, parent](const QPoint &pos) {
const auto indexAt = ui->listView->indexAt(pos);
if (indexAt.isValid()) {
QMenu contextMenu(tr("Context Menu"), ui->listView);
QAction startAction(tr("&Start"));
contextMenu.addAction(&startAction);
connect(&startAction, &QAction::triggered, [this] {
selected_sysconfig->startButtonPressed();
});
startAction.setEnabled(selected_sysconfig->process->state() == QProcess::NotRunning);
startAction.setVisible(selected_sysconfig->process->state() == QProcess::NotRunning);
QAction pauseAction(tr("&Pause"));
contextMenu.addAction(&pauseAction);
connect(&pauseAction, &QAction::triggered, [this] {
selected_sysconfig->pauseButtonPressed();
});
pauseAction.setEnabled(selected_sysconfig->process->state() == QProcess::Running);
pauseAction.setVisible(selected_sysconfig->process->state() == QProcess::Running);
if (selected_sysconfig->getProcessStatus() != VMManagerSystem::ProcessStatus::Running)
pauseAction.setText(tr("Re&sume"));
QAction resetAction(tr("&Hard reset"));
contextMenu.addAction(&resetAction);
connect(&resetAction, &QAction::triggered, [this] {
selected_sysconfig->restartButtonPressed();
});
resetAction.setEnabled(selected_sysconfig->process->state() == QProcess::Running);
QAction forceShutdownAction(tr("&Force shutdown"));
contextMenu.addAction(&forceShutdownAction);
connect(&forceShutdownAction, &QAction::triggered, [this] {
selected_sysconfig->shutdownForceButtonPressed();
});
forceShutdownAction.setEnabled(selected_sysconfig->process->state() == QProcess::Running);
QAction cadAction(tr("&Ctrl+Alt+Del"));
contextMenu.addAction(&cadAction);
connect(&cadAction, &QAction::triggered, [this] {
selected_sysconfig->cadButtonPressed();
});
cadAction.setEnabled(selected_sysconfig->process->state() == QProcess::Running);
contextMenu.addSeparator();
QAction settingsAction(tr("&Settings..."));
contextMenu.addAction(&settingsAction);
connect(&settingsAction, &QAction::triggered, [this] {
selected_sysconfig->launchSettings();
});
QAction nameChangeAction(tr("Change &display name..."));
contextMenu.addAction(&nameChangeAction);
// Use a lambda to call a function so indexAt can be passed
connect(&nameChangeAction, &QAction::triggered, ui->listView, [this, indexAt] {
updateDisplayName(indexAt);
});
nameChangeAction.setEnabled(!selected_sysconfig->window_obscured);
QAction setSystemIcon(tr("Set &icon..."));
contextMenu.addAction(&setSystemIcon);
connect(&setSystemIcon, &QAction::triggered, [this] {
IconSelectionDialog dialog(":/systemicons/");
if(dialog.exec() == QDialog::Accepted) {
const QString iconName = dialog.getSelectedIconName();
// A Blank iconName will cause setIcon to reset to the default
selected_sysconfig->setIcon(iconName);
}
});
setSystemIcon.setEnabled(!selected_sysconfig->window_obscured);
contextMenu.addSeparator();
QAction cloneMachine(tr("C&lone..."));
contextMenu.addAction(&cloneMachine);
connect(&cloneMachine, &QAction::triggered, [this] {
QDialog dialog = QDialog(this);
auto layout = new QVBoxLayout(&dialog);
layout->setSizeConstraint(QLayout::SetFixedSize);
layout->addWidget(new QLabel(tr("Virtual machine \"%1\" (%2) will be cloned into:").arg(selected_sysconfig->displayName, selected_sysconfig->config_dir)));
QLineEdit* edit = new QLineEdit(&dialog);
layout->addWidget(edit);
QLabel* errLabel = new QLabel(&dialog);
layout->addWidget(errLabel);
errLabel->setVisible(false);
QDialogButtonBox* buttonBox = new QDialogButtonBox(&dialog);
buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true);
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
layout->addWidget(buttonBox);
connect(edit, &QLineEdit::textChanged, this, [errLabel, buttonBox] (const QString& text) {
bool isSpaceOnly = true;
#ifdef Q_OS_WINDOWS
const char illegalChars[] = "<>:\"|?*\\/";
#else
const char illegalChars[] = "\\/";
#endif
for (const auto& curChar : text) {
for (size_t i = 0; i < sizeof(illegalChars) - 1; i++) {
if (illegalChars[i] == curChar) {
goto illegal_chars;
}
if (!curChar.isSpace()) {
isSpaceOnly = false;
}
}
}
errLabel->setVisible(false);
buttonBox->button(QDialogButtonBox::Ok)->setDisabled(isSpaceOnly || text.isEmpty());
if (QDir((QString(vmm_path) + "/") + text).exists() && buttonBox->button(QDialogButtonBox::Ok)->isEnabled()) {
goto dir_already_exists;
}
return;
dir_already_exists:
errLabel->setText(tr("Directory %1 already exists").arg(QDir((QString(vmm_path) + "/") + text).canonicalPath()));
errLabel->setVisible(true);
buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true);
return;
illegal_chars:
QString illegalCharsDisplay;
for (size_t i = 0; i < sizeof(illegalChars) - 1; i++) {
illegalCharsDisplay.push_back(illegalChars[i]);
illegalCharsDisplay.push_back(' ');
}
illegalCharsDisplay.chop(1);
errLabel->setText(tr("You cannot use the following characters in the name: %1").arg(illegalCharsDisplay));
errLabel->setVisible(true);
buttonBox->button(QDialogButtonBox::Ok)->setDisabled(true);
return;
});
if (dialog.exec() > 0) {
std::atomic_bool finished{false};
std::atomic_bool errCode;
auto vmDir = QDir(vmm_path).canonicalPath();
vmDir.append("/");
vmDir.append(edit->text());
vmDir.append("/");
if (!QDir(vmDir).mkpath(".")) {
QMessageBox::critical(this, tr("Clone"), tr("Failed to create directory for cloned VM"), QMessageBox::Ok);
return;
}
QProgressDialog* progDialog = new QProgressDialog(this);
progDialog->setMaximum(0);
progDialog->setMinimum(0);
progDialog->setWindowFlags(progDialog->windowFlags() & ~Qt::WindowCloseButtonHint);
progDialog->setMinimumSize(progDialog->sizeHint());
progDialog->setMaximumSize(progDialog->sizeHint());
progDialog->setMinimumDuration(0);
progDialog->setCancelButton(nullptr);
progDialog->setAutoClose(false);
progDialog->setAutoReset(false);
progDialog->setAttribute(Qt::WA_DeleteOnClose, true);
progDialog->setValue(0);
progDialog->setWindowTitle(tr("Clone"));
progDialog->show();
QString srcPath = selected_sysconfig->config_dir;
QString dstPath = vmDir;
std::thread copyThread([&finished, srcPath, dstPath, &errCode] {
errCode = copyPath(srcPath, dstPath, true);
finished = true;
});
while (!finished) {
QApplication::processEvents();
}
copyThread.join();
progDialog->close();
if (!errCode) {
QDir(dstPath).removeRecursively();
QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM."), QMessageBox::Ok);
return;
}
QFileInfo configFileInfo(vmDir + CONFIG_FILE);
if (configFileInfo.exists()) {
const auto current_index = ui->listView->currentIndex();
vm_model->reload(this);
const auto created_object = vm_model->getIndexForConfigFile(configFileInfo);
if (created_object.row() < 0) {
// For some reason the index of the new object couldn't be determined. Fall back to the old index.
ui->listView->setCurrentIndex(current_index);
return;
}
auto added_system = vm_model->getConfigObjectForIndex(created_object);
added_system->setDisplayName(edit->text());
// Get the index of the newly-created system and select it
const QModelIndex mapped_index = proxy_model->mapFromSource(created_object);
ui->listView->setCurrentIndex(mapped_index);
} else {
QDir(dstPath).removeRecursively();
QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM."), QMessageBox::Ok);
return;
}
}
});
QAction killIcon(tr("&Kill"));
contextMenu.addAction(&killIcon);
connect(&killIcon, &QAction::triggered, [this, parent] {
QMessageBox msgbox(QMessageBox::Warning, tr("Warning"), tr("Killing a virtual machine can cause data loss. Only do this if the 86Box process gets stuck.\n\nDo you really wish to kill the virtual machine \"%1\"?").arg(selected_sysconfig->displayName), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, parent);
msgbox.exec();
if (msgbox.result() == QMessageBox::Yes) {
disconnect(selected_sysconfig->process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), nullptr, nullptr);
selected_sysconfig->process->kill();
}
});
killIcon.setEnabled(selected_sysconfig->process->state() == QProcess::Running);
QAction clrNvram(tr("&Wipe NVRAM"));
contextMenu.addAction(&clrNvram);
connect(&clrNvram, &QAction::triggered, [this, parent] {
QMessageBox msgbox(QMessageBox::Warning, tr("Warning"), tr("This will delete all NVRAM (and related) files of the virtual machine located in the \"nvr\" subdirectory. You'll have to reconfigure the BIOS (and possibly other devices inside the VM) settings again if applicable.\n\nAre you sure you want to wipe all NVRAM contents of the virtual machine \"%1\"?").arg(selected_sysconfig->displayName), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, parent);
msgbox.exec();
if (msgbox.result() == QMessageBox::Yes) {
if (QDir(selected_sysconfig->config_dir + "/nvr/").removeRecursively())
QMessageBox::information(this, tr("Success"), tr("Successfully wiped the NVRAM contents of the virtual machine \"%1\"").arg(selected_sysconfig->displayName));
else {
QMessageBox::critical(this, tr("Error"), tr("An error occurred trying to wipe the NVRAM contents of the virtual machine \"%1\"").arg(selected_sysconfig->displayName));
}
}
});
clrNvram.setEnabled(selected_sysconfig->process->state() == QProcess::NotRunning);
QAction deleteAction(tr("&Delete"));
contextMenu.addAction(&deleteAction);
connect(&deleteAction, &QAction::triggered, [this] {
deleteSystem(selected_sysconfig);
});
deleteAction.setEnabled(selected_sysconfig->process->state() == QProcess::NotRunning);
contextMenu.addSeparator();
QAction openSystemFolderAction(tr("&Open folder..."));
contextMenu.addAction(&openSystemFolderAction);
connect(&openSystemFolderAction, &QAction::triggered, [indexAt] {
if (const auto configDir = indexAt.data(VMManagerModel::Roles::ConfigDir).toString(); !configDir.isEmpty()) {
QDir dir(configDir);
if (!dir.exists())
dir.mkpath(".");
QDesktopServices::openUrl(QUrl(QString("file:///") + dir.canonicalPath()));
}
});
QAction openPrinterFolderAction(tr("Open p&rinter tray..."));
contextMenu.addAction(&openPrinterFolderAction);
connect(&openPrinterFolderAction, &QAction::triggered, [indexAt] {
if (const auto printerDir = indexAt.data(VMManagerModel::Roles::ConfigDir).toString() + QString("/printer/"); !printerDir.isEmpty()) {
QDir dir(printerDir);
if (!dir.exists())
dir.mkpath(".");
QDesktopServices::openUrl(QUrl(QString("file:///") + dir.canonicalPath()));
}
});
QAction openScreenshotsFolderAction(tr("Open screenshots &folder..."));
contextMenu.addAction(&openScreenshotsFolderAction);
connect(&openScreenshotsFolderAction, &QAction::triggered, [indexAt] {
if (const auto screenshotsDir = indexAt.data(VMManagerModel::Roles::ConfigDir).toString() + QString("/screenshots/"); !screenshotsDir.isEmpty()) {
QDir dir(screenshotsDir);
if (!dir.exists())
dir.mkpath(".");
QDesktopServices::openUrl(QUrl(QString("file:///") + dir.canonicalPath()));
}
});
QAction showRawConfigFile(tr("Show &config file"));
contextMenu.addAction(&showRawConfigFile);
connect(&showRawConfigFile, &QAction::triggered, [this, indexAt] {
if (const auto configFile = indexAt.data(VMManagerModel::Roles::ConfigFile).toString(); !configFile.isEmpty()) {
showTextFileContents(indexAt.data(Qt::DisplayRole).toString(), configFile);
}
});
contextMenu.exec(ui->listView->viewport()->mapToGlobal(pos));
} else {
QMenu contextMenu(tr("Context Menu"), ui->listView);
QAction newMachineAction(tr("&New machine..."));
contextMenu.addAction(&newMachineAction);
connect(&newMachineAction, &QAction::triggered, this, &VMManagerMain::newMachineWizard);
contextMenu.exec(ui->listView->viewport()->mapToGlobal(pos));
}
});
connect(vm_model, &VMManagerModel::globalConfigurationChanged, this, [] () {
vmm_main_window->updateSettings();
});
// Initial default details view
vm_details = new VMManagerDetails(ui->detailsArea);
ui->detailsArea->layout()->addWidget(vm_details);
const QItemSelectionModel *selection_model = ui->listView->selectionModel();
connect(selection_model, &QItemSelectionModel::currentChanged, this, &VMManagerMain::currentSelectionChanged);
// If there are items in the model, make sure to select the first item by default.
// When settings are loaded, the last selected item will be selected (if available)
if (proxy_model->rowCount(QModelIndex()) > 0) {
const QModelIndex first_index = proxy_model->index(0, 0);
ui->listView->setCurrentIndex(first_index);
}
connect(ui->listView, &QListView::doubleClicked, this, &VMManagerMain::startButtonPressed);
// Load and apply settings
loadSettings();
ui->splitter->setSizes({ui->detailsArea->width(), (ui->listView->minimumWidth() * 2)});
// Set up search bar
connect(ui->searchBar, &QLineEdit::textChanged, this, &VMManagerMain::searchSystems);
// Create the completer
auto *completer = new QCompleter(this);
completer->setCaseSensitivity(Qt::CaseInsensitive);
completer->setFilterMode(Qt::MatchContains);
// Get the completer list
const auto allStrings = getSearchCompletionList();
// Set up the completer
auto *completerModel = new QStringListModel(allStrings, completer);
completer->setModel(completerModel);
ui->searchBar->setCompleter(completer);
// Set initial status bar after the event loop starts
QTimer::singleShot(0, this, [this] {
emit updateStatusRight(machineCountString());
});
#if EMU_BUILD_NUM != 0
// Start update check after a slight delay
QTimer::singleShot(1000, this, [this] {
if(updateCheck) {
backgroundUpdateCheckStart();
}
});
#endif
}
VMManagerMain::~VMManagerMain() {
delete ui;
delete vm_model;
}
void
VMManagerMain::updateGlobalSettings()
{
vmm_main_window->updateSettings();
}
void
VMManagerMain::currentSelectionChanged(const QModelIndex &current,
const QModelIndex &previous)
{
if(!current.isValid()) {
return;
}
/* hack to prevent strange segfaults when adding a machine after
removing all machines previously */
if (selected_sysconfig->config_signal_connected == true) {
disconnect(selected_sysconfig, &VMManagerSystem::configurationChanged, this, &VMManagerMain::onConfigUpdated);
selected_sysconfig->config_signal_connected = false;
}
const auto mapped_index = proxy_model->mapToSource(current);
selected_sysconfig = vm_model->getConfigObjectForIndex(mapped_index);
vm_details->updateData(selected_sysconfig);
if (selected_sysconfig->config_signal_connected == false) {
connect(selected_sysconfig, &VMManagerSystem::configurationChanged, this, &VMManagerMain::onConfigUpdated);
selected_sysconfig->config_signal_connected = true;
}
// Emit that the selection changed, include with the process state
emit selectionChanged(current, selected_sysconfig->process->state());
}
void
VMManagerMain::settingsButtonPressed() {
if(!currentSelectionIsValid()) {
return;
}
selected_sysconfig->launchSettings();
}
void
VMManagerMain::startButtonPressed() const
{
if(!currentSelectionIsValid()) {
return;
}
selected_sysconfig->startButtonPressed();
}
void
VMManagerMain::restartButtonPressed() const
{
if(!currentSelectionIsValid()) {
return;
}
selected_sysconfig->restartButtonPressed();
}
void
VMManagerMain::pauseButtonPressed() const
{
if(!currentSelectionIsValid()) {
return;
}
selected_sysconfig->pauseButtonPressed();
}
void
VMManagerMain::shutdownRequestButtonPressed() const
{
if (!currentSelectionIsValid()) {
return;
}
selected_sysconfig->shutdownRequestButtonPressed();
}
void
VMManagerMain::shutdownForceButtonPressed() const
{
if (!currentSelectionIsValid()) {
return;
}
selected_sysconfig->shutdownForceButtonPressed();
}
// This function doesn't appear to be needed any longer
void
VMManagerMain::refresh()
{
const auto current_index = ui->listView->currentIndex();
emit selectionChanged(current_index, selected_sysconfig->process->state());
// if(!selected_sysconfig->config_file.path().isEmpty()) {
if(!selected_sysconfig->isValid()) {
// what was happening here?
}
}
void
VMManagerMain::updateDisplayName(const QModelIndex &index)
{
QDialog dialog;
dialog.setMinimumWidth(400);
dialog.setWindowTitle(tr("Set display name"));
const auto layout = new QVBoxLayout(&dialog);
const auto label = new QLabel(tr("Enter the new display name (blank to reset)"));
label->setAlignment(Qt::AlignHCenter);
label->setContentsMargins(QMargins(0, 0, 0, 5));
layout->addWidget(label);
const auto lineEdit = new QLineEdit(index.data().toString(), &dialog);
layout->addWidget(lineEdit);
lineEdit->selectAll();
const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
layout->addWidget(buttonBox);
connect(buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (const bool accepted = dialog.exec() == QDialog::Accepted; accepted) {
const auto mapped_index = proxy_model->mapToSource(index);
vm_model->updateDisplayName(mapped_index, lineEdit->text());
selected_sysconfig = vm_model->getConfigObjectForIndex(mapped_index);
vm_details->updateData(selected_sysconfig);
ui->listView->scrollTo(ui->listView->currentIndex(), QAbstractItemView::PositionAtCenter);
}
}
void
VMManagerMain::loadSettings()
{
const auto config = new VMManagerConfig(VMManagerConfig::ConfigType::General);
const auto lastSelection = config->getStringValue("last_selection");
#if EMU_BUILD_NUM != 0
updateCheck = config->getStringValue("update_check").toInt();
#endif
regexSearch = config->getStringValue("regex_search").toInt();
const auto matches = ui->listView->model()->match(vm_model->index(0, 0), VMManagerModel::Roles::ConfigName, QVariant::fromValue(lastSelection));
if (!matches.empty()) {
ui->listView->setCurrentIndex(matches.first());
ui->listView->scrollTo(ui->listView->currentIndex(), QAbstractItemView::PositionAtCenter);
}
}
bool
VMManagerMain::currentSelectionIsValid() const
{
return ui->listView->currentIndex().isValid() && selected_sysconfig->isValid();
}
void
VMManagerMain::onConfigUpdated(const QString &uuid)
{
if (selected_sysconfig->uuid == uuid)
vm_details->updateData(selected_sysconfig);
}
// Used from MainWindow during app exit to obtain and persist the current selection
QString
VMManagerMain::getCurrentSelection() const
{
return ui->listView->currentIndex().data(VMManagerModel::Roles::ConfigName).toString();
}
void
VMManagerMain::searchSystems(const QString &text) const
{
// Escape the search text string unless regular expression searching is enabled.
// When escaped, the search string functions as a plain text match.
const auto searchText = regexSearch ? text : QRegularExpression::escape(text);
const QRegularExpression regex(searchText, QRegularExpression::CaseInsensitiveOption);
if (!regex.isValid()) {
qDebug() << "Skipping, invalid regex";
return;
}
proxy_model->setFilterRegularExpression(regex);
// Searching (filtering) can cause the list view to change. If there is still a valid selection,
// make sure to scroll to it
if (ui->listView->currentIndex().isValid()) {
ui->listView->scrollTo(ui->listView->currentIndex(), QAbstractItemView::PositionAtCenter);
}
}
void
VMManagerMain::newMachineWizard()
{
const auto wizard = new VMManagerAddMachine(this);
if (wizard->exec() == QDialog::Accepted) {
const auto newName = wizard->field("systemName").toString();
#ifdef CUSTOM_SYSTEM_LOCATION
const auto systemDir = wizard->field("systemLocation").toString();
#else
const auto systemDir = QDir(vmm_path).path();
#endif
const auto existingConfiguration = wizard->field("existingConfiguration").toString();
const auto displayName = wizard->field("displayName").toString();
addNewSystem(newName, systemDir, displayName, existingConfiguration);
}
}
void
VMManagerMain::addNewSystem(const QString &name, const QString &dir, const QString &displayName, const QString &configFile)
{
const auto newSystemDirectory = QDir(QDir::cleanPath(dir + "/" + name));
// qt replaces `/` with native separators
const auto newSystemConfigFile = QFileInfo(newSystemDirectory.path() + "/" + CONFIG_FILE);
if (newSystemConfigFile.exists() || newSystemDirectory.exists()) {
QMessageBox::critical(this, tr("Directory in use"), tr("The selected directory is already in use. Please select a different directory."));
return;
}
// Create the directory
const QDir qmkdir;
if (const bool mkdirResult = qmkdir.mkdir(newSystemDirectory.path()); !mkdirResult) {
QMessageBox::critical(this, tr("Create directory failed"), tr("Unable to create the directory for the new system"));
return;
}
// If specified, write the contents of the configuration file before starting
if (!configFile.isEmpty()) {
const auto configPath = newSystemConfigFile.absoluteFilePath();
const auto file = new QFile(configPath);
if (!file->open(QIODevice::WriteOnly)) {
qWarning() << "Unable to open file " << configPath;
QMessageBox::critical(this, tr("Configuration write failed"), tr("Unable to open the configuration file at %1 for writing").arg(configPath));
return;
}
file->write(configFile.toUtf8());
file->flush();
file->close();
}
const auto new_system = new VMManagerSystem(newSystemConfigFile.absoluteFilePath());
new_system->launchSettings();
// Handle this in a closure so we can capture the temporary new_system object
disconnect(new_system->process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), nullptr, nullptr);
connect(new_system->process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
[=](const int exitCode, const QProcess::ExitStatus exitStatus) {
bool fail = false;
if (exitCode != 0 || exitStatus != QProcess::NormalExit) {
qInfo().nospace().noquote() << "Abnormal program termination while creating new system: exit code " << exitCode << ", exit status " << exitStatus;
qInfo() << "Not adding system due to errors";
QString errMsg = tr("The virtual machine \"%1\"'s process has unexpectedly terminated with exit code %2.").arg(
(!displayName.isEmpty() ? displayName : name), QString::number(exitCode));
QMessageBox::critical(this, tr("Error adding system"),
QString("%1\n\n%2").arg(errMsg, tr("The system will not be added.")));
fail = true;
}
// Create a new QFileInfo because the info from the old one may be cached
if (const auto fi = QFileInfo(new_system->config_file.absoluteFilePath()); !fi.exists()) {
// No config file which means the cancel button was pressed in the settings dialog
// Attempt to clean up the directory that was created
const QDir qrmdir;
if (const bool result = qrmdir.rmdir(newSystemDirectory.path()); !result) {
qWarning() << "Error cleaning up the old directory for canceled operation. Continuing anyway.";
}
fail = true;
}
if (fail) {
delete new_system;
return;
}
const auto current_index = ui->listView->currentIndex();
vm_model->reload(this);
const auto created_object = vm_model->getIndexForConfigFile(new_system->config_file);
if (created_object.row() < 0) {
// For some reason the index of the new object couldn't be determined. Fall back to the old index.
ui->listView->setCurrentIndex(current_index);
delete new_system;
return;
}
auto added_system = vm_model->getConfigObjectForIndex(created_object);
added_system->setDisplayName(displayName);
// Get the index of the newly-created system and select it
const QModelIndex mapped_index = proxy_model->mapFromSource(created_object);
ui->listView->setCurrentIndex(mapped_index);
delete new_system;
});
}
void
VMManagerMain::deleteSystem(VMManagerSystem *sysconfig)
{
QMessageBox msgbox(QMessageBox::Icon::Warning, tr("Warning"), tr("Do you really want to delete the virtual machine \"%1\" and all its files? This action cannot be undone!").arg(sysconfig->displayName), QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No, qobject_cast<QWidget *>(this->parent()));
msgbox.exec();
if (msgbox.result() == QMessageBox::Yes) {
auto qrmdir = new QDir(sysconfig->config_dir);
if (const bool rmdirResult = qrmdir->removeRecursively(); !rmdirResult) {
QMessageBox::critical(this, tr("Remove directory failed"), tr("Some files in the machine's directory were unable to be deleted. Please delete them manually."));
return;
}
auto config = new VMManagerConfig(VMManagerConfig::ConfigType::General);
config->remove(sysconfig->uuid);
vm_model->removeConfigFromModel(sysconfig);
delete sysconfig;
if (vm_model->rowCount(QModelIndex()) <= 0) {
/* no machines left - get rid of the last machine's leftovers */
ui->detailsArea->layout()->removeWidget(vm_details);
delete vm_details;
vm_details = new VMManagerDetails();
ui->detailsArea->layout()->addWidget(vm_details);
}
}
}
QStringList
VMManagerMain::getSearchCompletionList() const
{
QSet<QString> uniqueStrings;
for (int row = 0; row < vm_model->rowCount(QModelIndex()); ++row) {
QModelIndex index = vm_model->index(row, 0);
auto fullList = vm_model->data(index, VMManagerModel::Roles::SearchList).toStringList();
QSet uniqueSet(fullList.begin(), fullList.end());
uniqueStrings.unite(uniqueSet);
}
// Convert the set back to a QStringList
QStringList allStrings = uniqueStrings.values();
return allStrings;
}
QString
VMManagerMain::machineCountString(QString states) const
{
const auto count = vm_model->rowCount(QModelIndex());
if (!states.isEmpty())
states.append(", ");
states.append(tr("%1 total").arg(count));
return tr("VMs: %1").arg(states);
}
QList<int>
VMManagerMain::getPaneSizes() const
{
return ui->splitter->sizes();
}
void
VMManagerMain::setPaneSizes(const QList<int> &sizes)
{
ui->splitter->setSizes(sizes);
}
void
VMManagerMain::modelDataChange()
{
// Model data has changed. This includes process status.
// Update the counts / totals accordingly
auto modelStats = vm_model->getProcessStats();
QStringList stats;
for (auto it = modelStats.constBegin(); it != modelStats.constEnd(); ++it) {
const auto &key = it.key();
QString text = "";
switch (key) {
case VMManagerSystem::ProcessStatus::Running:
text = tr("%n running", "", modelStats[key]);
break;
case VMManagerSystem::ProcessStatus::Paused:
text = tr("%n paused", "", modelStats[key]);
break;
case VMManagerSystem::ProcessStatus::PausedWaiting:
case VMManagerSystem::ProcessStatus::RunningWaiting:
text = tr("%n waiting", "", modelStats[key]);
break;
default:
break;
}
if(!text.isEmpty())
stats.append(text);
}
auto states = stats.join(", ");
emit updateStatusRight(machineCountString(states));
}
void
VMManagerMain::onPreferencesUpdated()
{
// Only reload values that we care about
const auto config = new VMManagerConfig(VMManagerConfig::ConfigType::General);
const auto oldRegexSearch = regexSearch;
regexSearch = config->getStringValue("regex_search").toInt();
if (oldRegexSearch != regexSearch) {
ui->searchBar->clear();
}
if (vm_model) {
vm_model->sendGlobalConfigurationChanged();
}
}
void
VMManagerMain::onLanguageUpdated()
{
vm_model->refreshConfigs();
modelDataChange();
/* Hack to work around details widgets not being re-translatable
without going through layers of abstraction */
ui->detailsArea->layout()->removeWidget(vm_details);
delete vm_details;
vm_details = new VMManagerDetails();
ui->detailsArea->layout()->addWidget(vm_details);
if (vm_model->rowCount(QModelIndex()) > 0)
vm_details->updateData(selected_sysconfig);
}
#ifdef Q_OS_WINDOWS
void
VMManagerMain::onDarkModeUpdated()
{
vm_details->updateStyle();
}
#endif
int
VMManagerMain::getActiveMachineCount()
{
return vm_model->getActiveMachineCount();
}
#if EMU_BUILD_NUM != 0
void
VMManagerMain::backgroundUpdateCheckStart() const
{
auto updateChannel = UpdateCheck::UpdateChannel::CI;
#ifdef RELEASE_BUILD
updateChannel = UpdateCheck::UpdateChannel::Stable;
#endif
const auto updateCheck = new UpdateCheck(updateChannel);
connect(updateCheck, &UpdateCheck::updateCheckComplete, this, &VMManagerMain::backgroundUpdateCheckComplete);
connect(updateCheck, &UpdateCheck::updateCheckError, this, &VMManagerMain::backgroundUpdateCheckError);
updateCheck->checkForUpdates();
}
void
VMManagerMain::backgroundUpdateCheckComplete(const UpdateCheck::UpdateResult &result)
{
qDebug() << "Check complete: update available?" << result.updateAvailable;
if (result.updateAvailable) {
auto type = result.channel == UpdateCheck::UpdateChannel::CI ? tr("build") : tr("version");
const auto updateMessage = tr("An update to 86Box is available: %1 %2").arg(type, result.latestVersion);
emit updateStatusLeft(updateMessage);
}
}
void
VMManagerMain::backgroundUpdateCheckError(const QString &errorMsg)
{
qDebug() << "Update check failed with the following error:" << errorMsg;
emit updateStatusLeft(tr("An error has occurred while checking for updates: %1").arg(errorMsg));
}
#endif
void
VMManagerMain::showTextFileContents(const QString &title, const QString &path)
{
// Make sure we can open the file
const auto fi = QFileInfo(path);
if(!fi.exists()) {
qWarning("Requested file does not exist: %s", path.toUtf8().constData());
return;
}
// Read the file
QFile displayFile(path);
if (!displayFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qWarning("Couldn't open the file: error %d", displayFile.error());
return;
}
const QString configFileContents = displayFile.readAll();
displayFile.close();
const auto textDisplayDialog = new QDialog(this);
textDisplayDialog->setMinimumSize(QSize(540, 360));
textDisplayDialog->setWindowTitle(QString("%1 - %2").arg(title, fi.fileName()));
const auto textEdit = new QPlainTextEdit();
const auto monospaceFont = new QFont();
#ifdef Q_OS_WINDOWS
monospaceFont->setFamily("Consolas");
#elif defined(Q_OS_MACOS)
monospaceFont->setFamily("Menlo");
#else
monospaceFont->setFamily("Monospace");
#endif
monospaceFont->setStyleHint(QFont::Monospace);
monospaceFont->setFixedPitch(true);
textEdit->setFont(*monospaceFont);
textEdit->setReadOnly(true);
textEdit->setPlainText(configFileContents);
const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
connect(buttonBox, &QDialogButtonBox::accepted, textDisplayDialog, &QDialog::accept);
const auto layout = new QVBoxLayout();
textDisplayDialog->setLayout(layout);
textDisplayDialog->layout()->addWidget(textEdit);
textDisplayDialog->layout()->addWidget(buttonBox);
textDisplayDialog->exec();
}