Files
logos-basecamp/src/MainContainer.cpp

400 lines
16 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "MainContainer.h"
#include "AppsFilterProxy.h"
#include "InstallEnums.h"
#include "MainUIBackend.h"
#include "ShortcutBridge.h"
#include "WorkspaceArea.h"
#include <QQuickWidget>
#include <QQmlEngine>
#include <QQmlContext>
#include <QQuickStyle>
#include <qqml.h>
#include <QVBoxLayout>
#include <QLabel>
#include <QDebug>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QQuickItem>
#include <QColor>
#include <QPalette>
#include <QEvent>
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);
}