#include "MainContainer.h" #include "AppsFilterProxy.h" #include "InstallEnums.h" #include "MainUIBackend.h" #include "ShortcutBridge.h" #include "WorkspaceArea.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int kAppsStackIndex = 0; // WorkspaceArea (QDockWidget-based) constexpr int kContentStackIndex = 1; // ContentViews.qml (App Manager + Settings) constexpr int kModulesStackIndex = 2; // package_manager_ui (sandboxed QQuickWidget) // DEV_QML_PATH: when set, load QML view entry files from the filesystem source // tree instead of the embedded qrc resource QString devQmlRoot() { const QString dev = QString::fromUtf8(qgetenv("DEV_QML_PATH")).trimmed(); if (dev.isEmpty()) return QString(); if (!QFileInfo(dev).isDir()) { qWarning().noquote() << "DEV_QML_PATH is not a directory:" << dev << "- using embedded QML"; return QString(); } return dev; } QUrl resolveQmlView(const QString& relPath, const QString& qrcFallback) { const QString root = devQmlRoot(); if (root.isEmpty()) return QUrl(qrcFallback); const QString fullPath = QDir(root).absoluteFilePath(relPath); if (!QFile::exists(fullPath)) { qWarning().noquote() << "DEV_QML_PATH set but" << relPath << "not found at" << fullPath << "- using embedded QML"; return QUrl(qrcFallback); } qInfo().noquote() << "DEV_QML_PATH override active:" << fullPath; return QUrl::fromLocalFile(fullPath); } void applyDevQmlImportPath(QQmlEngine* engine) { const QString root = devQmlRoot(); if (!root.isEmpty()) engine->addImportPath(root); } } // namespace MainContainer::MainContainer(LogosAPI* logosAPI, QWidget* parent) : QWidget(parent) , m_logosAPI(logosAPI) , m_backend(nullptr) , m_sidebarWidget(nullptr) , m_contentStack(nullptr) , m_workspaceArea(nullptr) , m_contentWidget(nullptr) , m_overlayWidget(nullptr) { // Set QML style QQuickStyle::setStyle("Basic"); // Create backend m_backend = new MainUIBackend(m_logosAPI, this); setupUi(); // Provider lets the bridge scan the front-most dock's shortcuts // when Workspace is the current section. m_shortcutBridge = new ShortcutBridge(this, m_contentStack, [this]() { return m_workspaceArea ? m_workspaceArea->activeDockWidget() : nullptr; }); // Tab-switching inside the workspace changes the dock without // touching m_contentStack — force a rebind so the new dock's // shortcuts become live. if (m_workspaceArea) { connect(m_workspaceArea, &WorkspaceArea::activeAppChanged, m_shortcutBridge, [this](const QString&) { m_shortcutBridge->rebindDeferred(); }); } // Connect section index changes connect(m_backend, &MainUIBackend::currentActiveSectionIndexChanged, this, &MainContainer::onViewIndexChanged); connect(m_backend, &MainUIBackend::navigateToApps, this, [this]() { if (m_suppressNextNavToApps) { m_suppressNextNavToApps = false; return; } onNavigateToApps(); }); connect(m_backend, &MainUIBackend::pluginWindowRequested, this, [this](QWidget* widget, const QString& title) { if (title == QStringLiteral("package_manager_ui") && !m_pmuiWidget) { m_pmuiWidget = widget; QWidget* placeholder = m_contentStack->widget(kModulesStackIndex); widget->setParent(m_contentStack); m_contentStack->insertWidget(kModulesStackIndex, widget); if (placeholder) { m_contentStack->removeWidget(placeholder); placeholder->deleteLater(); } // A new pane's QML just materialised — ask the bridge to // pick up any Shortcut { } blocks inside it. Deferred, so // the QML tree has time to instantiate. if (m_shortcutBridge) m_shortcutBridge->rebindDeferred(); m_suppressNextNavToApps = true; return; } onPluginWindowRequested(widget, title); }); connect(m_backend, &MainUIBackend::pluginWindowRemoveRequested, this, &MainContainer::onPluginWindowRemoveRequested); connect(m_backend, &MainUIBackend::pluginWindowActivateRequested, this, &MainContainer::onPluginWindowActivateRequested); // When user closes a plugin tab (× button), notify backend to unload. connect(m_workspaceArea, &WorkspaceArea::pluginClosed, m_backend, &MainUIBackend::unloadUiModule); // Keep the sidebar's active-app highlight in sync with the front-most // dock — QDockWidget tab clicks bypass onAppLauncherClicked, so without // this the sidebar stays stuck on whichever app was last launched. connect(m_workspaceArea, &WorkspaceArea::activeAppChanged, m_backend, &MainUIBackend::setCurrentVisibleApp); // WelcomePage "Install now" CTA → jump to Applications view. connect(m_workspaceArea, &WorkspaceArea::installClicked, m_backend, [this]() { m_backend->setCurrentActiveSectionIndex(1); }); // Connect to QML signals from SidebarPanel. // // launchUIModule uses QueuedConnection — the signal is emitted from a // SidebarAppDelegate.onClicked handler inside a Repeater delegate. // onAppLauncherClicked calls setCurrentVisibleApp which synchronously // emits launcherAppsChanged, causing both sidebar Repeaters to reset // their models. If the connection were direct the Repeater would call // setParentItem(nullptr) on the clicked delegate while its click handler // is still on the call stack, leading to a null deref in // QQuickItemPrivate::derefWindow. Queuing the call lets the click handler // return before any Repeater model update fires. QObject* sidebarRoot = m_sidebarWidget->rootObject(); if (sidebarRoot) { connect(sidebarRoot, SIGNAL(launchUIModule(QString)), m_backend, SLOT(onAppLauncherClicked(QString)), Qt::QueuedConnection); connect(sidebarRoot, SIGNAL(updateLauncherIndex(int)), m_backend, SLOT(setCurrentActiveSectionIndex(int))); connect(sidebarRoot, SIGNAL(tooltipRequested(QString, qreal)), this, SLOT(onSidebarTooltipRequested(QString, qreal))); } qDebug() << "MainContainer created"; } MainContainer::~MainContainer() { qDebug() << "MainContainer destroyed"; } void MainContainer::setupUi() { // We would likely move this to qml and use Logos.Theme instead QColor bgColor("#171717"); // set background color setAutoFillBackground(true); QPalette p = palette(); p.setColor(QPalette::Window, bgColor); setPalette(p); // Create main horizontal layout m_mainLayout = new QHBoxLayout(this); m_mainLayout->setSpacing(0); m_mainLayout->setContentsMargins(4, 0, 4, 2); // === SIDEBAR (QML) === m_sidebarWidget = new QQuickWidget(this); m_sidebarWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); applyDevQmlImportPath(m_sidebarWidget->engine()); m_sidebarWidget->rootContext()->setContextProperty("backend", m_backend); m_sidebarWidget->setSource(resolveQmlView( QStringLiteral("Basecamp/Sidebar/SidebarPanel.qml"), QStringLiteral("qrc:/qt/qml/Basecamp/Sidebar/Basecamp/Sidebar/SidebarPanel.qml"))); m_sidebarWidget->setMinimumWidth(80); m_sidebarWidget->setMaximumWidth(80); // set clear color to sidebar so that rounded corners don't show white m_sidebarWidget->setClearColor(bgColor); // === CONTENT AREA (vertical layout with stack + app launcher) === QWidget* contentArea = new QWidget(this); QVBoxLayout* contentLayout = new QVBoxLayout(contentArea); contentLayout->setSpacing(0); contentLayout->setContentsMargins(4, 9, 4, 4); // Create content stack m_contentStack = new QStackedWidget(contentArea); m_contentStack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // Index 0: WorkspaceArea (QDockWidget-based) m_workspaceArea = new WorkspaceArea(m_backend, m_contentStack); m_contentStack->addWidget(m_workspaceArea); // Index 1: QML content views (Dashboard, Modules, PackageManager, Settings) m_contentWidget = new QQuickWidget(m_contentStack); m_contentWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); m_contentWidget->setClearColor(bgColor); applyDevQmlImportPath(m_contentWidget->engine()); m_contentWidget->rootContext()->setContextProperty("backend", m_backend); m_contentWidget->setSource(resolveQmlView( QStringLiteral("Basecamp/Shell/ContentViews.qml"), QStringLiteral("qrc:/qt/qml/Basecamp/Shell/Basecamp/Shell/ContentViews.qml"))); m_contentStack->addWidget(m_contentWidget); // Index 2: placeholder for package_manager_ui — shows a centered // "Loading…" label until PMUI's QQuickWidget arrives via the // pluginWindowRequested intercept QWidget* pmuiPlaceholder = new QWidget(m_contentStack); pmuiPlaceholder->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); { QVBoxLayout* phLayout = new QVBoxLayout(pmuiPlaceholder); phLayout->setAlignment(Qt::AlignCenter); QLabel* loadingLabel = new QLabel(QStringLiteral("Loading Package Manager…"), pmuiPlaceholder); loadingLabel->setAlignment(Qt::AlignCenter); loadingLabel->setStyleSheet(QStringLiteral( "color: #a0a0a0; font-size: 14px;")); phLayout->addWidget(loadingLabel); } m_contentStack->addWidget(pmuiPlaceholder); // Content stack fills the content area — the version footer that // used to live in a QML bottom toolbar is now inside SidebarPanel.qml. contentLayout->addWidget(m_contentStack, 1); // Add widgets to main layout m_mainLayout->addWidget(m_sidebarWidget); m_mainLayout->addWidget(contentArea, 1); // === OVERLAY DIALOGS (QML) === // Child of `this` but deliberately NOT added to m_mainLayout — we // want it to float across the whole window, overlapping sidebar + // content. resizeEvent keeps its geometry in sync with the parent. m_overlayWidget = new QQuickWidget(this); m_overlayWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); // Transparent clear so the sidebar + content stay visible through // the overlay. The dialog itself paints its own opaque background. m_overlayWidget->setAttribute(Qt::WA_AlwaysStackOnTop); m_overlayWidget->setAttribute(Qt::WA_TranslucentBackground); m_overlayWidget->setClearColor(Qt::transparent); // Start transparent-to-input so the user can interact with the // normal UI; flipped off in onOverlayActiveChanged while a dialog // is visible so the dialog itself can receive clicks. m_overlayWidget->setAttribute(Qt::WA_TransparentForMouseEvents, true); applyDevQmlImportPath(m_overlayWidget->engine()); m_overlayWidget->rootContext()->setContextProperty("backend", m_backend); m_overlayWidget->setSource(resolveQmlView( QStringLiteral("Basecamp/Shell/OverlayDialogs.qml"), QStringLiteral("qrc:/qt/qml/Basecamp/Shell/Basecamp/Shell/OverlayDialogs.qml"))); // Hook up the QML signal that tracks "any dialog visible" so we can // toggle mouse-passthrough on the overlay QQuickWidget. if (QObject* overlayRoot = m_overlayWidget->rootObject()) { connect(overlayRoot, SIGNAL(overlayActiveChanged(bool)), this, SLOT(onOverlayActiveChanged(bool))); } m_overlayWidget->setVisible(false); // Watch for the mouse leaving the sidebar so we can hide the tooltip // overlay. if (m_sidebarWidget) m_sidebarWidget->installEventFilter(this); // Set initial state — Apps section (workspace) visible by default. m_contentStack->setCurrentIndex(kAppsStackIndex); // Set reasonable minimum size setMinimumSize(800, 600); } void MainContainer::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); if (m_overlayWidget) { m_overlayWidget->setGeometry(0, 0, width(), height()); // Qt re-stacks siblings on resize in some cases; keep the // overlay on top explicitly. m_overlayWidget->raise(); } } void MainContainer::onOverlayActiveChanged(bool active) { if (!m_overlayWidget) return; // When a dialog is open, the overlay must catch the click on the // Cancel/Continue buttons — so make it opaque to input. When no // dialog is showing, pass every click through to the sidebar / // content behind it. m_overlayWidget->setAttribute(Qt::WA_TransparentForMouseEvents, !active); m_overlayWidget->setVisible(active); if (active) m_overlayWidget->raise(); } void MainContainer::onSidebarTooltipRequested(const QString& text, qreal y) { if (!m_overlayWidget) return; QObject* overlayRoot = m_overlayWidget->rootObject(); if (!overlayRoot) return; overlayRoot->setProperty("sidebarTooltipText", text); overlayRoot->setProperty("sidebarTooltipY", y); if (!m_overlayWidget->isVisible()) { m_overlayWidget->setVisible(true); m_overlayWidget->raise(); } } bool MainContainer::eventFilter(QObject* watched, QEvent* event) { if (watched == m_sidebarWidget && event->type() == QEvent::Leave) { if (m_overlayWidget) { QObject* overlayRoot = m_overlayWidget->rootObject(); if (overlayRoot) { overlayRoot->setProperty("sidebarTooltipText", QString()); const bool dialogUp = overlayRoot->property("anyDialogOpen").toBool(); if (!dialogUp) m_overlayWidget->setVisible(false); } } } return QWidget::eventFilter(watched, event); } void MainContainer::onViewIndexChanged() { int sectionIndex = m_backend->currentActiveSectionIndex(); qDebug() << "MainContainer: Active section index changed to" << sectionIndex; // 0 (Workspace) → WorkspaceArea (QDockWidget-based) // 1 (Applications) → ContentViews.qml (App Manager view) // 2 (Package Manager) → package_manager_ui (preloaded in background) // 3 (Settings) → ContentViews.qml (StackLayout picks the page) switch (sectionIndex) { case 0: m_contentStack->setCurrentIndex(kAppsStackIndex); break; case 1: m_contentStack->setCurrentIndex(kContentStackIndex); break; case 2: if (!m_pmuiWidget) { m_backend->loadUiModule(QStringLiteral("package_manager_ui")); } m_contentStack->setCurrentIndex(kModulesStackIndex); break; case 3: m_contentStack->setCurrentIndex(kContentStackIndex); break; default: break; } } void MainContainer::onNavigateToApps() { // This is called when an app is loaded and we need to switch to Apps view m_backend->setCurrentActiveSectionIndex(0); } void MainContainer::onPluginWindowRequested(QWidget* widget, const QString& title) { if (m_workspaceArea && widget) { const QString resolved = m_backend ? m_backend->displayNameFor(title) : title; const QString label = resolved.isEmpty() ? title : resolved; m_workspaceArea->addPluginDock(widget, title, label); qDebug() << "MainContainer: Added plugin dock to WorkspaceArea:" << label << "(module:" << title << ")"; } } void MainContainer::onPluginWindowRemoveRequested(QWidget* widget) { if (widget && widget == m_pmuiWidget) return; if (m_workspaceArea && widget) m_workspaceArea->removePluginDock(widget); } void MainContainer::onPluginWindowActivateRequested(QWidget* widget) { if (widget && widget == m_pmuiWidget) return; if (m_workspaceArea && widget) m_workspaceArea->activatePluginDock(widget); }