#include "WorkspaceArea.h" #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 #include #include #include #include #include #include namespace { class ZeroTitleWidget : public QWidget { public: explicit ZeroTitleWidget(QWidget* parent = nullptr) : QWidget(parent) { setFixedHeight(0); } QSize sizeHint() const override { return {0, 0}; } QSize minimumSizeHint() const override { return {0, 0}; } }; // Overlay that "punches" antialiased rounded corners into the dock // content. Sits above the plugin widget. In the 4 corner regions it // paints the shell color, hiding the plugin's square corners; the // interior is left transparent so the plugin shows through unmodified. // This is why the rounded edges look smooth — QPainter antialiasing on // the QPainterPath — instead of the staircase you get with setMask // (which is 1-bit region clipping, no antialiasing). // // Transparent-for-mouse so clicks pass through to the plugin. class CornerCutOverlay : public QWidget { public: explicit CornerCutOverlay(QWidget* parent = nullptr, QColor cutColor = QColor("#171717"), qreal radius = 16.0) : QWidget(parent), m_cutColor(cutColor), m_radius(radius) { setAttribute(Qt::WA_TransparentForMouseEvents); setAttribute(Qt::WA_TranslucentBackground); setAutoFillBackground(false); } protected: void paintEvent(QPaintEvent*) override { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); // Full rect minus rounded interior = 4 corner slivers. QPainterPath outer; outer.addRect(rect()); QPainterPath inner; inner.addRoundedRect(rect(), m_radius, m_radius); p.fillPath(outer.subtracted(inner), m_cutColor); } private: QColor m_cutColor; qreal m_radius; }; // Transparent wrapper around the plugin widget inside a dock. Gives us: // * 4px vertical gap between the tab bar and the plugin content — // via the layout's top contentsMargin. // * Antialiased 16px rounded corners — via CornerCutOverlay (see above). // Not using setMask because it's binary/staircase; not using // stylesheet border-radius because it doesn't clip children. class DockCard : public QWidget { public: explicit DockCard(QWidget* pluginWidget, QWidget* parent = nullptr) : QWidget(parent) { setObjectName(QStringLiteral("dockCard")); setAutoFillBackground(false); setAttribute(Qt::WA_TranslucentBackground); auto* lay = new QVBoxLayout(this); lay->setContentsMargins(0, 4, 0, 0); // 4px gap under the tab strip lay->setSpacing(0); if (pluginWidget) lay->addWidget(pluginWidget); m_cornerOverlay = new CornerCutOverlay(this); m_cornerOverlay->raise(); } QWidget* pluginWidget() const { if (!layout() || layout()->count() == 0) return nullptr; return layout()->itemAt(0)->widget(); } // Detach the plugin widget so the caller (UIPluginManager) retains // ownership — mirrors the ownership contract WorkspaceArea gives // its consumers. QWidget* releasePluginWidget() { if (!layout() || layout()->count() == 0) return nullptr; QLayoutItem* item = layout()->takeAt(0); QWidget* w = item ? item->widget() : nullptr; delete item; if (w) w->setParent(nullptr); return w; } protected: void resizeEvent(QResizeEvent* e) override { QWidget::resizeEvent(e); if (m_cornerOverlay) { // Cover only the plugin area (below the 4px top gap). If // the overlay covered the full rect, its top corner slivers // would extend into the 4px gap AND clip the plugin's real // top edge with shell-colored fills. m_cornerOverlay->setGeometry(0, kTopGap, width(), height() - kTopGap); m_cornerOverlay->raise(); // stay on top of plugin } } static constexpr int kTopGap = 4; private: CornerCutOverlay* m_cornerOverlay = nullptr; }; // DEV_QML_PATH helpers — mirror the ones in MainContainer.cpp so // WelcomePage.qml participates in the same live-edit flow. If we grow // more shell-level QQuickWidget hosts, extract to a shared header. QString devQmlRoot() { const QString dev = QString::fromUtf8(qgetenv("DEV_QML_PATH")).trimmed(); if (dev.isEmpty()) return QString(); if (!QFileInfo(dev).isDir()) 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)) return QUrl(qrcFallback); return QUrl::fromLocalFile(fullPath); } void applyDevQmlImportPath(QQmlEngine* engine) { const QString root = devQmlRoot(); if (!root.isEmpty()) engine->addImportPath(root); } constexpr int kTabBarInsetPx = 24; } // namespace WorkspaceArea::WorkspaceArea(QObject* backend, QWidget* parent) : QMainWindow(parent) { setWindowFlags(Qt::Widget); setDockOptions(QMainWindow::AllowNestedDocks | QMainWindow::AllowTabbedDocks | QMainWindow::AnimatedDocks | QMainWindow::GroupedDragging); setTabPosition(Qt::AllDockWidgetAreas, QTabWidget::North); setAutoFillBackground(true); { QPalette pal = palette(); pal.setColor(QPalette::Window, QColor("#171717")); setPalette(pal); } setContentsMargins(0, 0, 0, 0); if (backend) { m_welcomeWidget = new QQuickWidget(this); m_welcomeWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); m_welcomeWidget->setClearColor(QColor("#171717")); m_welcomeWidget->setMinimumSize(300, 200); applyDevQmlImportPath(m_welcomeWidget->engine()); m_welcomeWidget->rootContext()->setContextProperty("backend", backend); m_welcomeWidget->setSource(resolveQmlView( QStringLiteral("Basecamp/Shell/WelcomePage.qml"), QStringLiteral("qrc:/qt/qml/Basecamp/Shell/Basecamp/Shell/WelcomePage.qml"))); if (m_welcomeWidget->status() == QQuickWidget::Error) { qWarning() << "WorkspaceArea: WelcomePage.qml failed to load:" << m_welcomeWidget->errors(); } setCentralWidget(m_welcomeWidget); // Forward the QML installClicked signal up to consumers. if (QObject* rootObj = m_welcomeWidget->rootObject()) { connect(rootObj, SIGNAL(installClicked()), this, SIGNAL(installClicked())); } else { qWarning() << "WorkspaceArea: WelcomePage.qml loaded but " "rootObject is null — installClicked not wired."; } } else { auto* placeholder = new QWidget(this); placeholder->setMaximumSize(0, 0); setCentralWidget(placeholder); } installEventFilter(this); // DEV: Ctrl+Shift+L flips between tabbed and side-by-side. Hidden // toggle, no UI surface — wire a button to toggleLayoutModeForTesting() // once we're happy with the behavior. auto* sc = new QShortcut(QKeySequence("Ctrl+Shift+L"), this); connect(sc, &QShortcut::activated, this, &WorkspaceArea::toggleLayoutModeForTesting); } void WorkspaceArea::toggleLayoutModeForTesting() { QTimer::singleShot(0, this, [this]() { if (m_dockOrder.size() < 2) return; m_sideBySide = !m_sideBySide; QMainWindow::DockOptions opts = QMainWindow::AllowNestedDocks | QMainWindow::AnimatedDocks | QMainWindow::GroupedDragging; if (!m_sideBySide) opts |= QMainWindow::AllowTabbedDocks; setDockOptions(opts); for (const QString& n : m_dockOrder) if (auto* d = m_docks.value(n)) removeDockWidget(d); QDockWidget* prev = nullptr; for (int i = 0; i < m_dockOrder.size(); ++i) { auto* d = m_docks.value(m_dockOrder.at(i)); if (!d) continue; // Swap the title bar to match the new mode: Qt's default in // grid (built-in drag + close ×), ZeroTitleWidget in tabbed. if (auto* prevBar = d->titleBarWidget()) prevBar->deleteLater(); d->setTitleBarWidget(m_sideBySide ? nullptr : static_cast(new ZeroTitleWidget(d))); if (!prev) { addDockWidget(Qt::LeftDockWidgetArea, d); } else if (m_sideBySide) { placeDockInGrid(d, i); } else { tabifyDockWidget(prev, d); } d->setVisible(true); // removeDockWidget hides as a side effect if (auto* w = d->widget()) w->show(); prev = d; } m_firstDock = m_docks.value(m_dockOrder.first()); if (m_sideBySide) { // Equalize row widths + row heights so each cell is roughly the // same size. Rows aren't uniform on remove (Qt collapses splits), // but on toggle we get a clean grid. const int rows = (m_dockOrder.size() + kColsPerRow - 1) / kColsPerRow; QList firstOfEachRow; for (int r = 0; r < rows; ++r) { if (auto* d = m_docks.value(m_dockOrder.at(r * kColsPerRow))) firstOfEachRow.append(d); } if (rows > 1) { const int rowH = qMax(1, height() / rows); QList rowHeights(firstOfEachRow.size(), rowH); resizeDocks(firstOfEachRow, rowHeights, Qt::Vertical); } // Equalize columns within each row. for (int r = 0; r < rows; ++r) { QList row; const int start = r * kColsPerRow; const int end = qMin(start + kColsPerRow, int(m_dockOrder.size())); for (int i = start; i < end; ++i) if (auto* d = m_docks.value(m_dockOrder.at(i))) row.append(d); if (row.size() > 1) { const int colW = qMax(1, width() / row.size()); QList colWidths(row.size(), colW); resizeDocks(row, colWidths, Qt::Horizontal); } } } QTimer::singleShot(0, this, [this]() { styleAllTabBars(); }); }); } WorkspaceArea::~WorkspaceArea() = default; void WorkspaceArea::placeDockInGrid(QDockWidget* dock, int gridIndex) { // Precondition: caller guarantees gridIndex >= 1 (index 0 is the // anchor and goes through addDockWidget). gridIndex is the target // position — i.e., where `dock` will land — not the current // m_dockOrder size at the callsite. When toggling from tabbed to // grid we iterate all existing docks and pass their new index. const int col = gridIndex % kColsPerRow; const int row = gridIndex / kColsPerRow; if (col == 0) { // Start of a new row — split vertically off the first dock // of the previous row so this row anchors below it. const int prevRowFirst = (row - 1) * kColsPerRow; if (auto* anchor = m_docks.value(m_dockOrder.value(prevRowFirst))) { splitDockWidget(anchor, dock, Qt::Vertical); } else { // Fallback: no previous row (shouldn't happen when // gridIndex >= kColsPerRow) — just addDockWidget so we // don't leave the dock unparented. addDockWidget(Qt::LeftDockWidgetArea, dock); } } else { // Continue the current row — horizontal split against the // previous dock in this row (its m_dockOrder index is // gridIndex - 1). if (auto* leftNeighbor = m_docks.value(m_dockOrder.value(gridIndex - 1))) { splitDockWidget(leftNeighbor, dock, Qt::Horizontal); } else { addDockWidget(Qt::LeftDockWidgetArea, dock); } } } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- void WorkspaceArea::ensurePhantomTab() { if (m_sideBySide) return; // no tab bar in grid mode if (m_phantomDock) return; // already present if (!m_firstDock) return; // no real dock to tabify with // Only meaningful when there's exactly one real dock — otherwise // Qt's real second tab already gives us a tab bar. if (m_dockOrder.size() != 1) return; m_phantomDock = new QDockWidget(this); m_phantomDock->setObjectName(QStringLiteral("__phantom_tab_placeholder__")); m_phantomDock->setFeatures(QDockWidget::NoDockWidgetFeatures); m_phantomDock->setTitleBarWidget(new ZeroTitleWidget(m_phantomDock)); auto* placeholder = new QWidget; placeholder->setMaximumSize(0, 0); m_phantomDock->setWidget(placeholder); tabifyDockWidget(m_firstDock, m_phantomDock); m_firstDock->raise(); // keep the real dock's content shown m_firstDock->setFeatures(QDockWidget::DockWidgetClosable); setDockOptions(dockOptions() & ~QMainWindow::GroupedDragging); QTimer::singleShot(0, this, [this]() { if (!m_phantomDock) return; for (QTabBar* bar : findChildren()) { for (int i = 0; i < bar->count(); ++i) { if (bar->tabText(i).isEmpty()) { bar->setTabVisible(i, false); bar->setTabEnabled(i, false); // block drag/click return; } } } }); } void WorkspaceArea::removePhantom() { if (!m_phantomDock) return; QDockWidget* p = m_phantomDock; m_phantomDock.clear(); // null before destroying, so any concurrent // deferred callback sees "no phantom". removeDockWidget(p); delete p; if (m_firstDock) { m_firstDock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable); } setDockOptions(dockOptions() | QMainWindow::GroupedDragging); } void WorkspaceArea::addPluginDock(QWidget* pluginWidget, const QString& moduleName, const QString& displayLabel) { if (!pluginWidget || moduleName.isEmpty()) return; if (m_docks.contains(moduleName)) { activatePluginDock(moduleName); return; } const QString title = displayLabel.isEmpty() ? moduleName : displayLabel; auto* dock = new QDockWidget(title, this); dock->setObjectName(moduleName); dock->setWidget(new DockCard(pluginWidget, dock)); dock->setAllowedAreas(Qt::AllDockWidgetAreas); dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable); if (m_sideBySide) { dock->setTitleBarWidget(nullptr); } else { dock->setTitleBarWidget(new ZeroTitleWidget(dock)); } dock->installEventFilter(this); pluginWidget->installEventFilter(this); const QIcon icon = pluginWidget->windowIcon(); if (!icon.isNull()) dock->setWindowIcon(icon); if (!m_firstDock) { addDockWidget(Qt::LeftDockWidgetArea, dock); m_firstDock = dock; } else if (m_sideBySide) { placeDockInGrid(dock, m_dockOrder.size()); } else { removePhantom(); tabifyDockWidget(m_firstDock, dock); dock->raise(); } m_docks[moduleName] = dock; m_dockOrder.append(moduleName); QTimer::singleShot(0, this, [this, moduleName]() { styleAllTabBars(); activatePluginDock(moduleName); }); ensurePhantomTab(); updateQmlPluginActiveStates(); updateWelcomeVisibility(); } void WorkspaceArea::removePluginDock(const QString& name) { auto it = m_docks.find(name); if (it == m_docks.end()) return; QDockWidget* dock = it.value(); m_docks.erase(it); m_dockOrder.removeAll(name); removeDockWidget(dock); dock->deleteLater(); if (m_firstDock == dock) { m_firstDock = m_dockOrder.isEmpty() ? nullptr : m_docks.value(m_dockOrder.first()); } QTimer::singleShot(0, this, [this]() { styleAllTabBars(); }); if (m_dockOrder.isEmpty()) removePhantom(); else ensurePhantomTab(); updateQmlPluginActiveStates(); updateWelcomeVisibility(); } void WorkspaceArea::activatePluginDock(const QString& moduleName) { QDockWidget* dock = m_docks.value(moduleName); if (!dock) return; dock->show(); dock->raise(); const QString tabText = dock->windowTitle(); for (QTabBar* bar : findChildren()) { for (int i = 0; i < bar->count(); ++i) { if (bar->tabText(i) == tabText) { bar->setCurrentIndex(i); break; } } } updateQmlPluginActiveStates(); } QString WorkspaceArea::moduleNameForTabText(const QString& tabText) const { if (tabText.isEmpty()) return {}; for (auto it = m_docks.cbegin(); it != m_docks.cend(); ++it) { if (it.value() && it.value()->windowTitle() == tabText) return it.key(); } return {}; } QString WorkspaceArea::nameForWidget(QWidget* w) const { if (!w) return {}; for (auto it = m_docks.cbegin(); it != m_docks.cend(); ++it) { QWidget* dockChild = it.value()->widget(); if (dockChild == w) return it.key(); if (auto* card = dynamic_cast(dockChild)) { if (card->pluginWidget() == w) return it.key(); } } return {}; } void WorkspaceArea::removePluginDock(QWidget* pluginWidget) { const QString name = nameForWidget(pluginWidget); if (!name.isEmpty()) removePluginDock(name); } void WorkspaceArea::activatePluginDock(QWidget* pluginWidget) { const QString name = nameForWidget(pluginWidget); if (!name.isEmpty()) activatePluginDock(name); } // --------------------------------------------------------------------------- // Tab-bar styling (ported from MdiView::customizeTabBarStyle + // insetTabBarGeometry). // --------------------------------------------------------------------------- void WorkspaceArea::customizeTabBarStyle(QTabBar* tabBar) { if (!tabBar) return; tabBar->setDocumentMode(true); tabBar->setDrawBase(false); tabBar->setAutoHide(false); tabBar->setAutoFillBackground(false); tabBar->setElideMode(Qt::ElideRight); tabBar->setUsesScrollButtons(false); tabBar->setExpanding(false); tabBar->setIconSize(QSize(15, 15)); tabBar->setMovable(true); QScroller::grabGesture(tabBar, QScroller::TouchGesture); QScrollerProperties props = QScroller::scroller(tabBar)->scrollerProperties(); props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); props.setScrollMetric(QScrollerProperties::ScrollingCurve, QEasingCurve::OutCubic); QScroller::scroller(tabBar)->setScrollerProperties(props); tabBar->setStyleSheet(QStringLiteral(R"( QTabBar { background: #171717; border: none; qproperty-drawBase: false; } QTabBar::tab { background: #262626; color: #A4A4A4; padding: 0px 8px 0px 4px; margin-right: 10px; border-top-left-radius: 10px; border-top-right-radius: 10px; height: 20px; min-width: 120px; } QTabBar::tab:!selected { background: rgba(38, 38, 38, 0.6); color: #626262; } QTabBar::tab:hover { background: #262626; } )")); connect(tabBar, &QTabBar::tabCloseRequested, this, [this, tabBar](int index) { const QString moduleName = moduleNameForTabText(tabBar->tabText(index)); if (moduleName.isEmpty()) return; emit pluginClosed(moduleName); }); // Track tab-current changes so we can update plugin isActiveTab and // sync the sidebar's active-app highlight (backend.currentVisibleApp). connect(tabBar, &QTabBar::currentChanged, this, [this, tabBar](int index) { updateQmlPluginActiveStates(); const QString name = index >= 0 ? moduleNameForTabText(tabBar->tabText(index)) : QString(); emit activeAppChanged(name); }); } void WorkspaceArea::installTabBarCloseButtons(QTabBar* tabBar) { if (!tabBar) return; const QTabBar::ButtonPosition closeSide = QTabBar::LeftSide; for (int i = 0; i < tabBar->count(); ++i) { if (tabBar->tabText(i).isEmpty()) continue; QWidget* oldBtn = tabBar->tabButton(i, closeSide); if (oldBtn) { tabBar->setTabButton(i, closeSide, nullptr); oldBtn->deleteLater(); } auto* btn = new QToolButton(tabBar); btn->setIcon(qApp->style()->standardIcon(QStyle::SP_TitleBarCloseButton)); btn->setIconSize(QSize(12, 12)); btn->setFixedSize(12, 12); btn->setCursor(Qt::PointingHandCursor); btn->setStyleSheet(QStringLiteral(R"( QToolButton { background: transparent; border: none; } QToolButton:hover { background: rgba(255,255,255,0.1); border-radius: 6px; } )")); connect(btn, &QToolButton::clicked, this, [tabBar, btn, closeSide]() { for (int j = 0; j < tabBar->count(); ++j) { if (tabBar->tabButton(j, closeSide) == btn) { emit tabBar->tabCloseRequested(j); break; } } }); btn->setVisible(false); // hover-show only btn->installEventFilter(this); tabBar->setTabButton(i, closeSide, btn); } tabBar->setMouseTracking(true); } void WorkspaceArea::insetTabBarGeometry(QTabBar* tabBar, int insetPx) { if (!tabBar) return; QWidget* p = tabBar->parentWidget(); if (!p) return; QRect g = tabBar->geometry(); tabBar->setGeometry(insetPx, g.y(), p->width() - insetPx - kTabBarInsetPx, g.height()); } void WorkspaceArea::styleAllTabBars() { static const char* kStyledMarker = "logosStyled"; for (auto* tabBar : findChildren()) { if (!tabBar->property(kStyledMarker).toBool()) { tabBar->setProperty(kStyledMarker, true); customizeTabBarStyle(tabBar); tabBar->installEventFilter(this); } // Re-run close-button install in case Qt grew the tab count. installTabBarCloseButtons(tabBar); for (int i = 0; i < tabBar->count(); ++i) { const QString moduleName = moduleNameForTabText(tabBar->tabText(i)); if (auto* dock = m_docks.value(moduleName)) { const QIcon icon = dock->windowIcon(); if (!icon.isNull()) tabBar->setTabIcon(i, icon); } } if (tabBar->x() != kTabBarInsetPx) insetTabBarGeometry(tabBar, kTabBarInsetPx); } } // --------------------------------------------------------------------------- // Plugin active-state propagation (port of MdiView::updateQmlPluginActiveStates) // --------------------------------------------------------------------------- void WorkspaceArea::updateWelcomeVisibility() { if (!m_welcomeWidget) return; m_welcomeWidget->setVisible(m_docks.isEmpty()); } QQuickWidget* WorkspaceArea::activeDockWidget() const { for (auto it = m_docks.cbegin(); it != m_docks.cend(); ++it) { QDockWidget* dock = it.value(); if (!dock || dock->visibleRegion().isEmpty()) continue; QWidget* dockChild = dock->widget(); if (auto* card = dynamic_cast(dockChild)) dockChild = card->pluginWidget(); if (auto* qw = qobject_cast(dockChild)) return qw; } return nullptr; } void WorkspaceArea::updateQmlPluginActiveStates() { const bool workspaceVisible = isVisible(); for (auto it = m_docks.cbegin(); it != m_docks.cend(); ++it) { QDockWidget* dock = it.value(); QWidget* dockChild = dock->widget(); // Plugin widgets are wrapped in DockCard; unwrap before casting. if (auto* card = dynamic_cast(dockChild)) dockChild = card->pluginWidget(); auto* qmlWidget = qobject_cast(dockChild); if (!qmlWidget) continue; const bool isActive = workspaceVisible && !dock->visibleRegion().isEmpty(); qmlWidget->rootContext()->setContextProperty( "isActiveTab", isActive); } } // --------------------------------------------------------------------------- // Event filter — hover-show for close buttons + wheel-scroll between tabs. // Ported from MdiView's eventFilter, minus the "+" add-button branch. // --------------------------------------------------------------------------- bool WorkspaceArea::eventFilter(QObject* watched, QEvent* event) { if (watched == this && event->type() == QEvent::ChildAdded) { QTimer::singleShot(0, this, [this]() { styleAllTabBars(); }); } if (auto* tabBar = qobject_cast(watched)) { switch (event->type()) { case QEvent::Resize: case QEvent::Move: { if (tabBar->x() != kTabBarInsetPx) insetTabBarGeometry(tabBar, kTabBarInsetPx); break; } case QEvent::MouseMove: { const QPoint pos = static_cast(event)->position().toPoint(); for (int i = 0; i < tabBar->count(); ++i) { if (!tabBar->isTabVisible(i)) continue; // skip phantom QWidget* closeBtn = tabBar->tabButton(i, QTabBar::LeftSide); if (closeBtn) { const QRect tabRect = tabBar->tabRect(i); const bool overTabOrButton = tabRect.contains(pos) || closeBtn->geometry().contains(pos); closeBtn->setVisible(overTabOrButton); } } break; } case QEvent::Leave: { for (int i = 0; i < tabBar->count(); ++i) { if (!tabBar->isTabVisible(i)) continue; // skip phantom QWidget* closeBtn = tabBar->tabButton(i, QTabBar::LeftSide); if (closeBtn) closeBtn->setVisible(false); } break; } case QEvent::Wheel: { if (tabBar->count() <= 1) break; auto* wheelEvent = static_cast(event); int delta = 0; if (!wheelEvent->pixelDelta().isNull()) delta = wheelEvent->pixelDelta().x(); else if (!wheelEvent->angleDelta().isNull()) delta = wheelEvent->angleDelta().x() / 2; if (delta != 0) { const int next = qBound(0, tabBar->currentIndex() + (delta > 0 ? -1 : 1), tabBar->count() - 1); if (next != tabBar->currentIndex()) { tabBar->setCurrentIndex(next); updateQmlPluginActiveStates(); return true; } } break; } default: break; } } // Close button hover persistence — show when pointer enters the // button itself, hide on leave. if (event->type() == QEvent::Enter || event->type() == QEvent::Leave) { if (auto* btn = qobject_cast(watched)) { if (qobject_cast(btn->parent())) { btn->setVisible(event->type() == QEvent::Enter); return false; } } } if (event->type() == QEvent::Close) { if (auto* dock = qobject_cast(watched)) { const QString moduleName = m_docks.key(dock); if (!moduleName.isEmpty()) { emit pluginClosed(moduleName); event->ignore(); return true; } } } // Plugin widget's windowIcon was refreshed if (event->type() == QEvent::WindowIconChange) { if (auto* widget = qobject_cast(watched)) { for (auto it = m_docks.cbegin(); it != m_docks.cend(); ++it) { QWidget* dw = it.value()->widget(); if (auto* card = dynamic_cast(dw)) dw = card->pluginWidget(); if (dw != widget) continue; const QIcon icon = widget->windowIcon(); it.value()->setWindowIcon(icon); const QString& name = it.key(); for (QTabBar* tabBar : findChildren()) { for (int i = 0; i < tabBar->count(); ++i) { if (moduleNameForTabText(tabBar->tabText(i)) == name) tabBar->setTabIcon(i, icon); } } break; } } } return QMainWindow::eventFilter(watched, event); } void WorkspaceArea::hideEvent(QHideEvent* event) { QMainWindow::hideEvent(event); updateQmlPluginActiveStates(); } void WorkspaceArea::showEvent(QShowEvent* event) { QMainWindow::showEvent(event); updateQmlPluginActiveStates(); }