Files
86Box/src/qt/qt_vmmanager_main.cpp

791 lines
32 KiB
C++
Raw Normal View History

2024-06-08 13:25:09 -04:00
/*
* 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
*/
2025-08-08 23:50:54 +06:00
#include <QDirIterator>
#include <QLabel>
2024-06-08 13:25:09 -04:00
#include <QAbstractListModel>
#include <QCompleter>
#include <QDebug>
#include <QDesktopServices>
#include <QMenu>
#include <QMessageBox>
#include <QStringListModel>
#include <QTimer>
2025-08-08 23:50:54 +06:00
#include <QProgressDialog>
#include <thread>
#include <atomic>
2024-06-08 13:25:09 -04:00
#include "qt_vmmanager_main.hpp"
#include "ui_qt_vmmanager_main.h"
#include "qt_vmmanager_model.hpp"
#include "qt_vmmanager_addmachine.hpp"
2025-08-10 00:44:03 +06:00
// 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;
}
2024-06-08 13:25:09 -04:00
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);
2025-07-11 23:30:14 +06:00
connect(ui->listView, &QListView::customContextMenuRequested, [this, parent](const QPoint &pos) {
2024-06-08 13:25:09 -04:00
const auto indexAt = ui->listView->indexAt(pos);
if (indexAt.isValid()) {
QMenu contextMenu(tr("Context Menu"), ui->listView);
QAction nameChangeAction(tr("Change &display name..."));
2024-06-08 13:25:09 -04:00
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);
2024-06-08 13:25:09 -04:00
QAction openSystemFolderAction(tr("&Open folder..."));
2024-06-08 13:25:09 -04:00
contextMenu.addAction(&openSystemFolderAction);
2025-06-28 19:33:57 -04:00
connect(&openSystemFolderAction, &QAction::triggered, [indexAt] {
2024-06-08 13:25:09 -04:00
if (const auto configDir = indexAt.data(VMManagerModel::Roles::ConfigDir).toString(); !configDir.isEmpty()) {
2025-05-31 16:26:23 +06:00
QDir dir(configDir);
if (!dir.exists())
dir.mkpath(".");
QDesktopServices::openUrl(QUrl(QString("file:///") + dir.canonicalPath()));
2024-06-08 13:25:09 -04:00
}
});
QAction openPrinterFolderAction(tr("Open &printer 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 setSystemIcon(tr("Set &icon..."));
2024-06-08 13:25:09 -04:00
contextMenu.addAction(&setSystemIcon);
2025-06-28 19:33:57 -04:00
connect(&setSystemIcon, &QAction::triggered, [this] {
2024-06-08 13:25:09 -04:00
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);
2024-06-08 13:25:09 -04:00
2025-08-09 00:28:38 +06:00
QAction cloneMachine(tr("C&lone..."));
2025-08-08 23:50:54 +06:00
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, [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 (int 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 (int 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_int 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->setFixedSize(progDialog->sizeHint());
progDialog->setMinimumDuration(0);
progDialog->setCancelButton(nullptr);
progDialog->setAutoClose(false);
progDialog->setAutoReset(false);
progDialog->setAttribute(Qt::WA_DeleteOnClose, true);
progDialog->setValue(0);
progDialog->show();
2025-08-10 00:44:03 +06:00
QString srcPath = selected_sysconfig->config_dir;
QString dstPath = vmDir;
2025-08-08 23:50:54 +06:00
std::thread copyThread([this, &finished, srcPath, dstPath, &errCode] {
2025-08-10 00:44:03 +06:00
errCode = copyPath(srcPath, dstPath, true);
2025-08-08 23:50:54 +06:00
finished = true;
});
while (!finished) {
QApplication::processEvents();
}
copyThread.join();
progDialog->close();
if (errCode) {
2025-08-10 00:44:03 +06:00
QDir(dstPath).removeRecursively();
2025-08-08 23:50:54 +06:00
QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM: %1").arg(errCode), 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 {
2025-08-10 00:44:03 +06:00
QDir(dstPath).removeRecursively();
2025-08-08 23:50:54 +06:00
QMessageBox::critical(this, tr("Clone"), tr("Failed to clone VM for an unknown reason."), QMessageBox::Ok);
return;
}
}
});
2025-07-11 23:30:14 +06:00
QAction killIcon(tr("&Kill"));
contextMenu.addAction(&killIcon);
connect(&killIcon, &QAction::triggered, [this, parent] {
2025-07-30 23:39:59 +05:00
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);
2025-07-11 23:30:14 +06:00
msgbox.exec();
if (msgbox.result() == QMessageBox::Yes) {
selected_sysconfig->process->kill();
}
});
killIcon.setEnabled(selected_sysconfig->process->state() == QProcess::Running);
2025-08-07 13:36:46 +05:00
QAction deleteAction(tr("&Delete"));
contextMenu.addAction(&deleteAction);
connect(&deleteAction, &QAction::triggered, [this, parent] {
deleteSystem(selected_sysconfig);
});
deleteAction.setEnabled(selected_sysconfig->process->state() == QProcess::NotRunning);
2024-06-08 13:25:09 -04:00
contextMenu.addSeparator();
QAction showRawConfigFile(tr("Show &config file"));
2024-06-08 13:25:09 -04:00
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));
}
});
// Initial default details view
vm_details = new VMManagerDetails();
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);
2024-06-08 13:25:09 -04:00
// Load and apply settings
loadSettings();
ui->splitter->setSizes({ui->detailsArea->width(), (ui->listView->minimumWidth() * 2)});
2024-06-08 13:25:09 -04:00
// 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(totalCountString());
});
#if EMU_BUILD_NUM != 0
2024-06-08 13:25:09 -04:00
// Start update check after a slight delay
QTimer::singleShot(1000, this, [this] {
if(updateCheck) {
backgroundUpdateCheckStart();
}
});
#endif
2024-06-08 13:25:09 -04:00
}
VMManagerMain::~VMManagerMain() {
delete ui;
delete vm_model;
}
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;
}
2024-06-08 13:25:09 -04:00
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;
}
2024-06-08 13:25:09 -04:00
// 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
2024-06-08 13:25:09 -04:00
updateCheck = config->getStringValue("update_check").toInt();
#endif
2024-06-08 13:25:09 -04:00
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);
}
2024-06-08 13:25:09 -04:00
// 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
2024-06-08 13:25:09 -04:00
const auto systemDir = wizard->field("systemLocation").toString();
#else
const auto systemDir = QDir(vmm_path).path();
#endif
2024-06-08 13:25:09 -04:00
const auto existingConfiguration = wizard->field("existingConfiguration").toString();
const auto displayName = wizard->field("displayName").toString();
addNewSystem(newName, systemDir, displayName, existingConfiguration);
2024-06-08 13:25:09 -04:00
}
}
void
VMManagerMain::addNewSystem(const QString &name, const QString &dir, const QString &displayName, const QString &configFile)
2024-06-08 13:25:09 -04:00
{
const auto newSytemDirectory = QDir(QDir::cleanPath(dir + "/" + name));
// qt replaces `/` with native separators
const auto newSystemConfigFile = QFileInfo(newSytemDirectory.path() + "/" + "86box.cfg");
if (newSystemConfigFile.exists() || newSytemDirectory.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(newSytemDirectory.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
connect(new_system->process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
[=](const int exitCode, const QProcess::ExitStatus exitStatus) {
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";
QMessageBox::critical(this, tr("Error adding system"),
tr("Abnormal program termination while creating new system: exit code %1, exit status %2.\n\nThe system will not be added.").arg(QString::number(exitCode), exitStatus));
delete new_system;
return;
}
// 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(newSytemDirectory.path()); !result) {
qWarning() << "Error cleaning up the old directory for canceled operation. Continuing anyway.";
}
delete new_system;
return;
}
const auto current_index = ui->listView->currentIndex();
vm_model->reload(this);
2024-06-08 13:25:09 -04:00
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);
2024-06-08 13:25:09 -04:00
// 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;
});
}
2025-08-07 13:36:46 +05:00
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);
}
2025-08-07 13:36:46 +05:00
}
}
2024-06-08 13:25:09 -04:00
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::totalCountString() const
{
const auto count = vm_model->rowCount(QModelIndex());
return QString("%1 %2").arg(QString::number(count), tr("total"));
}
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();
stats.append(QString("%1 %2").arg(QString::number(modelStats[key]), key));
}
auto states = stats.join(", ");
if (!modelStats.isEmpty()) {
states.append(", ");
}
emit updateStatusRight(states + totalCountString());
}
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();
}
}
int
VMManagerMain::getActiveMachineCount()
{
return vm_model->getActiveMachineCount();
}
#if EMU_BUILD_NUM != 0
2024-06-08 13:25:09 -04:00
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;
auto type = result.channel == UpdateCheck::UpdateChannel::CI ? tr("Build") : tr("Version");
const auto updateMessage = QString("%1: %2 %3").arg( tr("An update to 86Box is available"), type, result.latestVersion);
emit updateStatusLeft(updateMessage);
}
void
VMManagerMain::backgroundUpdateCheckError(const QString &errorMsg)
{
qDebug() << "Update check failed with the following error:" << errorMsg;
// TODO: Update the status bar
}
#endif
2024-06-08 13:25:09 -04:00
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));
2024-06-08 13:25:09 -04:00
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);
2024-06-08 13:25:09 -04:00
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();
}