From 7ee008e150bc5bcf76082d726f719ee0fdfda982 Mon Sep 17 00:00:00 2001 From: SulfurNitride Date: Wed, 11 Feb 2026 02:37:39 -0600 Subject: 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 --- libs/uibase/src/tutorialcontrol.cpp | 384 ++++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 libs/uibase/src/tutorialcontrol.cpp (limited to 'libs/uibase/src/tutorialcontrol.cpp') diff --git a/libs/uibase/src/tutorialcontrol.cpp b/libs/uibase/src/tutorialcontrol.cpp new file mode 100644 index 0000000..fee19f4 --- /dev/null +++ b/libs/uibase/src/tutorialcontrol.cpp @@ -0,0 +1,384 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer 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. + +Mod Organizer 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 Mod Organizer. If not, see . +*/ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace MOBase +{ + +TutorialControl::TutorialControl(const TutorialControl& reference) + : QObject(reference.parent()), m_TargetControl(reference.m_TargetControl), + m_Name(reference.m_Name), m_TutorialView(nullptr), + m_Manager(TutorialManager::instance()), m_ExpectedTab(0), + m_CurrentClickControl(nullptr) +{} + +TutorialControl::TutorialControl(QWidget* targetControl, const QString& name) + : QObject(nullptr), m_TargetControl(targetControl), m_Name(name), + m_TutorialView(nullptr), m_Manager(TutorialManager::instance()), m_ExpectedTab(0), + m_CurrentClickControl(nullptr) +{} + +TutorialControl::~TutorialControl() +{ + m_Manager.unregisterControl(m_Name); + finish(); +} + +void TutorialControl::registerControl() +{ + m_Manager.registerControl(m_Name, this); +} + +void TutorialControl::resize(const QSize& size) +{ + if (m_TutorialView != nullptr) { + m_TutorialView->resize(size.width(), size.height()); + } +} + +void TutorialControl::expose(const QString& widgetName, QObject* widget) +{ + m_ExposedObjects.push_back(std::make_pair(widgetName, widget)); +} + +static QString canonicalPath(const QString& path) +{ +#ifdef _WIN32 + std::unique_ptr buffer(new wchar_t[32768]); + DWORD res = ::GetShortPathNameW((wchar_t*)path.utf16(), buffer.get(), 32768); + if (res == 0) { + return path; + } + res = ::GetLongPathNameW(buffer.get(), buffer.get(), 32768); + if (res == 0) { + return path; + } + return QString::fromWCharArray(buffer.get()); +#else + QFileInfo fi(path); + QString canonical = fi.canonicalFilePath(); + return canonical.isEmpty() ? path : canonical; +#endif +} + +void TutorialControl::startTutorial(const QString& tutorial) +{ + if (m_TutorialView == nullptr) { + m_TutorialView = new QQuickWidget(m_TargetControl); + m_TutorialView->setResizeMode(QQuickWidget::SizeRootObjectToView); + m_TutorialView->setAttribute(Qt::WA_TranslucentBackground); + m_TutorialView->setAttribute(Qt::WA_AlwaysStackOnTop); + m_TutorialView->setClearColor(Qt::transparent); + m_TutorialView->setStyleSheet("background: transparent"); + m_TutorialView->setObjectName("tutorialView"); + m_TutorialView->rootContext()->setContextProperty("manager", &m_Manager); + + QString qmlName = + canonicalPath(QCoreApplication::applicationDirPath() + "/tutorials") + + "/tutorials_" + m_Name.toLower() + ".qml"; + QUrl qmlSource = QUrl::fromLocalFile(qmlName); + + m_TutorialView->setSource(qmlSource); + m_TutorialView->resize(m_TargetControl->width(), m_TargetControl->height()); + m_TutorialView->rootContext()->setContextProperty("scriptName", tutorial); + m_TutorialView->rootContext()->setContextProperty("tutorialControl", this); + m_TutorialView->rootContext()->setContextProperty("applicationWindow", + m_TargetControl); + m_TutorialView->rootContext()->setContextProperty("organizer", + m_Manager.organizerCore()); + + for (std::vector>::const_iterator iter = + m_ExposedObjects.begin(); + iter != m_ExposedObjects.end(); ++iter) { + m_TutorialView->rootContext()->setContextProperty(iter->first, iter->second); + } + m_TutorialView->show(); + m_TutorialView->raise(); + if (!QMetaObject::invokeMethod(m_TutorialView->rootObject(), "init")) { + reportError(tr( + "Tutorial failed to start, please check \"mo_interface.log\" for details.")); + m_TutorialView->close(); + } + } +} + +void TutorialControl::lockUI(bool locked) +{ + m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, !locked); + + QMetaObject::invokeMethod(m_TutorialView->rootObject(), "enableBackground", + Q_ARG(QVariant, QVariant(locked))); +} + +void TutorialControl::simulateClick(int x, int y) +{ + bool wasTransparent = m_TutorialView->testAttribute(Qt::WA_TransparentForMouseEvents); + if (!wasTransparent) { + m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, true); + } + QWidget* hitControl = m_TargetControl->childAt(x, y); + QPoint globalPos = m_TargetControl->mapToGlobal(QPoint(x, y)); + QPoint hitPos = hitControl->mapFromGlobal(globalPos); + QMouseEvent* downEvent = new QMouseEvent( + QEvent::MouseButtonPress, QPointF(hitPos), QPointF(globalPos), + Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + QMouseEvent* upEvent = + new QMouseEvent(QEvent::MouseButtonRelease, QPointF(hitPos), QPointF(globalPos), + Qt::LeftButton, Qt::LeftButton, Qt::NoModifier); + + qApp->postEvent(hitControl, (QEvent*)downEvent); + qApp->postEvent(hitControl, (QEvent*)upEvent); + + if (!wasTransparent) { + m_TutorialView->setAttribute(Qt::WA_TransparentForMouseEvents, false); + } +} + +QWidget* TutorialControl::getChild(const QString& name) +{ + if (m_TargetControl != nullptr) { + return m_TargetControl->findChild(name); + } else { + return nullptr; + } +} + +void TutorialControl::finish() +{ + if (m_TutorialView != nullptr) { + m_TutorialView->deleteLater(); + } + m_TutorialView = nullptr; + m_TutorialView = nullptr; +} + +QRect TutorialControl::getRect(const QString& widgetName) +{ + if (m_TargetControl != nullptr) { + QWidget* widget = m_TargetControl->findChild(widgetName); + if (widget != nullptr) { + QRect res = widget->rect(); + QPoint pos = widget->mapTo(m_TargetControl, res.topLeft()); + res.moveTopLeft(pos); + return res; + } else { + log::error("{} not found", widgetName); + return QRect(); + } + } else { + return QRect(); + } +} + +QRect TutorialControl::getActionRect(const QString& widgetName) +{ + if (m_TargetControl != nullptr) { + QToolBar* toolBar = m_TargetControl->findChild("toolBar"); + foreach (QAction* action, toolBar->actions()) { + if (action->objectName() == widgetName) { + return toolBar->actionGeometry(action); + } + } + } + return QRect(); +} + +QRect TutorialControl::getMenuRect(const QString&) +{ + if (m_TargetControl != nullptr) { + QMenuBar* menuBar = m_TargetControl->findChild("menuBar"); + return menuBar->geometry(); + } + return QRect(); +} + +void TutorialControl::nextTutorialStepProxy() +{ + + if (m_TutorialView != nullptr) { + QObject* background = m_TutorialView->rootObject(); + + QTimer::singleShot(1, background, SLOT(nextStep())); + lockUI(true); + + bool success = true; + for (QMetaObject::Connection connection : m_Connections) { + if (!disconnect(connection)) { + success = false; + } + } + m_Connections.clear(); + if (!success) { + log::error("failed to disconnect tutorial proxy"); + } + } else { + log::error("failed to proceed to next tutorial step"); + finish(); + } +} + +void TutorialControl::tabChangedProxy(int selected) +{ + if ((m_TutorialView != nullptr) && (selected == m_ExpectedTab)) { + QObject* background = m_TutorialView->rootObject(); + QTimer::singleShot(1, background, SLOT(nextStep())); + lockUI(true); + bool success = true; + for (QMetaObject::Connection connection : m_Connections) { + if (!disconnect(connection)) { + success = false; + } + } + m_Connections.clear(); + if (!success) { + log::error("failed to disconnect tab-changed proxy"); + } + } +} + +bool TutorialControl::waitForAction(const QString& actionName) +{ + if (m_TargetControl != nullptr) { + QAction* action = m_TargetControl->findChild(actionName); + if (action == nullptr) { + log::error("no action \"{}\" in control \"{}\"", actionName, m_Name); + return false; + } + if (action->isEnabled()) { + if (action->menu() != nullptr) { + m_Connections.append(connect(action->menu(), SIGNAL(aboutToShow()), this, + SLOT(nextTutorialStepProxy()))); + } else { + m_Connections.append( + connect(action, SIGNAL(triggered()), this, SLOT(nextTutorialStepProxy()))); + } + lockUI(false); + return true; + } else { + return false; + } + } else { + return false; + } +} + +bool TutorialControl::waitForButton(const QString& buttonName) +{ + if (m_TargetControl != nullptr) { + QAbstractButton* button = m_TargetControl->findChild(buttonName); + if (button == nullptr) { + log::error("no button \"{}\" in control \"{}\"", buttonName, m_Name); + return false; + } + if (button->isEnabled()) { + m_Connections.append( + connect(button, SIGNAL(pressed()), this, SLOT(nextTutorialStepProxy()))); + lockUI(false); + return true; + } else { + return false; + } + } else { + return false; + } +} + +bool TutorialControl::waitForTabOpen(const QString& tabControlName, const QString& tab) +{ + if (m_TargetControl != nullptr) { + QTabWidget* tabWidget = m_TargetControl->findChild(tabControlName); + if (tabWidget == nullptr) { + log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name); + return false; + } + if (tabWidget->findChild(tab) == nullptr) { + log::error("no widget \"{}\" found in tab widget \"{}\"", tab, tabControlName); + return false; + } + int tabIndex = tabWidget->indexOf(tabWidget->findChild(tab)); + if (tabIndex == -1) { + log::error("widget \"{}\" does not appear to be a tab in tab widget \"{}\"", tab, + tabControlName); + return false; + } + if (tabWidget->isEnabled()) { + if (tabWidget->currentIndex() != tabIndex) { + m_ExpectedTab = tabIndex; + m_Connections.append(connect(tabWidget, SIGNAL(currentChanged(int)), this, + SLOT(tabChangedProxy(int)))); + lockUI(false); + } else { + QObject* background = m_TutorialView->rootObject(); + QTimer::singleShot(1, background, SLOT(nextStep())); + lockUI(true); + } + return true; + } else { + return false; + } + } else { + return false; + } +} + +const QString TutorialControl::getTabName(const QString& tabControlName) +{ + if (m_TargetControl != nullptr) { + QTabWidget* tabWidget = m_TargetControl->findChild(tabControlName); + if (tabWidget == nullptr) { + log::error("no tab widget \"{}\" in control \"{}\"", tabControlName, m_Name); + return QString(); + } + if (tabWidget->currentIndex() == -1) { + return QString(); + } else { + return tabWidget->currentWidget()->objectName(); + } + } else { + return QString(); + } +} +} // namespace MOBase -- cgit v1.3.1