/* This file is part of KDDockWidgets. SPDX-FileCopyrightText: 2020 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 "TitleBar.h" #include "TitleBar_p.h" #include "Config.h" #include "ViewFactory.h" #include "View.h" #include "WindowBeingDragged_p.h" #include "Utils_p.h" #include "Logging_p.h" #include "Group_p.h" #include "views/TitleBarViewInterface.h" #include "DockWidget_p.h" #include "FloatingWindow.h" #include "DockRegistry.h" #include "FloatingWindow_p.h" #include "TabBar.h" #include "MainWindow.h" #include "MDILayout.h" #include "Stack.h" #ifdef KDDW_FRONTEND_QT #include #endif #include using namespace KDDockWidgets; using namespace KDDockWidgets::Core; TitleBar::TitleBar(Group *parent) : Controller( ViewType::TitleBar, Config::self().viewFactory()->createTitleBar(this, parent ? parent->view() : nullptr)) , Draggable(view()) , d(new Private()) , m_group(parent) , m_floatingWindow(nullptr) , m_supportsAutoHide((Config::self().flags() & Config::Flag_TitleBarShowAutoHide) == Config::Flag_TitleBarShowAutoHide) , m_isStandalone(false) { init(); d->numDockWidgetsChangedConnection = m_group->dptr()->numDockWidgetsChanged.connect([this] { updateCloseButton(); d->numDockWidgetsChanged.emit(); }); d->isFocusedChangedConnection = m_group->dptr()->isFocusedChanged.connect([this] { d->isFocusedChanged.emit(); }); d->isInMainWindowChangedConnection = m_group->dptr()->isInMainWindowChanged.connect([this] { updateAutoHideButton(); }); } TitleBar::TitleBar(FloatingWindow *parent) : Controller( ViewType::TitleBar, Config::self().viewFactory()->createTitleBar(this, parent ? parent->view() : nullptr)) , Draggable(view()) , d(new Private()) , m_group(nullptr) , m_floatingWindow(parent) , m_supportsAutoHide((Config::self().flags() & Config::Flag_TitleBarShowAutoHide) == Config::Flag_TitleBarShowAutoHide) , m_isStandalone(false) { init(); auto fwPrivate = m_floatingWindow->dptr(); fwPrivate->numGroupsChanged.connect([this] { updateButtons(); }); fwPrivate->numDockWidgetsChanged.connect([this] { d->numDockWidgetsChanged.emit(); }); fwPrivate->windowStateChanged.connect([this] { updateMaximizeButton(); }); fwPrivate->activatedChanged.connect([this] { d->isFocusedChanged.emit(); }); } TitleBar::TitleBar(Core::View *view) : Controller(ViewType::TitleBar, view) , Draggable(view, /*enabled=*/false) , d(new Private()) , m_group(nullptr) , m_floatingWindow(nullptr) , m_supportsAutoHide(false) , m_isStandalone(true) { } void TitleBar::init() { view()->init(); d->isFocusedChanged.connect([this] { // repaint view()->update(); }); updateButtons(); #ifdef KDDW_FRONTEND_QT // Auto-hide not supported in flutter yet QTimer::singleShot(0, this, &TitleBar::updateAutoHideButton); // have to wait after the group is // constructed #endif } TitleBar::~TitleBar() { delete d; } bool TitleBar::titleBarIsFocusable() const { return Config::self().flags() & Config::Flag_TitleBarIsFocusable; } MainWindow *TitleBar::mainWindow() const { if (m_floatingWindow || m_isStandalone) return nullptr; if (m_group) return m_group->mainWindow(); KDDW_ERROR("null group and null floating window"); return nullptr; } bool TitleBar::isMDI() const { auto p = view()->asWrapper(); while (p) { if (p->is(ViewType::MDILayout)) return true; if (p->is(ViewType::DropArea)) { // Note that the TitleBar can be inside a DropArea that's inside a MDIArea // so we need this additional check return false; } p = p->parentView(); } return false; } QString TitleBar::title() const { return m_title; } Icon TitleBar::icon() const { return m_icon; } bool TitleBar::onDoubleClicked() { if (Config::self().flags() & Config::Flag_DisableDoubleClick) { return false; } else if ((Config::self().flags() & Config::Flag_DoubleClickMaximizes) && m_floatingWindow) { // Not using isFloating(), as that can be a dock widget nested in a floating window. By // convention it's floating, but it's not the title bar of the top-level window. toggleMaximized(); return true; } else if (supportsFloatUnfloat()) { onFloatClicked(); return true; } return false; } bool TitleBar::floatButtonVisible() const { return m_floatButtonVisible; } bool TitleBar::maximizeButtonVisible() const { return m_maximizeButtonVisible; } bool TitleBar::supportsFloatUnfloat() const { if (m_isStandalone) return false; // not applicable if (DockWidget *dw = singleDockWidget()) { // Don't show the dock/undock button if the window is not dockable if (dw->options() & DockWidgetOption_NotDockable) return false; } // If we have a floating window with nested dock widgets we can't re-attach, because we don't // know where to return !m_floatingWindow || m_floatingWindow->hasSingleGroup(); } bool TitleBar::supportsFloatingButton() const { auto flags = Config::self().flags(); if (flags & Config::Flag_TitleBarHasMaximizeButton) { // Apps having a maximize/restore button traditionally don't have a floating one, // QDockWidget style only has floating and no maximize/restore. // We can add an option later if we need them to co-exist return false; } if (flags & Config::Flag_TitleBarNoFloatButton) { // Was explicitly disabled return false; } return supportsFloatUnfloat(); } bool TitleBar::supportsMaximizeButton() const { return m_floatingWindow && m_floatingWindow->supportsMaximizeButton(); } bool TitleBar::supportsMinimizeButton() const { return m_floatingWindow && m_floatingWindow->supportsMinimizeButton(); } bool TitleBar::supportsAutoHideButton() const { // Only dock widgets docked into the MainWindow can minimize return m_supportsAutoHide && m_group && (m_group->isInMainWindow() || m_group->isOverlayed()); } #ifdef DOCKS_TESTING_METHODS bool TitleBar::isFloatButtonVisible() const { return dynamic_cast(view())->isFloatButtonVisible(); } bool TitleBar::isCloseButtonVisible() const { return dynamic_cast(view())->isCloseButtonVisible(); } bool TitleBar::isCloseButtonEnabled() const { return dynamic_cast(view())->isCloseButtonEnabled(); } #endif bool TitleBar::hasIcon() const { return !m_icon.isNull(); } Core::Group *TitleBar::group() const { return m_group; } Core::FloatingWindow *TitleBar::floatingWindow() const { return m_floatingWindow; } void TitleBar::focus(Qt::FocusReason reason) { if (!(Config::self().flags() & Config::Flag_TitleBarIsFocusable)) return; if (m_group) { m_group->FocusScope::focus(reason); } else if (m_floatingWindow) { m_floatingWindow->focus(reason); } } void TitleBar::updateButtons() { updateCloseButton(); updateFloatButton(); updateMaximizeButton(); const bool isEnabled = true; const bool minimizeVisible = supportsMinimizeButton() && !buttonIsUserHidden(TitleBarButtonType::Minimize, isEnabled); d->minimizeButtonChanged.emit(minimizeVisible, isEnabled); updateAutoHideButton(); } void TitleBar::updateAutoHideButton() { TitleBarButtonType type = TitleBarButtonType::AutoHide; if (const Core::Group *group = this->group()) { if (group->isOverlayed()) type = TitleBarButtonType::UnautoHide; } const bool isEnabled = true; const bool visible = m_supportsAutoHide && !buttonIsUserHidden(type, isEnabled) && !m_floatingWindow; d->autoHideButtonChanged.emit(visible, isEnabled, type); } void TitleBar::updateMaximizeButton() { m_maximizeButtonVisible = false; m_maximizeButtonType = TitleBarButtonType::Maximize; if (auto fw = floatingWindow()) { m_maximizeButtonType = fw->view()->isMaximized() ? TitleBarButtonType::Normal : TitleBarButtonType::Maximize; m_maximizeButtonVisible = supportsMaximizeButton(); } const bool isEnabled = true; m_maximizeButtonVisible = m_maximizeButtonVisible && !buttonIsUserHidden(m_maximizeButtonType, isEnabled); d->maximizeButtonChanged.emit(m_maximizeButtonVisible, isEnabled, m_maximizeButtonType); } void TitleBar::updateCloseButton() { const bool anyNonClosable = group() ? group()->anyNonClosable() : (floatingWindow() ? floatingWindow()->anyNonClosable() : false); const bool isEnabled = !anyNonClosable; setCloseButtonEnabled(isEnabled); setCloseButtonVisible(!buttonIsUserHidden(TitleBarButtonType::Close, isEnabled)); } void TitleBar::toggleMaximized() { if (!m_floatingWindow) return; if (m_floatingWindow->view()->isMaximized()) m_floatingWindow->view()->showNormal(); else m_floatingWindow->view()->showMaximized(); } bool TitleBar::isOverlayed() const { return m_group && m_group->isOverlayed(); } void TitleBar::setCloseButtonEnabled(bool enabled) { if (enabled != m_closeButtonEnabled) { m_closeButtonEnabled = enabled; d->closeButtonChanged.emit(m_closeButtonVisible, enabled); } } void TitleBar::setCloseButtonVisible(bool visible) { if (visible != m_closeButtonVisible) { m_closeButtonVisible = visible; d->closeButtonChanged.emit(m_closeButtonVisible, m_closeButtonEnabled); } } void TitleBar::setFloatButtonVisible(bool visible) { if (visible != m_floatButtonVisible) { m_floatButtonVisible = visible; d->floatButtonVisibleChanged.emit(visible); } } void TitleBar::setFloatButtonToolTip(const QString &tip) { if (tip != m_floatButtonToolTip) { m_floatButtonToolTip = tip; d->floatButtonToolTipChanged.emit(tip); } } void TitleBar::setTitle(const QString &title) { if (title != m_title) { m_title = title; view()->update(); d->titleChanged.emit(); } } void TitleBar::setIcon(const Icon &icon) { m_icon = icon; d->iconChanged.emit(); } void TitleBar::onCloseClicked() { CloseReasonSetter reason(CloseReason::TitleBarCloseButton); const bool closeOnlyCurrentTab = Config::self().flags() & Config::Flag_CloseOnlyCurrentTab; if (m_group) { if (closeOnlyCurrentTab) { if (auto dw = m_group->currentDockWidget()) { dw->view()->close(); } else { // Doesn't happen KDDW_ERROR("Group with no dock widgets"); } } else { if (m_group->isTheOnlyGroup() && m_group->isInFloatingWindow()) { m_group->view()->d->closeRootView(); } else { m_group->view()->close(); } } } else if (m_floatingWindow) { if (closeOnlyCurrentTab) { if (Group *f = m_floatingWindow->singleFrame()) { if (DockWidget *dw = f->currentDockWidget()) { dw->view()->close(); } else { // Doesn't happen KDDW_ERROR("Group with no dock widgets"); } } else { m_floatingWindow->view()->close(); } } else { m_floatingWindow->view()->close(); } } else if (m_isStandalone) { view()->d->closeRootView(); } } void TitleBar::onFloatClicked() { const DockWidget::List dockWidgets = this->dockWidgets(); if (isFloating()) { // Let's dock it if (dockWidgets.isEmpty()) { KDDW_ERROR("TitleBar::onFloatClicked: empty list. Shouldn't happen"); return; } if (dockWidgets.size() == 1) { // Case 1: Single dockwidget floating dockWidgets[0]->setFloating(false); } else { // Case 2: Multiple dockwidgets are tabbed together and floating // Possible improvement: Just reuse the whole group and put it back. // The group currently doesn't remember the position in the main window // so use an hack for now if (!dockWidgets.isEmpty()) { // could be empty during destruction, maybe if (!dockWidgets.constFirst()->hasPreviousDockedLocation()) { // Don't attempt, there's no previous docked location return; } Group *group = dockWidgets[0]->d->group(); // suppress "isFloatingChanged" signals, as we're doing the float/unfloat hack Group::s_inFloatHack = group ? group->layoutItem() : nullptr; int i = 0; DockWidget *current = nullptr; for (auto dock : std::as_const(dockWidgets)) { if (!current && dock->isCurrentTab()) current = dock; dock->setFloating(true); dock->dptr()->m_lastPositions->m_tabIndex = i; dock->setFloating(false); ++i; } Group::s_inFloatHack = nullptr; // Restore the current tab if (current) current->setAsCurrentTab(); } } } else { // Let's float it if (dockWidgets.size() == 1) { // If there's a single dock widget, just call DockWidget::setFloating(true). The only // difference is that it has logic for using the last used geometry for the floating // window dockWidgets[0]->setFloating(true); } else { makeWindow(); } } } void TitleBar::onMaximizeClicked() { toggleMaximized(); } void TitleBar::onMinimizeClicked() { if (!m_floatingWindow) return; if (m_floatingWindow->isUtilityWindow()) { // Qt::Tool windows don't appear in the task bar. // Unless someone tells me a good reason to allow this situation. return; } m_floatingWindow->view()->showMinimized(); } void TitleBar::onAutoHideClicked() { if (!m_group) { // Doesn't happen KDDW_ERROR("Minimize not supported on floating windows"); return; } const auto &dockwidgets = m_group->dockWidgets(); if (isOverlayed() && dockwidgets.size() != 1) { // Doesn't happen KDDW_ERROR("TitleBar::onAutoHideClicked: There can only be a single dock widget per titlebar overlayed"); return; } const bool groupedAutoHide = Config::hasFlag(Config::Flag_AutoHideAsTabGroups); const auto currentDw = m_group->currentDockWidget(); auto registry = DockRegistry::self(); if (isOverlayed()) { // Restore it: auto dw = dockwidgets.first(); MainWindow *mainWindow = dw->mainWindow(); auto sideBarGroup = groupedAutoHide ? registry->sideBarGroupingFor(dw) : DockWidget::List(); if (sideBarGroup.isEmpty()) { mainWindow->restoreFromSideBar(dw); } else { // Config::Flag_AutoHideAsTabGroups case. Restore its friends too for (auto it = sideBarGroup.rbegin(); it != sideBarGroup.rend(); ++it) { mainWindow->restoreFromSideBar(*it); } dw->setAsCurrentTab(); registry->removeSideBarGrouping(sideBarGroup); } } else { // Send it to sidebar: if (groupedAutoHide) registry->addSideBarGrouping(dockwidgets); CloseReasonSetter reason(CloseReason::MovedToSideBar); for (DockWidget *dw : dockwidgets) { if (groupedAutoHide || dw == currentDw) dw->moveToSideBar(); } } } bool TitleBar::closeButtonEnabled() const { return m_closeButtonEnabled; } std::unique_ptr TitleBar::makeWindow() { if (m_isStandalone) return {}; // not applicable if (!isVisible() && view()->rootView()->controller()->isVisible() && !(Config::self().flags() & Config::Flag_ShowButtonsOnTabBarIfTitleBarHidden)) { // When using Flag_ShowButtonsOnTabBarIfTitleBarHidden we forward the call from the tab // bar's buttons to the title bar's buttons, just to reuse logic KDDW_ERROR("TitleBar::makeWindow shouldn't be called on invisible title bar this={}, root.isVisible={}", ( void * )this, view()->rootView()->isVisible()); if (m_group) { KDDW_ERROR("this={}; actual={}", ( void * )this, ( void * )m_group->actualTitleBar()); } else if (m_floatingWindow) { KDDW_ERROR("Has floating window with titlebar={}, isVisible={}", ( void * )m_floatingWindow->titleBar(), m_floatingWindow->isVisible()); } assert(false); return {}; } if (m_floatingWindow) { // We're already a floating window, no detach needed return std::make_unique(m_floatingWindow, this); } if (FloatingWindow *fw = floatingWindow()) { // Already floating if (m_group->isTheOnlyGroup()) { // We don't detach. This one drags the entire window // instead. KDDW_DEBUG("TitleBar::makeWindow no detach needed"); return std::make_unique(fw, this); } } Rect r = m_group->view()->geometry(); r.moveTopLeft(m_group->mapToGlobal(Point(0, 0))); auto floatingWindow = new Core::FloatingWindow(m_group, {}); floatingWindow->setSuggestedGeometry(r, SuggestedGeometryHint_GeometryIsFromDocked); floatingWindow->view()->show(); auto draggable = KDDockWidgets::usesNativeTitleBar() ? static_cast(floatingWindow) : static_cast(this); return std::make_unique(floatingWindow, draggable); } bool TitleBar::isWindow() const { return m_floatingWindow != nullptr; } Core::DockWidget::List TitleBar::dockWidgets() const { if (m_floatingWindow) { DockWidget::List result; const auto groups = m_floatingWindow->groups(); for (Group *group : groups) { result.append(group->dockWidgets()); } return result; } if (m_group) return m_group->dockWidgets(); if (m_isStandalone) return {}; // not applicable KDDW_ERROR("TitleBar::dockWidget: shouldn't happen"); return {}; } Core::DockWidget *TitleBar::singleDockWidget() const { const DockWidget::List dockWidgets = this->dockWidgets(); return dockWidgets.isEmpty() ? nullptr : dockWidgets.first(); } bool TitleBar::isFloating() const { if (m_floatingWindow) return true; if (m_group) return m_group->isFloating(); if (m_isStandalone) return false; // not applicable KDDW_ERROR("TitleBar::isFloating: shouldn't happen"); return false; } bool TitleBar::isFocused() const { if (m_group) return m_group->isFocused(); else if (m_floatingWindow) return m_floatingWindow->view()->isActiveWindow(); else if (m_isStandalone) return view()->isActiveWindow(); return false; } void TitleBar::updateFloatButton() { setFloatButtonToolTip(floatingWindow() ? tr("Dock window") : tr("Undock window")); setFloatButtonVisible(supportsFloatingButton() && !buttonIsUserHidden(TitleBarButtonType::Float, /*enabled=*/true)); } QString TitleBar::floatButtonToolTip() const { return m_floatButtonToolTip; } TabBar *TitleBar::tabBar() const { if (m_floatingWindow && m_floatingWindow->hasSingleGroup()) { if (Group *group = m_floatingWindow->singleFrame()) { return group->stack()->tabBar(); } else { // Shouldn't happen KDDW_ERROR("Expected a group"); } } else if (m_group) { return m_group->stack()->tabBar(); } return nullptr; } TitleBarButtonType TitleBar::maximizeButtonType() const { return m_maximizeButtonType; } bool TitleBar::isStandalone() const { return m_isStandalone; } bool TitleBar::buttonIsUserHidden(TitleBarButtonType type) const { return d->m_userHiddenButtonTypes & type; } bool TitleBar::buttonIsUserHidden(TitleBarButtonType type, bool enabled) const { if (buttonIsUserHidden(type)) return true; if (!enabled) return buttonHidesIfDisabled(type); return false; } void TitleBar::setUserHiddenButtons(TitleBarButtonTypes types) { if (d->m_userHiddenButtonTypes != types) { d->m_userHiddenButtonTypes = types; updateButtons(); } } TitleBar::Private *TitleBar::dptr() const { return d; } void TitleBar::setHideDisabledButtons(TitleBarButtonTypes types) { if (d->m_buttonsToHideIfDisabled != types) { d->m_buttonsToHideIfDisabled = types; updateButtons(); } } bool TitleBar::buttonHidesIfDisabled(TitleBarButtonType type) const { return d->m_buttonsToHideIfDisabled & type; }