mirror of
https://github.com/logos-co/logos-basecamp.git
synced 2026-08-31 08:41:15 +00:00
345 lines
14 KiB
C++
345 lines
14 KiB
C++
#include "MainContainer.h"
|
|
#include "AppsFilterProxy.h"
|
|
#include "InstallEnums.h"
|
|
#include "MainUIBackend.h"
|
|
#include "mdiview.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 <QTimer>
|
|
|
|
namespace {
|
|
constexpr int kAppsStackIndex = 0; // MdiView (C++ widget)
|
|
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_mdiView(nullptr)
|
|
, m_contentWidget(nullptr)
|
|
, m_overlayWidget(nullptr)
|
|
{
|
|
// Set QML style
|
|
QQuickStyle::setStyle("Basic");
|
|
|
|
// Create backend
|
|
m_backend = new MainUIBackend(m_logosAPI, this);
|
|
|
|
setupUi();
|
|
|
|
// 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();
|
|
}
|
|
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 window (tab/window X), notify backend to unload
|
|
connect(m_mdiView, &MdiView::pluginWindowClosed,
|
|
m_backend, &MainUIBackend::onPluginWindowClosed);
|
|
|
|
// 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)));
|
|
}
|
|
|
|
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: MdiView (C++ widget)
|
|
m_mdiView = new MdiView(m_backend, m_contentStack);
|
|
m_contentStack->addWidget(m_mdiView);
|
|
|
|
// Index 1: QML content views (Dashboard, Modules, PackageManager, Settings)
|
|
m_contentWidget = new QQuickWidget(m_contentStack);
|
|
m_contentWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
|
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);
|
|
|
|
// === BOTTOM TOOLBAR
|
|
m_bottomToolbar = new QQuickWidget(contentArea);
|
|
m_bottomToolbar->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
|
m_bottomToolbar->setClearColor(Qt::transparent);
|
|
m_bottomToolbar->setAttribute(Qt::WA_TranslucentBackground);
|
|
applyDevQmlImportPath(m_bottomToolbar->engine());
|
|
m_bottomToolbar->rootContext()->setContextProperty("backend", m_backend);
|
|
m_bottomToolbar->setSource(resolveQmlView(
|
|
QStringLiteral("Basecamp/Shell/BottomToolbar.qml"),
|
|
QStringLiteral("qrc:/qt/qml/Basecamp/Shell/Basecamp/Shell/BottomToolbar.qml")));
|
|
m_bottomToolbar->setFixedHeight(50);
|
|
|
|
// Add widgets to content layout (4px gap between stack and bottom toolbar)
|
|
contentLayout->addWidget(m_contentStack, 1);
|
|
contentLayout->addSpacing(4);
|
|
contentLayout->addWidget(m_bottomToolbar);
|
|
|
|
// 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)));
|
|
}
|
|
|
|
// Set initial state
|
|
m_contentStack->setCurrentIndex(kAppsStackIndex); // Show MdiView by default
|
|
|
|
// 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);
|
|
if (active) m_overlayWidget->raise();
|
|
}
|
|
|
|
void MainContainer::onViewIndexChanged()
|
|
{
|
|
int sectionIndex = m_backend->currentActiveSectionIndex();
|
|
|
|
qDebug() << "MainContainer: Active section index changed to" << sectionIndex;
|
|
|
|
// 0 (Workspace) → MdiView
|
|
// 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_mdiView && widget) {
|
|
m_mdiView->addPluginWindow(widget, title);
|
|
qDebug() << "MainContainer: Added plugin window to MdiView:" << title;
|
|
}
|
|
}
|
|
|
|
void MainContainer::onPluginWindowRemoveRequested(QWidget* widget)
|
|
{
|
|
if (widget && widget == m_pmuiWidget) return;
|
|
if (m_mdiView && widget) {
|
|
m_mdiView->removePluginWindow(widget);
|
|
qDebug() << "MainContainer: Removed plugin window from MdiView";
|
|
}
|
|
}
|
|
|
|
void MainContainer::onPluginWindowActivateRequested(QWidget* widget)
|
|
{
|
|
if (widget && widget == m_pmuiWidget) return;
|
|
if (m_mdiView && widget) {
|
|
m_mdiView->activatePluginWindow(widget);
|
|
qDebug() << "MainContainer: Activated plugin window in MdiView";
|
|
}
|
|
}
|
|
|