/* This file is part of KDDockWidgets. SPDX-FileCopyrightText: 2019 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Sérgio Martins SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only Contact KDAB at for commercial licensing options. */ #include "TabBar.h" #include "DockWidget.h" #include "Stack.h" #include "kddockwidgets/core/DockWidget.h" #include "kddockwidgets/core/TabBar.h" #include "kddockwidgets/core/Stack.h" #include "core/Utils_p.h" #include "core/TabBar_p.h" #include "core/Logging_p.h" #include "Config.h" #include "qtwidgets/ViewFactory.h" #include "kddockwidgets/core/DockRegistry.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KDDockWidgets; using namespace KDDockWidgets::QtWidgets; namespace KDDockWidgets { class TabCloseButtonFilter : public QObject { public: explicit TabCloseButtonFilter(QObject* parent) : QObject(parent) {} bool eventFilter(QObject* obj, QEvent* event) override { if (event->type() != QEvent::Paint) return false; auto* btn = qobject_cast(obj); if (!btn) return false; auto* tabBar = qobject_cast(btn->parentWidget()); if (!tabBar) return false; const QRect rect = btn->rect(); const int tabIndex = tabBar->tabAt(btn->mapToParent(rect.center())); const bool tabSelected = (tabBar->currentIndex() == tabIndex); bool tabHovered = false; if (tabBar->underMouse()) { const auto mousePos = tabBar->mapFromGlobal(QCursor::pos()); tabHovered = (tabBar->tabAt(mousePos) == tabIndex); } if (!tabSelected && !tabHovered) return true; const bool buttonHovered = btn->underMouse(); const bool buttonPressed = btn->isDown(); QPainter p(btn); p.setRenderHint(QPainter::Antialiasing, true); if (buttonHovered || buttonPressed) { const auto radius = rect.height() / 2.0; QColor bgColor = tabBar->palette().color(QPalette::WindowText); bgColor.setAlphaF(buttonPressed ? 0.2f : 0.1f); p.setPen(Qt::NoPen); p.setBrush(bgColor); p.drawRoundedRect(rect, radius, radius); } QColor fgColor = tabBar->palette().color(QPalette::WindowText); if (!tabSelected && !buttonHovered) fgColor.setAlphaF(0.5f); const int iconSz = qRound(qMin(rect.width(), rect.height()) * 0.38); const QRectF closeRect( rect.x() + (rect.width() - iconSz) / 2.0, rect.y() + (rect.height() - iconSz) / 2.0, iconSz, iconSz ); p.setPen(QPen(fgColor, 1.2, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); p.setBrush(Qt::NoBrush); p.drawLine(closeRect.topLeft(), closeRect.bottomRight()); p.drawLine(closeRect.topRight(), closeRect.bottomLeft()); return true; } }; class ScrollButtonFilter : public QObject { public: explicit ScrollButtonFilter(bool isLeft, QObject* parent) : QObject(parent), m_isLeft(isLeft) {} bool eventFilter(QObject* obj, QEvent* event) override { if (event->type() != QEvent::Paint) return false; auto* btn = qobject_cast(obj); if (!btn) return false; QPainter p(btn); p.setRenderHint(QPainter::Antialiasing, true); const QRect r = btn->rect(); QColor bgColor = btn->palette().color(QPalette::Window); p.fillRect(r, bgColor); if (btn->underMouse()) { QColor hover = btn->palette().color(QPalette::WindowText); hover.setAlphaF(0.08f); p.fillRect(r, hover); } QColor fgColor = btn->palette().color(QPalette::WindowText); if (!btn->isEnabled()) fgColor.setAlphaF(0.3f); const int arrowH = qRound(r.height() * 0.3); const int arrowW = qRound(arrowH * 0.5); const int cx = r.center().x(); const int cy = r.center().y(); p.setPen(QPen(fgColor, 1.4, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin)); p.setBrush(Qt::NoBrush); if (m_isLeft) { p.drawLine(QPoint(cx + arrowW/2, cy - arrowH/2), QPoint(cx - arrowW/2, cy)); p.drawLine(QPoint(cx - arrowW/2, cy), QPoint(cx + arrowW/2, cy + arrowH/2)); } else { p.drawLine(QPoint(cx - arrowW/2, cy - arrowH/2), QPoint(cx + arrowW/2, cy)); p.drawLine(QPoint(cx + arrowW/2, cy), QPoint(cx - arrowW/2, cy + arrowH/2)); } return true; } private: bool m_isLeft; }; QStyle* TabBarProxyStyle::appStyle() const { return QApplication::style(); } TabBarProxyStyle::TabBarProxyStyle(TabBar* tabBar) : QProxyStyle(), m_tabBar(tabBar){} int TabBarProxyStyle::styleHint(StyleHint hint, const QStyleOption *option, const QWidget *widget, QStyleHintReturn *returnData) const { if (hint == SH_Widget_Animation_Duration) return 0; return appStyle()->styleHint(hint, option, widget, returnData); } void TabBarProxyStyle::drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const { appStyle()->drawPrimitive(element, option, painter, widget); } QRect TabBarProxyStyle::subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget) const { if (element == SE_TabBarScrollLeftButton || element == SE_TabBarScrollRightButton) { const auto& rect = option->rect; const int compactW = qMax(20, rect.height() / 2 + 4); const int h = rect.height(); if (element == SE_TabBarScrollLeftButton) { const int x = rect.x() + rect.width() - 2 * compactW; return { x, rect.y(), compactW, h }; } else { const int x = rect.x() + rect.width() - compactW; return { x, rect.y(), compactW, h }; } } if (element == SE_TabBarTabRightButton || element == SE_TabBarTabLeftButton) { if (const auto *tab = qstyleoption_cast(option)) { bool fixRight = (element == SE_TabBarTabRightButton && tab->rightButtonSize.isEmpty()); bool fixLeft = (element == SE_TabBarTabLeftButton && tab->leftButtonSize.isEmpty()); if (fixRight || fixLeft) { int w = appStyle()->pixelMetric(PM_TabCloseIndicatorWidth, option, widget); int h = appStyle()->pixelMetric(PM_TabCloseIndicatorHeight, option, widget); QStyleOptionTab fixedOpt = *tab; if (fixRight) fixedOpt.rightButtonSize = QSize(w, h); if (fixLeft) fixedOpt.leftButtonSize = QSize(w, h); return appStyle()->subElementRect(element, &fixedOpt, widget); } } } return appStyle()->subElementRect(element, option, widget); } QSize TabBarProxyStyle::sizeFromContents(ContentsType type, const QStyleOption *option, const QSize &size, const QWidget *widget) const { if (type == CT_TabBarTab) { if (const auto *tab = qstyleoption_cast(option)) { const auto *tabBar = qobject_cast(widget); if (tabBar && tabBar->tabsClosable() && tab->rightButtonSize.isEmpty()) { QStyleOptionTab fixedOpt = *tab; int w = appStyle()->pixelMetric(PM_TabCloseIndicatorWidth, option, widget); int h = appStyle()->pixelMetric(PM_TabCloseIndicatorHeight, option, widget); fixedOpt.rightButtonSize = QSize(w, h); return appStyle()->sizeFromContents(type, &fixedOpt, size, widget); } } } return appStyle()->sizeFromContents(type, option, size, widget); } int TabBarProxyStyle::pixelMetric(PixelMetric metric, const QStyleOption *option, const QWidget *widget) const { if (metric == PM_TabBarScrollButtonWidth) { const int tabH = appStyle()->pixelMetric(PM_TabBarTabVSpace, option, widget); return qMax(20, tabH > 0 ? tabH : 24); } return appStyle()->pixelMetric(metric, option, widget); } QIcon TabBarProxyStyle::standardIcon(StandardPixmap standardIcon, const QStyleOption *option, const QWidget *widget) const { return appStyle()->standardIcon(standardIcon, option, widget); } QPalette TabBarProxyStyle::standardPalette() const { return appStyle()->standardPalette(); } void TabBarProxyStyle::drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const { if (element == CE_TabBarTabLabel && m_tabBar && m_tabBar->count() > 0) { const QStyleOptionTab *tab = qstyleoption_cast(option); if (tab) { int tabIndex = m_tabBar->tabIndexFromRect(tab->rect); if (tabIndex >= 0 && tabIndex < m_tabBar->count() && m_tabBar->isTabHighlighted(tabIndex) && tabIndex != m_tabBar->currentIndex()) { const int iconSz = appStyle()->pixelMetric(PM_TabBarIconSize, tab, widget); const int iconSpacing = 6; const int iconPadding = 8; QRect iconRect; if (!tab->icon.isNull()) { iconRect = QRect(tab->rect.left() + iconPadding, tab->rect.center().y() - iconSz/2, iconSz, iconSz); tab->icon.paint(painter, iconRect); } QRect textRect = tab->rect; if (!tab->icon.isNull()) { textRect.setLeft(iconRect.right() + iconSpacing); } textRect.adjust(iconPadding, 0, -iconPadding, 0); painter->save(); QColor highlightColor = m_tabBar->currentHighlightColor(); painter->setPen(highlightColor); painter->setFont(m_tabBar->font()); painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, tab->text); painter->restore(); return; } } } appStyle()->drawControl(element, option, painter, widget); } class QtWidgets::TabBar::Private { public: explicit Private(Core::TabBar *controller) : m_controller(controller) { } void onTabMoved(int from, int to); Core::TabBar *const m_controller; KDBindings::ScopedConnection m_currentDockWidgetChangedConnection; int wheelDeltaAccumulator = 0; int scrollOffset = 0; int targetScrollOffset = 0; QTimer *scrollAnimationTimer = nullptr; }; } TabBar::TabBar(Core::TabBar *controller, QWidget *parent) : View(controller, Core::ViewType::TabBar, parent) , TabBarViewInterface(controller) , d(new Private(controller)) { setShape(Config::self().tabsAtBottom() ? QTabBar::RoundedSouth : QTabBar::RoundedNorth); setStyle(new TabBarProxyStyle(this)); setUsesScrollButtons(true); setExpanding(false); setElideMode(Qt::ElideNone); d->scrollAnimationTimer = new QTimer(this); d->scrollAnimationTimer->setInterval(16); connect(d->scrollAnimationTimer, &QTimer::timeout, this, &TabBar::performSmoothScroll); } TabBar::~TabBar() { delete d; } void TabBar::init() { connect(this, &QTabBar::currentChanged, m_tabBar, &Core::TabBar::setCurrentIndex); connect(this, &QTabBar::tabMoved, this, [this](int from, int to) { d->onTabMoved(from, to); }); d->m_currentDockWidgetChangedConnection = d->m_controller->dptr()->currentDockWidgetChanged.connect([this](KDDockWidgets::Core::DockWidget *dw) { Q_EMIT currentDockWidgetChanged(dw); }); connect(this, &QTabBar::currentChanged, this, [this](int index) { Q_UNUSED(index) if (!m_highlightedTabs.isEmpty()) { update(); } }); QTimer::singleShot(0, this, [this]() { updateScrollButtonsColors(); }); } int TabBar::tabAt(QPoint localPos) const { return QTabBar::tabAt(localPos); } void TabBar::mousePressEvent(QMouseEvent *e) { d->m_controller->onMousePress(e->pos()); QTabBar::mousePressEvent(e); } void TabBar::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::MiddleButton) { const int index = tabAt(e->pos()); if (index >= 0) { if (auto stack = d->m_controller->stack()) { if (auto dw = stack->tabBar()->dockWidgetAt(index)) { if (dw->options() & DockWidgetOption_NotClosable) { qWarning() << "TabBar::mouseReleaseEvent: Refusing to close dock widget with " "Option_NotClosable option. name=" << dw->uniqueName(); } else { dw->view()->close(); } } } e->accept(); return; } } QTabBar::mouseReleaseEvent(e); } void TabBar::mouseMoveEvent(QMouseEvent *e) { if (count() > 1) { QTabBar::mouseMoveEvent(e); } } void TabBar::mouseDoubleClickEvent(QMouseEvent *e) { e->setAccepted(d->m_controller->onMouseDoubleClick(e->pos())); } void TabBar::keyPressEvent(QKeyEvent *e) { if (e->key() == Qt::Key_Left || e->key() == Qt::Key_Right) { if (count() > 0 && usesScrollButtons()) { QAbstractButton *scrollLeftBtn = nullptr; QAbstractButton *scrollRightBtn = nullptr; const auto children = findChildren(); for (QAbstractButton *btn : children) { if (btn->isVisible()) { QRect btnRect = btn->geometry(); QRect tabBarRect = rect(); if (btnRect.left() < tabBarRect.width() / 2) { scrollLeftBtn = btn; } else { scrollRightBtn = btn; } } } if (e->key() == Qt::Key_Left && scrollLeftBtn && scrollLeftBtn->isEnabled()) { scrollLeftBtn->click(); e->accept(); return; } else if (e->key() == Qt::Key_Right && scrollRightBtn && scrollRightBtn->isEnabled()) { scrollRightBtn->click(); e->accept(); return; } const int scrollStep = 50; if (e->key() == Qt::Key_Left) { scroll(-scrollStep, 0); } else { scroll(scrollStep, 0); } e->accept(); return; } } QTabBar::keyPressEvent(e); } void TabBar::wheelEvent(QWheelEvent *e) { int delta = e->angleDelta().y(); if (delta == 0) { delta = -e->angleDelta().x(); } if (delta == 0) { QTabBar::wheelEvent(e); return; } const int scrollAmount = delta / 8; d->targetScrollOffset += scrollAmount; if (!d->scrollAnimationTimer->isActive()) { d->scrollAnimationTimer->start(); } e->accept(); } void TabBar::performSmoothScroll() { const int diff = d->targetScrollOffset - d->scrollOffset; if (qAbs(diff) < 2) { d->scrollOffset = d->targetScrollOffset; d->scrollAnimationTimer->stop(); return; } const int step = diff / 4; const int actualStep = (step == 0) ? (diff > 0 ? 1 : -1) : step; d->scrollOffset += actualStep; QList scrollButtons = findChildren(); QToolButton *leftButton = nullptr; QToolButton *rightButton = nullptr; for (QToolButton *btn : scrollButtons) { if (btn->arrowType() == Qt::LeftArrow) { leftButton = btn; } else if (btn->arrowType() == Qt::RightArrow) { rightButton = btn; } } if (actualStep > 0 && leftButton && leftButton->isEnabled()) { leftButton->click(); } else if (actualStep < 0 && rightButton && rightButton->isEnabled()) { rightButton->click(); } else { d->targetScrollOffset = d->scrollOffset; d->scrollAnimationTimer->stop(); } } bool TabBar::event(QEvent *ev) { auto parent = parentWidget(); if (!parent) { return QTabBar::event(ev); } const bool result = QTabBar::event(ev); if (ev->type() == QEvent::Show) { parent->setFocusProxy(this); } else if (ev->type() == QEvent::Hide) { parent->setFocusProxy(nullptr); } else if (ev->type() == QEvent::PaletteChange || ev->type() == QEvent::StyleChange) { QTimer::singleShot(0, this, [this]() { updateScrollButtonsColors(); }); } return result; } QString TabBar::text(int index) const { return tabText(index); } QRect TabBar::rectForTab(int index) const { return QTabBar::tabRect(index); } void TabBar::moveTabTo(int from, int to) { moveTab(from, to); } void TabBar::tabInserted(int index) { QTabBar::tabInserted(index); if (tabsClosable()) { auto closeSide = static_cast( style()->styleHint(QStyle::SH_TabBar_CloseButtonPosition, nullptr, this)); QWidget *btn = tabButton(index, closeSide); if (btn) { if (btn->size().isEmpty()) btn->resize(btn->sizeHint()); bool hasFilter = false; for (auto* child : btn->children()) { if (dynamic_cast(child)) { hasFilter = true; break; } } if (!hasFilter) btn->installEventFilter(new TabCloseButtonFilter(btn)); } } Q_EMIT dockWidgetInserted(index); Q_EMIT countChanged(); QSet newHighlighted; for (int i : m_highlightedTabs) { if (i >= index) { newHighlighted.insert(i + 1); } else { newHighlighted.insert(i); } } m_highlightedTabs = newHighlighted; } void TabBar::tabRemoved(int index) { QTabBar::tabRemoved(index); Q_EMIT dockWidgetRemoved(index); Q_EMIT countChanged(); QSet newHighlighted; for (int i : m_highlightedTabs) { if (i < index) { newHighlighted.insert(i); } else if (i > index) { newHighlighted.insert(i - 1); } } m_highlightedTabs = newHighlighted; if (m_highlightedTabs.isEmpty()) { stopBlinkTimer(); } } void TabBar::setCurrentIndex(int index) { QTabBar::setCurrentIndex(index); } void TabBar::updateScrollButtonsColors() { const auto allButtons = findChildren(); for (QToolButton *btn : allButtons) { if (btn->arrowType() == Qt::NoArrow) continue; bool isLeft = (btn->arrowType() == Qt::LeftArrow); bool hasFilter = false; for (auto* child : btn->children()) { if (dynamic_cast(child)) { hasFilter = true; break; } } if (!hasFilter) btn->installEventFilter(new ScrollButtonFilter(isLeft, btn)); const int compactW = qMax(18, height() * 2 / 3); btn->setFixedWidth(compactW); } } QTabWidget *TabBar::tabWidget() const { if (auto tw = dynamic_cast(d->m_controller->stack()->view())) return tw; qWarning() << Q_FUNC_INFO << "Unexpected null QTabWidget"; return nullptr; } void TabBar::renameTab(int index, const QString &text) { setTabText(index, text); } void TabBar::changeTabIcon(int index, const QIcon &icon) { setTabIcon(index, icon); } void TabBar::removeDockWidget(Core::DockWidget *dw) { auto tabWidget = static_cast(View_qt::asQWidget(m_tabBar->stack())); tabWidget->removeTab(m_tabBar->indexOfDockWidget(dw)); } void TabBar::insertDockWidget(int index, Core::DockWidget *dw, const QIcon &icon, const QString &title) { auto tabWidget = static_cast(View_qt::asQWidget(m_tabBar->stack())); tabWidget->insertTab(index, View_qt::asQWidget(dw), icon, title); } void TabBar::setTabsAreMovable(bool are) { QTabBar::setMovable(are); } Core::TabBar *TabBar::tabBar() const { return d->m_controller; } void TabBar::Private::onTabMoved(int from, int to) { if (from == to || m_controller->isMovingTab()) return; m_controller->dptr()->moveTabTo(from, to); } void TabBar::paintEvent(QPaintEvent *event) { QTabBar::paintEvent(event); if (!m_highlightedTabs.isEmpty()) { QPainter painter(this); painter.setRenderHint(QPainter::TextAntialiasing); QColor textColor = currentHighlightColor(); painter.setPen(textColor); painter.setFont(font()); for (int index : m_highlightedTabs) { if (index >= 0 && index < count() && index != currentIndex()) { QStyleOptionTab opt; initStyleOption(&opt, index); QRect textRect = style()->subElementRect(QStyle::SE_TabBarTabText, &opt, this); if (!opt.icon.isNull()) { QRect iconRect = style()->subElementRect(QStyle::SE_TabBarTabLeftButton, &opt, this); if (iconRect.isValid()) { int iconRight = iconRect.right() + style()->pixelMetric(QStyle::PM_TabBarTabHSpace, &opt, this) / 2; if (textRect.left() < iconRight) { textRect.setLeft(iconRight); } } else { int iconWidth = opt.iconSize.width(); if (iconWidth <= 0) iconWidth = style()->pixelMetric(QStyle::PM_TabBarIconSize, &opt, this); int spacing = style()->pixelMetric(QStyle::PM_TabBarTabHSpace, &opt, this) / 2; textRect.setLeft(textRect.left() + iconWidth + spacing); } } painter.drawText(textRect, Qt::AlignCenter, opt.text); } } } } QColor TabBar::currentHighlightColor() const { return m_blinkState ? QColor("#FF6600") : QColor("#FFAA44"); } int TabBar::tabIndexFromRect(const QRect& rect) const { for (int i = 0; i < count(); ++i) { if (tabRect(i) == rect) { return i; } } return -1; } void TabBar::setTabHighlighted(int index, bool highlighted) { if (index < 0 || index >= count()) return; bool wasHighlighted = m_highlightedTabs.contains(index); if (wasHighlighted == highlighted) return; if (highlighted) { if (index == currentIndex()) return; m_highlightedTabs.insert(index); startBlinkTimer(); } else { m_highlightedTabs.remove(index); if (m_highlightedTabs.isEmpty()) { stopBlinkTimer(); } } update(); Q_EMIT tabHighlightChanged(index, highlighted); } bool TabBar::isTabHighlighted(int index) const { return m_highlightedTabs.contains(index); } void TabBar::clearAllHighlights() { QSet tabs = m_highlightedTabs; for (int index : tabs) { setTabHighlighted(index, false); } } void TabBar::startBlinkTimer() { if (!m_blinkTimer) { m_blinkTimer = new QTimer(this); m_blinkTimer->setInterval(600); connect(m_blinkTimer, &QTimer::timeout, this, [this]() { m_blinkState = !m_blinkState; repaint(); }); } if (!m_blinkTimer->isActive()) { m_blinkState = true; m_blinkTimer->start(); } } void TabBar::stopBlinkTimer() { if (m_blinkTimer && m_blinkTimer->isActive()) { m_blinkTimer->stop(); } m_blinkState = false; }