#ifndef ADAPTIXCLIENT_ABSTRACTDOCK_H #define ADAPTIXCLIENT_ABSTRACTDOCK_H #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef __GNUC__ #include #endif /** * @brief Base class for all dock widgets with tab blinking support on updates. * * AUTOMATIC BLINK: * - Enabled by default for QTableView, QTreeWidget, QTreeView, QTextEdit, QPlainTextEdit * - Triggers automatically when rows are inserted into model or text changes * - 100ms debounce prevents too frequent triggers * - blink clears when user scrolls to see new content (not just on tab switch) * * DISABLE FOR SPECIFIC WIDGET: * @code * // In widget constructor: * setAutoBlinkEnabled(false); // Completely disable auto-blinks * @endcode * * TEMPORARY DISABLE (e.g., during bulk data loading): * @code * void MyWidget::loadBulkData() { * AutoBlinkGuard guard(this); // Disables blinks * // ... bulk loading ... * } // Blinks are re-enabled automatically * @endcode * * MANUAL CALL (for custom widgets): * @code * blinkNewContent(); // Highlights the tab if inactive * @endcode */ class DockTab : public QWidget { Q_OBJECT protected: KDDockWidgets::QtWidgets::DockWidget* dockWidget; mutable QString m_cachedClassName; bool m_autoBlinkEnabled = true; QTimer* m_debounceTimer = nullptr; QSet m_newTableRows; int m_newTextPosition = -1; QSet m_trackedViews; QSet m_trackedTextEdits; QString getClassName() const { if (!m_cachedClassName.isEmpty()) return m_cachedClassName; #ifdef __GNUC__ int status; char* demangled = abi::__cxa_demangle(typeid(*this).name(), nullptr, nullptr, &status); m_cachedClassName = (status == 0 && demangled) ? QString(demangled) : QString(typeid(*this).name()); free(demangled); #else m_cachedClassName = QString(typeid(*this).name()); #endif return m_cachedClassName; } public: DockTab(const QString &tabName, const QString &projectName, const QString &icon = "") { dockWidget = new KDDockWidgets::QtWidgets::DockWidget(tabName + ":Dock-" + projectName, KDDockWidgets::DockWidgetOption_None, KDDockWidgets::LayoutSaverOption::None); dockWidget->setTitle(tabName); if (!icon.isEmpty()) dockWidget->setIcon(QIcon(icon), KDDockWidgets::IconPlace::TabBar); connect(dockWidget, &KDDockWidgets::QtWidgets::DockWidget::isCurrentTabChanged, this, &DockTab::onCurrentTabChanged); QTimer::singleShot(0, this, &DockTab::setupAutoBlink); }; ~DockTab() override { if (dockWidget) { dockWidget->setWidget(nullptr); delete dockWidget; dockWidget = nullptr; } }; KDDockWidgets::QtWidgets::DockWidget* dock() { return this->dockWidget; }; void setAutoBlinkEnabled(bool enabled) { m_autoBlinkEnabled = enabled; } bool isAutoBlinkEnabled() const { return m_autoBlinkEnabled; } void blinkNewContent() { if (GlobalClient && GlobalClient->settings) { auto& data = GlobalClient->settings->data; if (!data.TabBlinkEnabled) return; QString className = getClassName(); if (data.BlinkWidgets.contains(className) && !data.BlinkWidgets[className]) return; } auto* coreDw = dockWidget->dockWidget(); if (!coreDw) return; if (!coreDw->isCurrentTab()) { if (auto tabBar = getTabBar()) { int index = getTabIndex(); if (index >= 0) { tabBar->setTabHighlighted(index, true); } } } } protected: KDDockWidgets::QtWidgets::TabBar* getTabBar() const { auto* group = dockWidget->group(); if (!group) return nullptr; auto* stack = group->stack(); if (!stack) return nullptr; auto* coreTabBar = stack->tabBar(); if (!coreTabBar) return nullptr; return static_cast( KDDockWidgets::QtCommon::View_qt::asQWidget(static_cast(coreTabBar)) ); } int getTabIndex() const { auto* coreDw = dockWidget->dockWidget(); if (!coreDw) return -1; auto* group = dockWidget->group(); if (!group) return -1; auto* stack = group->stack(); if (!stack) return -1; return stack->tabBar()->indexOfDockWidget(coreDw); } void clearHighlight() { if (auto tabBar = getTabBar()) { int index = getTabIndex(); if (index >= 0 && tabBar->isTabHighlighted(index)) { tabBar->setTabHighlighted(index, false); } } m_newTableRows.clear(); m_newTextPosition = -1; } bool hasNewContent() const { return !m_newTableRows.isEmpty() || m_newTextPosition >= 0; } private Q_SLOTS: void onCurrentTabChanged(bool isCurrent) { if (isCurrent) { clearHighlight(); } } void setupAutoBlink() { connectChildWidgets(this); } void onTableRowsInserted(const QModelIndex &parent, int first, int last) { Q_UNUSED(parent) if (!m_autoBlinkEnabled) return; for (int i = first; i <= last; ++i) { m_newTableRows.insert(i); } triggerBlink(); } void onTextChanged() { if (!m_autoBlinkEnabled) return; if (auto* textEdit = qobject_cast(sender())) { m_newTextPosition = textEdit->document()->blockCount() - 1; } else if (auto* plainTextEdit = qobject_cast(sender())) { m_newTextPosition = plainTextEdit->document()->blockCount() - 1; } triggerBlink(); } void triggerBlink() { if (!m_debounceTimer) { m_debounceTimer = new QTimer(this); m_debounceTimer->setSingleShot(true); connect(m_debounceTimer, &QTimer::timeout, this, [this]() { blinkNewContent(); }); } if (!m_debounceTimer->isActive()) { m_debounceTimer->start(100); } } void onScroll() { checkNewContentVisibility(); } void checkNewContentVisibility() { auto* coreDw = dockWidget->dockWidget(); if (!coreDw || !coreDw->isCurrentTab()) return; if (!hasNewContent()) { clearHighlight(); return; } bool hasVisibleViews = false; if (!m_newTableRows.isEmpty()) { for (auto* view : m_trackedViews) { if (!view->isVisible()) continue; hasVisibleViews = true; QSet stillHidden; for (int row : m_newTableRows) { QModelIndex idx = view->model()->index(row, 0); QRect rect = view->visualRect(idx); if (!view->viewport()->rect().intersects(rect)) { stillHidden.insert(row); } } m_newTableRows = stillHidden; } if (!hasVisibleViews && !m_trackedViews.isEmpty()) { m_newTableRows.clear(); } } if (m_newTextPosition >= 0) { bool hasVisibleEdits = false; for (auto* scrollArea : m_trackedTextEdits) { if (!scrollArea->isVisible()) continue; hasVisibleEdits = true; QScrollBar* vbar = scrollArea->verticalScrollBar(); if (vbar && vbar->value() >= vbar->maximum() - 10) { m_newTextPosition = -1; break; } } if (!hasVisibleEdits && !m_trackedTextEdits.isEmpty()) { m_newTextPosition = -1; } } if (!hasNewContent()) { clearHighlight(); } } private: template void connectItemView(T* view) { if (!view) return; m_trackedViews.insert(view); auto* model = view->model(); if (model && model->metaObject()) { connect(model, &QAbstractItemModel::rowsInserted, this, &DockTab::onTableRowsInserted, Qt::UniqueConnection); } auto* vbar = view->verticalScrollBar(); if (vbar && vbar->metaObject()) { connect(vbar, &QScrollBar::valueChanged, this, &DockTab::onScroll, Qt::UniqueConnection); } } template void connectTextEdit(T* edit, Signal signal) { if (!edit || !edit->metaObject()) return; m_trackedTextEdits.insert(edit); connect(edit, signal, this, &DockTab::onTextChanged, Qt::UniqueConnection); auto* vbar = edit->verticalScrollBar(); if (vbar && vbar->metaObject()) { connect(vbar, &QScrollBar::valueChanged, this, &DockTab::onScroll, Qt::UniqueConnection); } } void connectChildWidgets(const QWidget* parent) { for (auto* w : parent->findChildren()) connectItemView(w); for (auto* w : parent->findChildren()) connectItemView(w); for (auto* w : parent->findChildren()) if (!qobject_cast(w)) connectItemView(w); for (auto* w : parent->findChildren()) connectTextEdit(w, &QTextEdit::textChanged); for (auto* w : parent->findChildren()) connectTextEdit(w, &QPlainTextEdit::textChanged); } }; /// RAII class for temporarily disabling auto-blinks /// Usage: /// { /// AutoBlinkGuard guard(this); // disables blinks /// // ... data loading ... /// } // blinks are re-enabled automatically class AutoBlinkGuard { public: explicit AutoBlinkGuard(DockTab* tab) : m_tab(tab), m_wasEnabled(tab->isAutoBlinkEnabled()) { m_tab->setAutoBlinkEnabled(false); } ~AutoBlinkGuard() { m_tab->setAutoBlinkEnabled(m_wasEnabled); } AutoBlinkGuard(const AutoBlinkGuard&) = delete; AutoBlinkGuard& operator=(const AutoBlinkGuard&) = delete; private: DockTab* m_tab; bool m_wasEnabled; }; #endif //ADAPTIXCLIENT_ABSTRACTDOCK_H