375 lines
10 KiB
C++
375 lines
10 KiB
C++
/*
|
|
This file is part of KDDockWidgets.
|
|
|
|
SPDX-FileCopyrightText: 2020 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
|
|
Author: Sérgio Martins <sergio.martins@kdab.com>
|
|
|
|
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
|
|
|
|
Contact KDAB at <info@kdab.com> for commercial licensing options.
|
|
*/
|
|
|
|
#include "TabBar.h"
|
|
#include "TabBar_p.h"
|
|
#include "core/Draggable_p.h"
|
|
#include "Controller.h"
|
|
#include "core/ScopedValueRollback_p.h"
|
|
#include "core/Stack.h"
|
|
#include "core/FloatingWindow.h"
|
|
#include "core/DockWidget_p.h"
|
|
#include "core/Logging_p.h"
|
|
#include "views/TabBarViewInterface.h"
|
|
#include "Platform.h"
|
|
|
|
#include "core/DragController_p.h"
|
|
#include "core/Utils_p.h"
|
|
#include "Config.h"
|
|
#include "core/ViewFactory.h"
|
|
|
|
#include <cstdlib>
|
|
|
|
using namespace KDDockWidgets;
|
|
using namespace KDDockWidgets::Core;
|
|
|
|
Core::TabBar::TabBar(Stack *stack)
|
|
: Controller(ViewType::TabBar, Config::self().viewFactory()->createTabBar(this, stack->view()))
|
|
, Draggable(view())
|
|
, d(new Private(stack))
|
|
{
|
|
view()->init();
|
|
if (auto tvi = dynamic_cast<Core::TabBarViewInterface *>(view()))
|
|
tvi->setTabsAreMovable(tabsAreMovable());
|
|
}
|
|
|
|
Core::TabBar::~TabBar()
|
|
{
|
|
delete d;
|
|
}
|
|
|
|
bool Core::TabBar::tabsAreMovable() const
|
|
{
|
|
return Config::self().flags() & Config::Flag_AllowReorderTabs;
|
|
}
|
|
|
|
bool Core::TabBar::dragCanStart(Point pressPos, Point pos) const
|
|
{
|
|
// Here we allow the user to re-order tabs instead of dragging them off.
|
|
// To do that we just return false here, and QTabBar will handle the mouse event, assuming
|
|
// QTabBar::isMovable.
|
|
|
|
const bool defaultResult = Draggable::dragCanStart(pressPos, pos);
|
|
|
|
if (!defaultResult || !tabsAreMovable()) {
|
|
// Nothing more to do. If the drag wouldn't start anyway, return false.
|
|
// And if the tabs aren't movable, just return the default result, which just considers
|
|
// startDragDistance
|
|
return defaultResult;
|
|
}
|
|
|
|
const int index =
|
|
dynamic_cast<Core::TabBarViewInterface *>(view())->tabAt(view()->mapFromGlobal(pos));
|
|
if (index == -1)
|
|
return defaultResult;
|
|
|
|
const int deltaX = std::abs(pos.x() - pressPos.x());
|
|
const int deltaY = std::abs(pos.y() - pressPos.y());
|
|
|
|
const int startDragDistance = Platform::instance()->startDragDistance();
|
|
|
|
if (deltaY > 5 * startDragDistance) {
|
|
// Moving up or down too much results in a detach. No tab re-ordering allowed.
|
|
return true;
|
|
} else if (deltaY > startDragDistance && deltaX < startDragDistance) {
|
|
// Moved a bit up or down, but not left/right, then detach too.
|
|
// Only if it's going considerably left/right we allow to re-order tabs.
|
|
return true;
|
|
}
|
|
|
|
// Also detach if cursor moves outside the tab bar bounds (allows horizontal splitting)
|
|
const Point localPos = view()->mapFromGlobal(pos);
|
|
const Rect tabBarRect = view()->rect();
|
|
if (!tabBarRect.contains(localPos)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Core::DockWidget *Core::TabBar::dockWidgetAt(int index) const
|
|
{
|
|
if (index < 0 || index >= numDockWidgets())
|
|
return nullptr;
|
|
|
|
return const_cast<DockWidget *>(d->m_dockWidgets.value(index));
|
|
}
|
|
|
|
Core::DockWidget *Core::TabBar::dockWidgetAt(Point localPos) const
|
|
{
|
|
if (auto tvi = dynamic_cast<Core::TabBarViewInterface *>(view()))
|
|
return dockWidgetAt(tvi->tabAt(localPos));
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
int TabBar::indexOfDockWidget(const Core::DockWidget *dw) const
|
|
{
|
|
return d->m_dockWidgets.indexOf(dw);
|
|
}
|
|
|
|
void TabBar::removeDockWidget(Core::DockWidget *dw)
|
|
{
|
|
if (m_inDtor)
|
|
return;
|
|
|
|
auto it = d->aboutToDeleteConnections.find(dw);
|
|
if (it != d->aboutToDeleteConnections.end())
|
|
d->aboutToDeleteConnections.erase(it);
|
|
|
|
const bool wasCurrent = dw == d->m_currentDockWidget;
|
|
const int index = d->m_dockWidgets.indexOf(dw);
|
|
|
|
if (wasCurrent) {
|
|
const bool isLast = index == d->m_dockWidgets.count() - 1;
|
|
const int newCurrentIndex = isLast ? index - 1 : index + 1;
|
|
setCurrentIndex(newCurrentIndex);
|
|
}
|
|
|
|
d->m_removeGuard = true;
|
|
// The view might call setCurrenteIndex() before our d->m_dockWidgets reflectig the state.
|
|
// d->m_removeGuard protects against that.
|
|
if (auto tvi = dynamic_cast<Core::TabBarViewInterface *>(view()))
|
|
tvi->removeDockWidget(dw);
|
|
d->m_removeGuard = false;
|
|
|
|
d->m_dockWidgets.removeOne(dw);
|
|
group()->onDockWidgetCountChanged();
|
|
}
|
|
|
|
void TabBar::insertDockWidget(int index, Core::DockWidget *dw, const Icon &icon,
|
|
const QString &title)
|
|
{
|
|
if (auto oldGroup = dw->dptr()->group()) {
|
|
if (auto oldTabBar = oldGroup->tabBar()) {
|
|
if (oldTabBar != this) {
|
|
oldTabBar->removeDockWidget(dw);
|
|
}
|
|
}
|
|
}
|
|
|
|
d->m_dockWidgets.insert(index, dw);
|
|
KDBindings::ScopedConnection conn = dw->d->aboutToDelete.connect([this, dw] {
|
|
removeDockWidget(dw);
|
|
});
|
|
d->aboutToDeleteConnections[dw] = std::move(conn);
|
|
|
|
dynamic_cast<Core::TabBarViewInterface *>(view())->insertDockWidget(index, dw, icon, title);
|
|
if (!d->m_currentDockWidget)
|
|
setCurrentDockWidget(dw);
|
|
|
|
group()->onDockWidgetCountChanged();
|
|
}
|
|
|
|
std::unique_ptr<WindowBeingDragged> Core::TabBar::makeWindow()
|
|
{
|
|
auto dock = d->m_lastPressedDockWidget;
|
|
d->m_lastPressedDockWidget = nullptr;
|
|
|
|
const bool hideTitleBarWhenTabsVisible =
|
|
Config::self().flags() & Config::Flag_HideTitleBarWhenTabsVisible;
|
|
const bool alwaysShowTabs = Config::self().flags() & Config::Flag_AlwaysShowTabs;
|
|
|
|
if (hideTitleBarWhenTabsVisible) {
|
|
if (dock) {
|
|
if (alwaysShowTabs && hasSingleDockWidget()) {
|
|
// Case #1. User is dragging a tab but there's only 1 tab (and tabs are always
|
|
// visible), so drag everything instead, no detaching happens
|
|
return d->m_stack->makeWindow();
|
|
}
|
|
} else {
|
|
// Case #2. User is dragging on the QTabBar background, not on an actual tab.
|
|
// As Flag_HideTitleBarWhenTabsVisible is set, we let the user drag through the tab
|
|
// widget background.
|
|
return d->m_stack->makeWindow();
|
|
}
|
|
} else {
|
|
if (dock && hasSingleDockWidget() && alwaysShowTabs) {
|
|
// Case #3. window with title bar and single tab, no detaching should happen, just use
|
|
// the title bar.
|
|
return {};
|
|
}
|
|
}
|
|
|
|
if (!dock)
|
|
return {};
|
|
|
|
FloatingWindow *floatingWindow = group()->detachTab(dock);
|
|
if (!floatingWindow)
|
|
return {};
|
|
|
|
auto draggable = KDDockWidgets::usesNativeTitleBar() ? static_cast<Draggable *>(floatingWindow)
|
|
: static_cast<Draggable *>(this);
|
|
return std::make_unique<WindowBeingDragged>(floatingWindow, draggable);
|
|
}
|
|
|
|
bool Core::TabBar::isWindow() const
|
|
{
|
|
// Same semantics as tab widget, no need to duplicate logic
|
|
return d->m_stack->isWindow();
|
|
}
|
|
|
|
void Core::TabBar::onMousePress(Point localPos)
|
|
{
|
|
d->m_lastPressedDockWidget = dockWidgetAt(localPos);
|
|
Group *group = this->group();
|
|
if (Config::self().flags() & Config::Flag_TitleBarIsFocusable) {
|
|
// User clicked on a tab which was already focused
|
|
// A tab changing also counts as a change of scope
|
|
group->FocusScope::focus(Qt::MouseFocusReason);
|
|
}
|
|
}
|
|
|
|
bool Core::TabBar::onMouseDoubleClick(Point localPos)
|
|
{
|
|
if (Config::self().flags() & Config::Flag_DisableDoubleClick) {
|
|
return false;
|
|
} else if (DockWidget *dw = dockWidgetAt(localPos)) {
|
|
dw->setFloating(!dw->isFloating());
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Core::TabBar::hasSingleDockWidget() const
|
|
{
|
|
return numDockWidgets() == 1;
|
|
}
|
|
|
|
int Core::TabBar::numDockWidgets() const
|
|
{
|
|
return d->m_dockWidgets.size();
|
|
}
|
|
|
|
Core::DockWidget *Core::TabBar::singleDockWidget() const
|
|
{
|
|
return d->m_stack->singleDockWidget();
|
|
}
|
|
|
|
bool Core::TabBar::isMDI() const
|
|
{
|
|
Group *f = group();
|
|
return f && f->isMDI();
|
|
}
|
|
|
|
Group *Core::TabBar::group() const
|
|
{
|
|
return d->m_stack->group();
|
|
}
|
|
|
|
Stack *TabBar::stack() const
|
|
{
|
|
return d->m_stack;
|
|
}
|
|
|
|
// NOLINTNEXTLINE(bugprone-easily-swappable-parameters
|
|
void Core::TabBar::Private::moveTabTo(int from, int to)
|
|
{
|
|
auto fromDw = m_dockWidgets.takeAt(from);
|
|
m_dockWidgets.insert(to, fromDw);
|
|
}
|
|
|
|
void Core::TabBar::moveTabTo(int from, int to)
|
|
{
|
|
ScopedValueRollback guard(d->m_isMovingTab, true);
|
|
|
|
d->moveTabTo(from, to);
|
|
|
|
// Tell GUI:
|
|
if (auto tvi = dynamic_cast<Core::TabBarViewInterface *>(view()))
|
|
tvi->moveTabTo(from, to);
|
|
}
|
|
|
|
QString Core::TabBar::text(int index) const
|
|
{
|
|
if (auto tvi = dynamic_cast<Core::TabBarViewInterface *>(view()))
|
|
return tvi->text(index);
|
|
|
|
return {};
|
|
}
|
|
|
|
Rect Core::TabBar::rectForTab(int index) const
|
|
{
|
|
if (auto tvi = dynamic_cast<Core::TabBarViewInterface *>(view()))
|
|
return tvi->rectForTab(index);
|
|
|
|
return {};
|
|
}
|
|
|
|
DockWidget *TabBar::currentDockWidget() const
|
|
{
|
|
return d->m_currentDockWidget;
|
|
}
|
|
|
|
void TabBar::setCurrentDockWidget(DockWidget *dw)
|
|
{
|
|
if (d->m_removeGuard) // We're in the middle of a remove.
|
|
return;
|
|
|
|
if (dw == d->m_currentDockWidget)
|
|
return;
|
|
|
|
setCurrentIndex(indexOfDockWidget(dw));
|
|
}
|
|
|
|
int TabBar::currentIndex() const
|
|
{
|
|
if (!d->m_currentDockWidget)
|
|
return -1;
|
|
|
|
return d->m_dockWidgets.indexOf(d->m_currentDockWidget);
|
|
}
|
|
|
|
void TabBar::setCurrentIndex(int index)
|
|
{
|
|
if (d->m_removeGuard) // We're in the middle of a remove.
|
|
return;
|
|
|
|
auto newCurrentDw = dockWidgetAt(index);
|
|
if (newCurrentDw == d->m_currentDockWidget)
|
|
return;
|
|
|
|
if (d->m_currentDockWidget) {
|
|
d->m_currentDockWidget->d->isCurrentTabChanged.emit(false);
|
|
}
|
|
|
|
d->m_currentDockWidget = newCurrentDw;
|
|
d->currentDockWidgetChanged.emit(newCurrentDw);
|
|
if (auto tvi = dynamic_cast<Core::TabBarViewInterface *>(view()))
|
|
tvi->setCurrentIndex(index);
|
|
|
|
if (newCurrentDw)
|
|
newCurrentDw->d->isCurrentTabChanged.emit(true);
|
|
}
|
|
|
|
void TabBar::renameTab(int index, const QString &text)
|
|
{
|
|
if (auto tvi = dynamic_cast<Core::TabBarViewInterface *>(view()))
|
|
tvi->renameTab(index, text);
|
|
}
|
|
|
|
void TabBar::changeTabIcon(int index, const Icon &icon)
|
|
{
|
|
if (auto tvi = dynamic_cast<Core::TabBarViewInterface *>(view()))
|
|
tvi->changeTabIcon(index, icon);
|
|
}
|
|
|
|
bool TabBar::isMovingTab() const
|
|
{
|
|
return d->m_isMovingTab;
|
|
}
|
|
|
|
TabBar::Private *TabBar::dptr() const
|
|
{
|
|
return d;
|
|
}
|