// Copyright (C) 2020 Uwe Kindler // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-2.1-or-later OR GPL-3.0-or-later #include "dockwidgettab.h" #include "ads_globals.h" #include "ads_globals_p.h" #include "advanceddockingsystemtr.h" #include "dockareawidget.h" #include "dockfocuscontroller.h" #include "dockmanager.h" #include "dockoverlay.h" #include "dockwidget.h" #include "elidinglabel.h" #include "floatingdockcontainer.h" #include "floatingdragpreview.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace ADS { using TabLabelType = ElidingLabel; /** * Private data class of DockWidgetTab class (pimpl) */ class DockWidgetTabPrivate { public: DockWidgetTab *q; DockWidget *m_dockWidget = nullptr; QLabel *m_iconLabel = nullptr; TabLabelType *m_titleLabel = nullptr; QPoint m_globalDragStartMousePosition; QPoint m_dragStartMousePosition; bool m_isActiveTab = false; DockAreaWidget *m_dockArea = nullptr; eDragState m_dragState = DraggingInactive; AbstractFloatingWidget *m_floatingWidget = nullptr; QIcon m_icon; TabButton *m_closeButton = nullptr; QPoint m_tabDragStartPosition; QSize m_iconSize; /** * Private data constructor */ DockWidgetTabPrivate(DockWidgetTab *parent); /** * Creates the complete layout including all controls */ void createLayout(); /** * Moves the tab depending on the position in the given mouse event */ void moveTab(QMouseEvent *event); /** * Test function for current drag state */ bool isDraggingState(eDragState dragState) const { return this->m_dragState == dragState; } /** * Starts floating of the dock widget that belongs to this title bar * Returns true, if floating has been started and false if floating * is not possible for any reason */ bool startFloating(eDragState draggingState = DraggingFloatingWidget); /** * Returns true if the given config flag is set */ bool testConfigFlag(DockManager::eConfigFlag flag) const { return DockManager::testConfigFlag(flag); } /** * Creates the close button as QPushButton or as QToolButton */ TabButton *createCloseButton() const { return new TabButton(); } template AbstractFloatingWidget *createFloatingWidget(T *widget, bool opaqueUndocking) { if (opaqueUndocking) { return new FloatingDockContainer(widget); } else { auto w = new FloatingDragPreview(widget); QObject::connect(w, &FloatingDragPreview::draggingCanceled, q, [this] { m_dragState = DraggingInactive; }); return w; } } /** * Update the close button visibility from current feature/config */ void updateCloseButtonVisibility(bool active) { bool dockWidgetClosable = m_dockWidget->features().testFlag(DockWidget::DockWidgetClosable); bool activeTabHasCloseButton = testConfigFlag(DockManager::ActiveTabHasCloseButton); bool allTabsHaveCloseButton = testConfigFlag(DockManager::AllTabsHaveCloseButton); bool tabHasCloseButton = (activeTabHasCloseButton && active) | allTabsHaveCloseButton; m_closeButton->setVisible(dockWidgetClosable && tabHasCloseButton); } /** * Update the size policy of the close button depending on the * RetainTabSizeWhenCloseButtonHidden feature */ void updateCloseButtonSizePolicy() { auto features = m_dockWidget->features(); auto sizePolicy = m_closeButton->sizePolicy(); sizePolicy.setRetainSizeWhenHidden( features.testFlag(DockWidget::DockWidgetClosable) && testConfigFlag(DockManager::RetainTabSizeWhenCloseButtonHidden)); m_closeButton->setSizePolicy(sizePolicy); } /** * Saves the drag start position in global and local coordinates */ void saveDragStartMousePosition(const QPoint &globalPos) { m_globalDragStartMousePosition = globalPos; m_dragStartMousePosition = q->mapFromGlobal(globalPos); } /** * Update the icon in case the icon size changed */ void updateIcon() { if (!m_iconLabel || m_icon.isNull()) return; if (m_iconSize.isValid()) m_iconLabel->setPixmap(m_icon.pixmap(m_iconSize)); else m_iconLabel->setPixmap( m_icon.pixmap(q->style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, q))); m_iconLabel->setVisible(true); } /** * Convenience function for access to the dock manager dock focus controller */ DockFocusController *focusController() const { return m_dockWidget->dockManager()->dockFocusController(); } /** * Helper function to create and initialize the menu entries for * the "Auto Hide Group To..." menu */ QAction *createAutoHideToAction(const QString &title, SideBarLocation location, QMenu *menu) { auto action = menu->addAction(title); QObject::connect(action, &QAction::triggered, q, [this, location] { m_dockWidget->toggleAutoHide(location); }); return action; } }; // class DockWidgetTabPrivate DockWidgetTabPrivate::DockWidgetTabPrivate(DockWidgetTab *parent) : q(parent) {} void DockWidgetTabPrivate::createLayout() { m_titleLabel = new TabLabelType(); m_titleLabel->setElideMode(Qt::ElideRight); m_titleLabel->setText(m_dockWidget->windowTitle()); m_titleLabel->setObjectName("dockWidgetTabLabel"); m_titleLabel->setAlignment(Qt::AlignCenter); QObject::connect(m_titleLabel, &ElidingLabel::elidedChanged, q, &DockWidgetTab::elidedChanged); m_closeButton = createCloseButton(); m_closeButton->setObjectName("tabCloseButton"); internal::setButtonIcon(m_closeButton, QStyle::SP_TitleBarCloseButton, TabCloseIcon); m_closeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_closeButton->setIconSize(QSize(11, 11)); m_closeButton->setFixedSize(QSize(17, 17)); m_closeButton->setFocusPolicy(Qt::NoFocus); updateCloseButtonSizePolicy(); internal::setToolTip(m_closeButton, Tr::tr("Close Tab")); QObject::connect(m_closeButton, &QAbstractButton::clicked, q, &DockWidgetTab::closeRequested); QFontMetrics fontMetrics(m_titleLabel->font()); int spacing = qRound(fontMetrics.height() / 4.0); // Fill the layout QBoxLayout *boxLayout = new QBoxLayout(QBoxLayout::LeftToRight); boxLayout->setContentsMargins(2 * spacing, 0, 0, 0); boxLayout->setSpacing(0); q->setLayout(boxLayout); boxLayout->addWidget(m_titleLabel, 1, Qt::AlignVCenter); boxLayout->addSpacing(qRound(spacing * 4.0 / 3.0)); boxLayout->addWidget(m_closeButton, 0, Qt::AlignVCenter); boxLayout->addSpacing(1); boxLayout->setAlignment(Qt::AlignCenter | Qt::AlignVCenter); if (DockManager::testConfigFlag(DockManager::FocusHighlighting)) m_closeButton->setCheckable(true); m_titleLabel->setVisible(true); } void DockWidgetTabPrivate::moveTab(QMouseEvent *event) { event->accept(); QPoint distance = event->globalPosition().toPoint() - m_globalDragStartMousePosition; distance.setY(0); auto targetPos = distance + m_tabDragStartPosition; targetPos.rx() = qMax(targetPos.x(), 0); targetPos.rx() = qMin(q->parentWidget()->rect().right() - q->width() + 1, targetPos.rx()); q->move(targetPos); q->raise(); } bool DockWidgetTabPrivate::startFloating(eDragState draggingState) { auto dockContainer = m_dockWidget->dockContainer(); // If this is the last dock widget inside of this floating widget, then it does not make any // sense, to make it floating because it is already floating. if (dockContainer->isFloating() && (dockContainer->visibleDockAreaCount() == 1) && (m_dockWidget->dockAreaWidget()->dockWidgetsCount() == 1)) return false; m_dragState = draggingState; AbstractFloatingWidget *floatingWidget = nullptr; bool createContainer = (DraggingFloatingWidget != draggingState); // If section widget has multiple tabs, we take only one tab. If it has only one single tab, // we can move the complete dock area into floating widget. QSize size; if (m_dockArea->dockWidgetsCount() > 1) { floatingWidget = createFloatingWidget(m_dockWidget, createContainer); size = m_dockWidget->size(); } else { floatingWidget = createFloatingWidget(m_dockArea, createContainer); size = m_dockArea->size(); } if (DraggingFloatingWidget == draggingState) { floatingWidget->startFloating(m_dragStartMousePosition, size, DraggingFloatingWidget, q); auto overlay = m_dockWidget->dockManager()->containerOverlay(); overlay->setAllowedAreas(OuterDockAreas); m_floatingWidget = floatingWidget; qApp->postEvent(m_dockWidget, new QEvent((QEvent::Type) internal::g_dockedWidgetDragStartEvent)); } else { floatingWidget->startFloating(m_dragStartMousePosition, size, DraggingInactive, nullptr); } return true; } TabButton::TabButton(QWidget *parent) : TabButtonType(parent) , m_active(false) , m_focus(false) {} void TabButton::setActive(bool value) { m_active = value; } void TabButton::setFocus(bool value) { m_focus = value; } void TabButton::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QStylePainter p(this); QStyleOptionToolButton opt; initStyleOption(&opt); opt.icon = QIcon(); // set to null icon otherwise it is drawn twice p.drawComplexControl(QStyle::CC_ToolButton, opt); QIcon::Mode mode = QIcon::Mode::Normal; if (m_active) mode = QIcon::Mode::Active; if (m_focus) mode = QIcon::Mode::Selected; const QPoint iconPosition = rect().center() - QPoint(iconSize().width() * 0.5, iconSize().height() * 0.5); p.drawPixmap(iconPosition, icon().pixmap(iconSize(), mode)); } DockWidgetTab::DockWidgetTab(DockWidget *dockWidget, QWidget *parent) : QFrame(parent) , d(new DockWidgetTabPrivate(this)) { setAttribute(Qt::WA_NoMousePropagation, true); setObjectName(dockWidget->objectName()); d->m_dockWidget = dockWidget; d->createLayout(); setFocusPolicy(Qt::NoFocus); } DockWidgetTab::~DockWidgetTab() { qCInfo(adsLog) << Q_FUNC_INFO; delete d; } void DockWidgetTab::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { event->accept(); d->saveDragStartMousePosition(event->globalPosition().toPoint()); d->m_dragState = DraggingMousePressed; if (DockManager::testConfigFlag(DockManager::FocusHighlighting)) { d->focusController()->setDockWidgetTabPressed(true); d->focusController()->setDockWidgetTabFocused(this); } emit clicked(); return; } Super::mousePressEvent(event); } void DockWidgetTab::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { auto currentDragState = d->m_dragState; d->m_globalDragStartMousePosition = QPoint(); d->m_dragStartMousePosition = QPoint(); d->m_dragState = DraggingInactive; switch (currentDragState) { case DraggingTab: // End of tab moving, emit signal if (d->m_dockArea) { event->accept(); emit moved(event->globalPosition().toPoint()); } break; case DraggingFloatingWidget: event->accept(); d->m_floatingWidget->finishDragging(); break; default: break; } if (DockManager::testConfigFlag(DockManager::FocusHighlighting)) d->focusController()->setDockWidgetTabPressed(false); } else if (event->button() == Qt::MiddleButton) { if (DockManager::testConfigFlag(DockManager::MiddleMouseButtonClosesTab) && d->m_dockWidget->features().testFlag(DockWidget::DockWidgetClosable)) { // Only attempt to close if the mouse is still // on top of the widget, to allow the user to cancel. if (rect().contains(mapFromGlobal(QCursor::pos()))) { event->accept(); emit closeRequested(); } } } Super::mouseReleaseEvent(event); } void DockWidgetTab::mouseMoveEvent(QMouseEvent *event) { if (!(event->buttons() & Qt::LeftButton) || d->isDraggingState(DraggingInactive)) { d->m_dragState = DraggingInactive; Super::mouseMoveEvent(event); return; } // Move floating window if (d->isDraggingState(DraggingFloatingWidget)) { d->m_floatingWidget->moveFloating(); Super::mouseMoveEvent(event); return; } // Move tab if (d->isDraggingState(DraggingTab)) { // Moving the tab is always allowed because it does not mean moving the dock widget around d->moveTab(event); } auto mappedPos = mapToParent(event->pos()); bool mouseOutsideBar = (mappedPos.x() < 0) || (mappedPos.x() > parentWidget()->rect().right()); // Maybe a fixed drag distance is better here ? int dragDistanceY = qAbs(d->m_globalDragStartMousePosition.y() - event->globalPosition().toPoint().y()); if (dragDistanceY >= DockManager::startDragDistance() || mouseOutsideBar) { // If this is the last dock area in a dock container with only one single dock widget it // does not make sense to move it to a new floating widget and leave this one empty if (d->m_dockArea->dockContainer()->isFloating() && d->m_dockArea->openDockWidgetsCount() == 1 && d->m_dockArea->dockContainer()->visibleDockAreaCount() == 1) { return; } // Floating is only allowed for widgets that are floatable. We can create the drag preview // if the widget is movable. auto features = d->m_dockWidget->features(); if (features.testFlag(DockWidget::DockWidgetFloatable) || features.testFlag(DockWidget::DockWidgetMovable)) { // If we undock, we need to restore the initial position of this tab because it looks // strange if it remains on its dragged position if (d->isDraggingState(DraggingTab)) parentWidget()->layout()->update(); d->startFloating(); } return; } else if (d->m_dockArea->openDockWidgetsCount() > 1 && (event->globalPosition().toPoint() - d->m_globalDragStartMousePosition) .manhattanLength() >= QApplication::startDragDistance()) // Wait a few pixels before start moving { // If we start dragging the tab, we save its initial position to restore it later if (DraggingTab != d->m_dragState) d->m_tabDragStartPosition = this->pos(); d->m_dragState = DraggingTab; return; } Super::mouseMoveEvent(event); } void DockWidgetTab::contextMenuEvent(QContextMenuEvent *event) { if (DockManager::testConfigFlag(DockManager::HideContextMenuDockWidgetTab)) return; event->accept(); if (d->isDraggingState(DraggingFloatingWidget)) return; d->saveDragStartMousePosition(event->globalPos()); const bool isFloatable = d->m_dockWidget->features().testFlag(DockWidget::DockWidgetFloatable); const bool isNotOnlyTabInContainer = !d->m_dockArea->dockContainer()->hasTopLevelDockWidget(); const bool isTopLevelArea = d->m_dockArea->isTopLevelArea(); const bool isDetachable = isFloatable && isNotOnlyTabInContainer; QMenu menu(this); if (!isTopLevelArea) { QAction *detachAction = menu.addAction(Tr::tr("Detach")); detachAction->connect(detachAction, &QAction::triggered, this, &DockWidgetTab::detachDockWidget); detachAction->setEnabled(isDetachable); if (DockManager::testAutoHideConfigFlag(DockManager::AutoHideFeatureEnabled)) { QAction *pinAction = menu.addAction(Tr::tr("Pin")); pinAction->connect(pinAction, &QAction::triggered, this, &DockWidgetTab::autoHideDockWidget); auto isPinnable = d->m_dockWidget->features().testFlag(DockWidget::DockWidgetPinnable); pinAction->setEnabled(isPinnable); auto subMenu = menu.addMenu(Tr::tr("Pin To...")); subMenu->setEnabled(isPinnable); d->createAutoHideToAction(Tr::tr("Top"), SideBarTop, subMenu); d->createAutoHideToAction(Tr::tr("Left"), SideBarLeft, subMenu); d->createAutoHideToAction(Tr::tr("Right"), SideBarRight, subMenu); d->createAutoHideToAction(Tr::tr("Bottom"), SideBarBottom, subMenu); } } menu.addSeparator(); QAction *closeAction = menu.addAction(Tr::tr("Close")); closeAction->connect(closeAction, &QAction::triggered, this, &DockWidgetTab::closeRequested); closeAction->setEnabled(isClosable()); if (d->m_dockArea->openDockWidgetsCount() > 1) { QAction *closeOthersAction = menu.addAction(Tr::tr("Close Others")); closeOthersAction->connect(closeOthersAction, &QAction::triggered, this, &DockWidgetTab::closeOtherTabsRequested); } menu.exec(event->globalPos()); } bool DockWidgetTab::isActiveTab() const { return d->m_isActiveTab; } void DockWidgetTab::setActiveTab(bool active) { d->updateCloseButtonVisibility(active); d->m_closeButton->setActive(active); // TODO // Focus related stuff if (DockManager::testConfigFlag(DockManager::FocusHighlighting) && !d->m_dockWidget->dockManager()->isRestoringState()) { bool updateFocusStyle = false; if (active && !hasFocus()) { d->focusController()->setDockWidgetTabFocused(this); updateFocusStyle = true; } if (d->m_isActiveTab == active) { if (updateFocusStyle) updateStyle(); return; } } else if (d->m_isActiveTab == active) { return; } d->m_isActiveTab = active; updateStyle(); update(); updateGeometry(); emit activeTabChanged(); } DockWidget *DockWidgetTab::dockWidget() const { return d->m_dockWidget; } void DockWidgetTab::setDockAreaWidget(DockAreaWidget *dockArea) { d->m_dockArea = dockArea; } DockAreaWidget *DockWidgetTab::dockAreaWidget() const { return d->m_dockArea; } void DockWidgetTab::setIcon(const QIcon &icon) { QBoxLayout *boxLayout = qobject_cast(layout()); if (!d->m_iconLabel && icon.isNull()) return; if (!d->m_iconLabel) { d->m_iconLabel = new QLabel(); d->m_iconLabel->setAlignment(Qt::AlignVCenter); d->m_iconLabel->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); internal::setToolTip(d->m_iconLabel, d->m_titleLabel->toolTip()); boxLayout->insertWidget(0, d->m_iconLabel, Qt::AlignVCenter); boxLayout->insertSpacing(1, qRound(1.5 * boxLayout->contentsMargins().left() / 2.0)); } else if (icon.isNull()) { // Remove icon label and spacer item boxLayout->removeWidget(d->m_iconLabel); boxLayout->removeItem(boxLayout->itemAt(0)); delete d->m_iconLabel; d->m_iconLabel = nullptr; } d->m_icon = icon; if (d->m_iconLabel) { d->m_iconLabel->setPixmap( icon.pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize, nullptr, this))); d->m_iconLabel->setVisible(true); } } const QIcon &DockWidgetTab::icon() const { return d->m_icon; } QString DockWidgetTab::text() const { return d->m_titleLabel->text(); } void DockWidgetTab::mouseDoubleClickEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { // If this is the last dock area in a dock container it does not make // sense to move it to a new floating widget and leave this one empty if ((!d->m_dockArea->dockContainer()->isFloating() || d->m_dockArea->dockWidgetsCount() > 1) && d->m_dockWidget->features().testFlag(DockWidget::DockWidgetFloatable)) { event->accept(); d->saveDragStartMousePosition(event->globalPosition().toPoint()); d->startFloating(DraggingInactive); } } Super::mouseDoubleClickEvent(event); } void DockWidgetTab::setVisible(bool visible) { visible &= !d->m_dockWidget->features().testFlag(DockWidget::NoTab); Super::setVisible(visible); } void DockWidgetTab::setText(const QString &title) { d->m_titleLabel->setText(title); } bool DockWidgetTab::isTitleElided() const { return d->m_titleLabel->isElided(); } bool DockWidgetTab::isClosable() const { return d->m_dockWidget && d->m_dockWidget->features().testFlag(DockWidget::DockWidgetClosable); } void DockWidgetTab::detachDockWidget() { if (!d->m_dockWidget->features().testFlag(DockWidget::DockWidgetFloatable)) return; d->saveDragStartMousePosition(QCursor::pos()); d->startFloating(DraggingInactive); } void DockWidgetTab::autoHideDockWidget() { d->m_dockWidget->setAutoHide(true); } bool DockWidgetTab::event(QEvent *event) { #ifndef QT_NO_TOOLTIP if (event->type() == QEvent::ToolTipChange) { const auto text = toolTip(); d->m_titleLabel->setToolTip(text); if (d->m_iconLabel) d->m_iconLabel->setToolTip(text); } #endif if (event->type() == QEvent::StyleChange) d->updateIcon(); return Super::event(event); } void DockWidgetTab::onDockWidgetFeaturesChanged() { d->updateCloseButtonSizePolicy(); d->updateCloseButtonVisibility(isActiveTab()); } void DockWidgetTab::setElideMode(Qt::TextElideMode mode) { d->m_titleLabel->setElideMode(mode); } void DockWidgetTab::updateStyle() { if (DockManager::testConfigFlag(DockManager::FocusHighlighting)) d->m_closeButton->setFocus(property("focused").toBool()); internal::repolishStyle(this, internal::RepolishDirectChildren); } QSize DockWidgetTab::iconSize() const { return d->m_iconSize; } void DockWidgetTab::setIconSize(const QSize &size) { if (size == d->m_iconSize) return; d->m_iconSize = size; d->updateIcon(); emit iconSizeChanged(); } } // namespace ADS