aboutsummaryrefslogtreecommitdiff
path: root/libs/uibase/src/report.cpp
diff options
context:
space:
mode:
authorSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
committerSulfurNitride <SulfurNitride@users.noreply.github.com>2026-02-11 02:37:39 -0600
commit7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch)
tree27fb39be241fdb5ac2734c574de678977d1856d0 /libs/uibase/src/report.cpp
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem, Proton/umu-run integration, and Flatpak packaging. Key features: - FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak) - Proton/GE-Proton/umu-run launcher with env var forwarding - Flatpak support (sandbox-aware VFS, NXM handler, umu-run) - Wine prefix management UI - Case-insensitive path resolution for Linux filesystems - QSettings-safe INI handling (avoids Bethesda INI corruption) - Portable instance support with auto-generated launcher scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/uibase/src/report.cpp')
-rw-r--r--libs/uibase/src/report.cpp453
1 files changed, 453 insertions, 0 deletions
diff --git a/libs/uibase/src/report.cpp b/libs/uibase/src/report.cpp
new file mode 100644
index 0000000..1c47d95
--- /dev/null
+++ b/libs/uibase/src/report.cpp
@@ -0,0 +1,453 @@
+/*
+Mod Organizer shared UI functionality
+
+Copyright (C) 2012 Sebastian Herbord. All rights reserved.
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 3 of the License, or (at your option) any later version.
+
+This library is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+
+#include <uibase/report.h>
+#include <uibase/expanderwidget.h>
+#include <uibase/log.h>
+#include <uibase/questionboxmemory.h>
+#include "ui_taskdialog.h"
+#include <uibase/utility.h>
+#include <QComboBox>
+#include <QMainWindow>
+#include <QRadioButton>
+#include <QThread>
+
+namespace MOBase
+{
+
+QWidget* topLevelWindow()
+{
+ if (!qApp) {
+ return nullptr;
+ }
+
+ for (QWidget* w : qApp->topLevelWidgets()) {
+ if (dynamic_cast<QMainWindow*>(w)) {
+ return w;
+ }
+ }
+
+ return nullptr;
+}
+
+void criticalOnTop(const QString& message)
+{
+ QMessageBox mb(QMessageBox::Critical, "Mod Organizer", message);
+
+ mb.show();
+ mb.activateWindow();
+ mb.raise();
+ mb.exec();
+}
+
+void reportError(const QString& message)
+{
+ log::error("{}", message);
+
+ // QMessageBox must only be created on the main (GUI) thread.
+ // If we're on a background thread, just log — showing a dialog would crash.
+ if (QThread::currentThread() != qApp->thread()) {
+ return;
+ }
+
+ if (QApplication::topLevelWidgets().count() != 0) {
+ if (auto* mw = topLevelWindow()) {
+ QMessageBox::warning(mw, QObject::tr("Error"), message, QMessageBox::Ok);
+ } else {
+ criticalOnTop(message);
+ }
+ } else {
+ // No top-level widget available, show a standalone critical dialog
+ criticalOnTop(message);
+ }
+}
+
+TaskDialogButton::TaskDialogButton(QString t, QString d, QMessageBox::StandardButton b)
+ : text(std::move(t)), description(std::move(d)), button(b)
+{}
+
+TaskDialogButton::TaskDialogButton(QString t, QMessageBox::StandardButton b)
+ : TaskDialogButton(std::move(t), {}, b)
+{}
+
+TaskDialog::TaskDialog(QWidget* parent, QString title)
+ : m_dialog(new QDialog(parent)), ui(new Ui::TaskDialog), m_title(std::move(title)),
+ m_icon(QMessageBox::NoIcon), m_result(QMessageBox::Cancel), m_width(-1),
+ m_rememberCheck(nullptr), m_rememberCombo(nullptr)
+{
+ ui->setupUi(m_dialog.get());
+}
+
+TaskDialog::~TaskDialog() = default;
+
+TaskDialog& TaskDialog::title(const QString& s)
+{
+ m_title = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::main(const QString& s)
+{
+ m_main = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::content(const QString& s)
+{
+ m_content = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::details(const QString& s)
+{
+ m_details = s;
+ return *this;
+}
+
+TaskDialog& TaskDialog::icon(QMessageBox::Icon i)
+{
+ m_icon = i;
+ return *this;
+}
+
+TaskDialog& TaskDialog::button(TaskDialogButton b)
+{
+ m_buttons.emplace_back(std::move(b));
+ return *this;
+}
+
+TaskDialog& TaskDialog::remember(const QString& action, const QString& file)
+{
+ m_rememberAction = action;
+ m_rememberFile = file;
+ return *this;
+}
+
+void TaskDialog::addContent(QWidget* w)
+{
+ auto* ly = static_cast<QVBoxLayout*>(ui->contentPanel->layout());
+
+ // add before spacer
+ ly->insertWidget(ly->count() - 1, w);
+}
+
+void TaskDialog::setWidth(int w)
+{
+ m_width = w;
+}
+
+QMessageBox::StandardButton TaskDialog::exec()
+{
+ const auto b = checkMemory();
+ if (b != QMessageBox::NoButton) {
+ return b;
+ }
+
+ setDialog();
+ setWidgets();
+ setChoices();
+ setButtons();
+ setDetails();
+
+ if (m_width >= 0) {
+ ui->topPanel->setMinimumWidth(m_width);
+ } else {
+ ui->topPanel->setMinimumWidth(400);
+ }
+
+ m_dialog->adjustSize();
+
+ if (!m_dialog->parent()) {
+ // no parent, make sure it's shown on top
+ m_dialog->show();
+ m_dialog->activateWindow();
+ m_dialog->raise();
+ }
+
+ if (m_dialog->exec() != QDialog::Accepted) {
+ return QMessageBox::Cancel;
+ }
+
+ rememberChoice();
+ return m_result;
+}
+
+QMessageBox::StandardButton TaskDialog::checkMemory() const
+{
+ if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) {
+ return QMessageBox::NoButton;
+ }
+
+ const auto b = QuestionBoxMemory::getMemory(m_rememberAction, m_rememberFile);
+
+ const auto logName = m_rememberAction +
+ (m_rememberFile.isEmpty() ? "" : QString("/") + m_rememberFile);
+
+ if (b == QDialogButtonBox::NoButton) {
+ log::debug("{}: asking because the user has not set a choice before", logName);
+ } else {
+ log::debug("{}: not asking because user always wants response {}", logName,
+ QuestionBoxMemory::buttonToString(b));
+ }
+
+ return static_cast<QMessageBox::StandardButton>(b);
+}
+
+void TaskDialog::rememberChoice()
+{
+ if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) {
+ // nothing
+ return;
+ }
+
+ const auto b = static_cast<QuestionBoxMemory::Button>(m_result);
+
+ if (m_rememberCheck) {
+ // action only
+ if (m_rememberCheck->isChecked()) {
+ QuestionBoxMemory::setWindowMemory(m_rememberAction, b);
+ }
+ } else if (m_rememberCombo) {
+ Q_ASSERT(m_rememberCombo->count() == 3);
+
+ if (m_rememberCombo->currentIndex() == 1) {
+ // remember action
+ QuestionBoxMemory::setWindowMemory(m_rememberAction, b);
+ } else if (m_rememberCombo->currentIndex() == 2) {
+ // remember file
+ QuestionBoxMemory::setFileMemory(m_rememberAction, m_rememberFile, b);
+ }
+ }
+}
+
+void TaskDialog::setButtons()
+{
+ if (m_buttons.empty()) {
+ setStandardButtons();
+ } else {
+ setCommandButtons();
+ }
+
+ m_expander.reset(new ExpanderWidget(ui->detailsExpander, ui->detailsWidget));
+}
+
+void TaskDialog::setStandardButtons()
+{
+ ui->standardButtonsPanel->show();
+ ui->commandButtonsPanel->hide();
+
+ for (auto* b : ui->standardButtons->buttons()) {
+ ui->standardButtons->removeButton(b);
+ }
+
+ ui->standardButtons->addButton(QDialogButtonBox::Ok);
+
+ QObject::connect(ui->standardButtons, &QDialogButtonBox::clicked, [&](auto* b) {
+ m_result = static_cast<QMessageBox::StandardButton>(
+ ui->standardButtons->standardButton(b));
+
+ m_dialog->accept();
+ });
+}
+
+void TaskDialog::setCommandButtons()
+{
+ ui->standardButtonsPanel->hide();
+ ui->commandButtonsPanel->show();
+
+ deleteChildWidgets(ui->commandButtons);
+
+ for (auto&& b : m_buttons) {
+ auto* cb = new QCommandLinkButton(b.text, b.description);
+
+ QObject::connect(cb, &QAbstractButton::clicked, [&] {
+ m_result = b.button;
+ m_dialog->accept();
+ });
+
+ ui->commandButtons->layout()->addWidget(cb);
+ }
+}
+
+void TaskDialog::setDetails()
+{
+ if (m_details.isEmpty()) {
+ ui->detailsExpander->setEnabled(false);
+ return;
+ }
+
+ ui->details->setPlainText(m_details);
+
+ setVisibleLines(ui->details, 10);
+ setFontPercent(ui->details, 0.9);
+
+ const QColor bg = detailsColor();
+
+ ui->detailsPanel->setStyleSheet(QString("background-color: rgb(%1, %2, %3)")
+ .arg(bg.redF() * 255)
+ .arg(bg.greenF() * 255)
+ .arg(bg.blueF() * 255));
+}
+
+void TaskDialog::setDialog()
+{
+#ifdef _WIN32
+ m_dialog->setWindowFlags(Qt::Dialog | Qt::MSWindowsFixedSizeDialogHint);
+#else
+ m_dialog->setWindowFlags(Qt::Dialog);
+#endif
+ m_dialog->layout()->setSizeConstraint(QLayout::SetFixedSize);
+
+ m_dialog->setWindowTitle(m_title);
+}
+
+void TaskDialog::setWidgets()
+{
+ ui->main->setText(m_main);
+ setFontPercent(ui->main, 1.5);
+
+ ui->content->setText(m_content);
+ ui->content->setVisible(!m_content.isEmpty());
+
+ auto icon = standardIcon(m_icon);
+
+ if (icon.isNull()) {
+ ui->iconPanel->hide();
+ } else {
+ ui->iconPanel->show();
+ ui->icon->setPixmap(std::move(icon));
+ }
+}
+
+void TaskDialog::setChoices()
+{
+ if (m_rememberAction.isEmpty() && m_rememberFile.isEmpty()) {
+ ui->rememberPanel->hide();
+ return;
+ }
+
+ ui->rememberPanel->show();
+ deleteChildWidgets(ui->rememberPanel);
+
+ const auto tooltip = QObject::tr(
+ "You can reset these choices by clicking \"Reset Dialog Choices\" in the "
+ "General tab of the Settings");
+
+ if (!m_rememberAction.isEmpty() && !m_rememberFile.isEmpty()) {
+ // both
+ m_rememberCombo = new QComboBox;
+ m_rememberCombo->setToolTip(tooltip);
+
+ m_rememberCombo->addItem(QObject::tr("Always ask"));
+ m_rememberCombo->addItem(QObject::tr("Remember my choice"));
+ m_rememberCombo->addItem(
+ QObject::tr("Remember my choice for %1").arg(m_rememberFile));
+
+ ui->rememberPanel->layout()->setAlignment(Qt::AlignLeft);
+ ui->rememberPanel->layout()->addWidget(m_rememberCombo);
+ } else if (!m_rememberAction.isEmpty() || !m_rememberFile.isEmpty()) {
+ // either
+ m_rememberCheck = new QCheckBox(QObject::tr("Remember my choice"));
+ m_rememberCheck->setToolTip(tooltip);
+ ui->rememberPanel->layout()->addWidget(m_rememberCheck);
+ }
+}
+
+QColor TaskDialog::detailsColor() const
+{
+ auto b = std::make_unique<QPushButton>();
+ m_dialog->style()->polish(b.get());
+ return b->palette().color(b->backgroundRole());
+}
+
+QPixmap TaskDialog::standardIcon(QMessageBox::Icon icon) const
+{
+ QStyle* s = m_dialog->style();
+ QIcon i;
+
+ switch (icon) {
+ case QMessageBox::Information:
+ i = s->standardIcon(QStyle::SP_MessageBoxInformation, 0, m_dialog.get());
+ break;
+ case QMessageBox::Warning:
+ i = s->standardIcon(QStyle::SP_MessageBoxWarning, 0, m_dialog.get());
+ break;
+ case QMessageBox::Critical:
+ i = s->standardIcon(QStyle::SP_MessageBoxCritical, 0, m_dialog.get());
+ break;
+ case QMessageBox::Question:
+ i = s->standardIcon(QStyle::SP_MessageBoxQuestion, 0, m_dialog.get());
+ break;
+ case QMessageBox::NoIcon:
+ [[fallthrough]];
+ default:
+ break;
+ }
+
+ if (i.isNull()) {
+ return {};
+ }
+
+ QWindow* window = m_dialog->windowHandle();
+ if (!window) {
+ if (const QWidget* nativeParent = m_dialog->nativeParentWidget())
+ window = nativeParent->windowHandle();
+ }
+
+ const int iconSize = s->pixelMetric(QStyle::PM_MessageBoxIconSize, 0, m_dialog.get());
+
+ return i.pixmap(QSize(iconSize, iconSize));
+}
+
+void TaskDialog::setVisibleLines(QPlainTextEdit* w, int lines)
+{
+ QTextDocument* d = w->document();
+ QFontMetrics fm(d->defaultFont());
+
+ const QMargins margins = ui->details->contentsMargins();
+
+ double height = 0;
+
+ // lines
+ height += fm.lineSpacing() * lines;
+
+ // top and bottom margins for document and frame
+ height += (d->documentMargin() + ui->details->frameWidth()) * 2;
+
+ // widget margins
+ height += margins.top() + margins.bottom();
+
+ w->setMaximumHeight(static_cast<int>(std::round(height)));
+}
+
+void TaskDialog::setFontPercent(QWidget* w, double p)
+{
+ auto f = w->font();
+
+ if (f.pointSizeF() > 0) {
+ f.setPointSizeF(f.pointSizeF() * p);
+ } else if (f.pixelSize() > 0) {
+ f.setPixelSize(static_cast<int>(std::round(f.pixelSize() * p)));
+ }
+
+ w->setFont(f);
+}
+
+} // namespace MOBase