/* 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 "Stack.h" #include "qtwidgets/views/DockWidget.h" #include "qtwidgets/views/TabBar.h" #include "core/Controller.h" #include "core/Stack.h" #include "core/TitleBar.h" #include "core/Group.h" #include "core/Group_p.h" #include "core/Window_p.h" #include "core/DockRegistry_p.h" #include "core/Stack_p.h" #include "Config.h" #include "core/View_p.h" #include "qtwidgets/ViewFactory.h" #include #include #include #include #include #include #include #include #include #include #include "kdbindings/signal.h" using namespace KDDockWidgets; using namespace KDDockWidgets::QtWidgets; namespace KDDockWidgets::QtWidgets { class Stack::Private { public: KDBindings::ScopedConnection tabBarAutoHideChanged; KDBindings::ScopedConnection buttonsToHideIfDisabledConnection; KDBindings::ScopedConnection titleBarVisibilityConnection; QWidget *buttonsWidget = nullptr; QHBoxLayout *buttonsLayout = nullptr; QAbstractButton *floatButton = nullptr; QAbstractButton *closeButton = nullptr; bool updatingButtons = false; }; } Stack::Stack(Core::Stack *controller, QWidget *parent) : View(controller, Core::ViewType::Stack, parent) , StackViewInterface(controller) , d(new Private()) { setTabPosition(Config::self().tabsAtBottom() ? TabPosition::South : TabPosition::North); } Stack::~Stack() { delete d; } void Stack::init() { setTabBar(tabBar()); setTabsClosable(Config::self().flags() & Config::Flag_TabsHaveCloseButton); setContextMenuPolicy(Qt::CustomContextMenu); connect(this, &QTabWidget::customContextMenuRequested, this, &Stack::showContextMenu); // In case tabs closable is set by the factory, a tabClosedRequested() is emitted when the user // presses [x] connect(this, &QTabWidget::tabCloseRequested, this, [this](int index) { if (auto dw = m_stack->tabBar()->dockWidgetAt(index)) { if (dw->options() & DockWidgetOption_NotClosable) { qWarning() << "QTabWidget::tabCloseRequested: Refusing to close dock widget with " "Option_NotClosable option. name=" << dw->uniqueName(); } else { dw->view()->close(); } } else { qWarning() << "QTabWidget::tabCloseRequested Couldn't find dock widget for index" << index << "; count=" << count(); } }); QTabWidget::setTabBarAutoHide(m_stack->tabBarAutoHide()); d->tabBarAutoHideChanged = m_stack->d->tabBarAutoHideChanged.connect( [this](bool is) { QTabWidget::setTabBarAutoHide(is); }); if (!QTabWidget::tabBar()->isVisible()) setFocusProxy(nullptr); setupTabBarButtons(); setDocumentMode(m_stack->options() & StackOption_DocumentMode); } void Stack::mouseDoubleClickEvent(QMouseEvent *ev) { if (m_stack->onMouseDoubleClick(ev->pos())) { ev->accept(); } else { ev->ignore(); } } void Stack::mousePressEvent(QMouseEvent *ev) { QTabWidget::mousePressEvent(ev); if ((Config::self().flags() & Config::Flag_TitleBarIsFocusable) && !m_stack->group()->isFocused()) { // User clicked on the tab widget itself m_stack->group()->FocusScope::focus(Qt::MouseFocusReason); } } void Stack::setupTabBarButtons() { if (!(Config::self().flags() & Config::Flag_ShowButtonsOnTabBarIfTitleBarHidden)) return; if (d->buttonsWidget != nullptr) return; auto factory = static_cast(Config::self().viewFactory()); d->closeButton = factory->createTitleBarButton(this, TitleBarButtonType::Close); d->floatButton = factory->createTitleBarButton(this, TitleBarButtonType::Float); d->buttonsWidget = new QWidget(this); d->buttonsWidget->setObjectName(QStringLiteral("TabBar Buttons")); d->buttonsWidget->setAttribute(Qt::WA_TransparentForMouseEvents, false); d->buttonsLayout = new QHBoxLayout(d->buttonsWidget); d->buttonsLayout->setContentsMargins(2, 0, 4, 0); d->buttonsLayout->setSpacing(2); d->buttonsLayout->setAlignment(Qt::AlignVCenter); d->floatButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); d->closeButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); d->buttonsLayout->addWidget(d->floatButton, 0, Qt::AlignVCenter); d->buttonsLayout->addWidget(d->closeButton, 0, Qt::AlignVCenter); d->buttonsWidget->raise(); connect(d->floatButton, &QAbstractButton::clicked, this, [this] { Core::TitleBar *tb = m_stack->group()->titleBar(); tb->onFloatClicked(); }); connect(d->closeButton, &QAbstractButton::clicked, this, [this] { Core::TitleBar *tb = m_stack->group()->titleBar(); tb->onCloseClicked(); }); d->buttonsToHideIfDisabledConnection = m_stack->d->buttonsToHideIfDisabledChanged.connect([this] { updateTabBarButtons(); }); auto tb = qobject_cast(tabBar()); if (tb) { connect(tb, &QtWidgets::TabBar::countChanged, this, &Stack::updateTabBarButtons); connect(tb, &QtWidgets::TabBar::countChanged, this, &Stack::updateButtonsPosition); } if (auto group = m_stack->group()) { d->titleBarVisibilityConnection = group->dptr()->actualTitleBarChanged.connect([this] { QTimer::singleShot(0, this, [this] { updateTabBarButtons(); updateButtonsPosition(); }); }); } updateTabBarButtons(); QTimer::singleShot(0, this, [this] { updateTabBarButtons(); updateButtonsPosition(); }); } void Stack::updateTabBarButtons() { if (!d->buttonsWidget) return; if (d->updatingButtons) return; d->updatingButtons = true; auto group = m_stack->group(); if (!group) { d->updatingButtons = false; return; } const bool hideTitleBarFlag = Config::self().flags() & Config::Flag_HideTitleBarWhenTabsVisible; const bool showButtons = hideTitleBarFlag && group->hasTabsVisible(); d->buttonsWidget->setVisible(showButtons); if (showButtons) { d->floatButton->setVisible(true); if (d->closeButton) { const bool enabled = !group->anyNonClosable(); const bool visible = enabled || !m_stack->buttonHidesIfDisabled(TitleBarButtonType::Close); d->closeButton->setEnabled(enabled); d->closeButton->setVisible(visible); } } updateButtonsPosition(); d->updatingButtons = false; } void Stack::updateMargins() { // Deprecated - used updateButtonsPosition() } void Stack::updateButtonsPosition() { if (!d->buttonsWidget || !d->buttonsWidget->isVisible()) return; QTabBar *tb = QTabWidget::tabBar(); if (!tb || !tb->isVisible()) return; const int tabBarHeight = tb->height(); if (tabBarHeight <= 0) return; const int btnSize = qMax(16, tabBarHeight - 6); if (d->floatButton && d->floatButton->isVisible()) d->floatButton->setFixedSize(btnSize, btnSize); if (d->closeButton && d->closeButton->isVisible()) d->closeButton->setFixedSize(btnSize, btnSize); d->buttonsWidget->adjustSize(); const QSize widgetSize = d->buttonsWidget->sizeHint(); const int w = widgetSize.width(); const int h = qMin(widgetSize.height(), tabBarHeight); const int x = width() - w; const int y = tb->y() + (tabBarHeight - h) / 2; d->buttonsWidget->setGeometry(x, y, w, h); d->buttonsWidget->raise(); QRect tbRect = tb->geometry(); const int maxRight = x - 2; if (tbRect.right() > maxRight) { tbRect.setRight(maxRight); tb->setGeometry(tbRect); } } void Stack::resizeEvent(QResizeEvent *event) { QTabWidget::resizeEvent(event); updateButtonsPosition(); } bool Stack::event(QEvent *e) { if (e->type() == QEvent::LayoutRequest || e->type() == QEvent::Show) { QTimer::singleShot(0, this, &Stack::updateButtonsPosition); } return QTabWidget::event(e); } void Stack::showContextMenu(QPoint pos) { if (!(Config::self().flags() & Config::Flag_AllowSwitchingTabsViaMenu)) return; QTabBar *tabBar = QTabWidget::tabBar(); // We don't want context menu if there is only one tab if (tabBar->count() <= 1) return; // Convert pos to tabBar coordinates for tabAt() check QPoint tabBarPos = tabBar->mapFromGlobal(mapToGlobal(pos)); int clickedTabIndex = tabBar->tabAt(tabBarPos); if (clickedTabIndex >= 0) { auto* coreDw = m_stack->tabBar()->dockWidgetAt(clickedTabIndex); if (!coreDw) return; QWidget* guestWidget = nullptr; if (auto guestView = coreDw->guestView()) guestWidget = View_qt::asQWidget(guestView.get()); QString uniqueName = QString::fromStdString(coreDw->uniqueName().toStdString()); bool isRenameable = uniqueName.startsWith("Console [") || uniqueName.startsWith("Terminal [") || uniqueName.startsWith("Files [") || uniqueName.startsWith("Processes ["); if (!isRenameable) return; bool hasCustomName = false; if (guestWidget) { const QVariant v = guestWidget->property("adaptix.customTabTitle"); hasCustomName = v.isValid() && v.canConvert() && !v.toString().isEmpty(); } QMenu menu(this); menu.addAction("Rename", this, [this, clickedTabIndex, coreDw, guestWidget]() { bool ok; QString currentTitle = tabText(clickedTabIndex); QString newTitle = QInputDialog::getText(this, "Rename Tab", "Enter new name:", QLineEdit::Normal, currentTitle, &ok); if (ok && !newTitle.isEmpty() && newTitle != currentTitle) { setTabText(clickedTabIndex, newTitle); coreDw->setTitle(newTitle); if (guestWidget) guestWidget->setProperty("adaptix.customTabTitle", newTitle); } }); if (hasCustomName) { menu.addAction("Reset Name", this, [this, clickedTabIndex, coreDw, guestWidget]() { QString uniqueName = QString::fromStdString(coreDw->uniqueName().toStdString()); QString originalName = uniqueName.split(":Dock-").first(); setTabText(clickedTabIndex, originalName); coreDw->setTitle(originalName); if (guestWidget) guestWidget->setProperty("adaptix.customTabTitle", QVariant()); }); } menu.exec(mapToGlobal(pos)); return; } if (!(Config::self().flags() & Config::Flag_AllowSwitchingTabsViaMenu)) return; // We don't want context menu if there is only one tab if (tabBar->count() <= 1) return; // Right click is allowed only on the tabs area QRect tabAreaRect = QRect(mapFromGlobal(tabBar->mapToGlobal(QPoint(0, 0))), tabBar->size()); if (tabPosition() == QTabWidget::North || tabPosition() == QTabWidget::South) { tabAreaRect.setLeft(0); tabAreaRect.setRight(width()); } if (!tabAreaRect.contains(pos)) return; QMenu menu(this); for (int i = 0; i < tabBar->count(); ++i) { QAction *action = menu.addAction(tabText(i), this, [this, i] { setCurrentIndex(i); }); if (i == currentIndex()) action->setDisabled(true); } menu.exec(mapToGlobal(pos)); } QTabBar *Stack::tabBar() const { return static_cast(View_qt::asQWidget((m_stack->tabBar()))); } void Stack::setDocumentMode(bool is) { QTabWidget::setDocumentMode(is); } Core::Stack *Stack::stack() const { return m_stack; } bool Stack::isPositionDraggable(QPoint p) const { switch (tabPosition()) { case QTabWidget::North: return p.y() >= 0 && p.y() <= tabBar()->height(); case QTabWidget::South: return p.y() >= tabBar()->y(); default: qWarning() << Q_FUNC_INFO << "Not implemented yet. Only North and South is supported"; return false; } } QAbstractButton *Stack::button(TitleBarButtonType type) const { switch (type) { case TitleBarButtonType::Close: return d->closeButton; case TitleBarButtonType::Float: return d->floatButton; case TitleBarButtonType::Minimize: case TitleBarButtonType::Maximize: case TitleBarButtonType::Normal: case TitleBarButtonType::AutoHide: case TitleBarButtonType::UnautoHide: case TitleBarButtonType::AllTitleBarButtonTypes: return nullptr; } return nullptr; }