Qt-Advanced-Docking-System/src/AutoHideTab.cpp
Ahmad Syarifuddin 61573cba16
Fix crash in auto hide tab (#552)
- Crash is caused by dragging an autohide tab out when it is an icon only.
- The crash can actually happen with non icon only tabs, but is masked by the orientation check.
- Soln: Check if auto hide container exists first on mouse finish dragging event.
2023-08-24 11:15:43 +02:00

562 lines
15 KiB
C++

/*******************************************************************************
** Qt Advanced Docking System
** Copyright (C) 2017 Uwe Kindler
**
** This library is free software; you can redistribute it and/or
** modify it under the terms of the GNU Lesser General Public
** License as published by the Free Software Foundation; either
** version 2.1 of the License, or (at your option) any later version.
**
** This library is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
** Lesser General Public License for more details.
**
** You should have received a copy of the GNU Lesser General Public
** License along with this library; If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
//============================================================================
/// \file AutoHideTab.cpp
/// \author Syarif Fakhri
/// \date 05.09.2022
/// \brief Implementation of CAutoHideTab class
//============================================================================
//============================================================================
// INCLUDES
//============================================================================
#include "AutoHideTab.h"
#include <QBoxLayout>
#include <QApplication>
#include <QElapsedTimer>
#include <QMenu>
#include "AutoHideDockContainer.h"
#include "AutoHideSideBar.h"
#include "DockAreaWidget.h"
#include "DockManager.h"
#include "DockWidget.h"
#include "FloatingDragPreview.h"
#include "DockOverlay.h"
namespace ads
{
static const char* const LocationProperty = "Location";
/**
* Private data class of CDockWidgetTab class (pimpl)
*/
struct AutoHideTabPrivate
{
CAutoHideTab* _this;
CDockWidget* DockWidget = nullptr;
CAutoHideSideBar* SideBar = nullptr;
Qt::Orientation Orientation{Qt::Vertical};
QElapsedTimer TimeSinceHoverMousePress;
bool MousePressed = false;
eDragState DragState = DraggingInactive;
QPoint GlobalDragStartMousePosition;
QPoint DragStartMousePosition;
IFloatingWidget* FloatingWidget = nullptr;
Qt::Orientation DragStartOrientation;
/**
* Private data constructor
*/
AutoHideTabPrivate(CAutoHideTab* _public);
/**
* Update the orientation, visibility and spacing based on the area of
* the side bar
*/
void updateOrientation();
/**
* Convenience function to ease dock container access
*/
CDockContainerWidget* dockContainer() const
{
return DockWidget ? DockWidget->dockContainer() : nullptr;
}
/**
* Forward this event to the dock container
*/
void forwardEventToDockContainer(QEvent* event)
{
auto DockContainer = dockContainer();
if (DockContainer)
{
DockContainer->handleAutoHideWidgetEvent(event, _this);
}
}
/**
* 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);
Action->setProperty("Location", Location);
QObject::connect(Action, &QAction::triggered, _this, &CAutoHideTab::onAutoHideToActionClicked);
Action->setEnabled(Location != _this->sideBarLocation());
return Action;
}
/**
* Test function for current drag state
*/
bool isDraggingState(eDragState dragState) const
{
return this->DragState == dragState;
}
/**
* Saves the drag start position in global and local coordinates
*/
void saveDragStartMousePosition(const QPoint& GlobalPos)
{
GlobalDragStartMousePosition = GlobalPos;
DragStartMousePosition = _this->mapFromGlobal(GlobalPos);
}
/**
* 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);
template <typename T>
IFloatingWidget* createFloatingWidget(T* Widget)
{
auto w = new CFloatingDragPreview(Widget);
_this->connect(w, &CFloatingDragPreview::draggingCanceled, [=]()
{
DragState = DraggingInactive;
});
return w;
}
}; // struct DockWidgetTabPrivate
//============================================================================
AutoHideTabPrivate::AutoHideTabPrivate(CAutoHideTab* _public) :
_this(_public)
{
}
//============================================================================
void AutoHideTabPrivate::updateOrientation()
{
bool IconOnly = CDockManager::testAutoHideConfigFlag(CDockManager::AutoHideSideBarsIconOnly);
if (IconOnly && !_this->icon().isNull())
{
_this->setText("");
_this->setOrientation(Qt::Horizontal);
}
else
{
auto area = SideBar->sideBarLocation();
_this->setOrientation((area == SideBarBottom || area == SideBarTop) ? Qt::Horizontal : Qt::Vertical);
}
}
//============================================================================
bool AutoHideTabPrivate::startFloating(eDragState DraggingState)
{
auto DockArea = DockWidget->dockAreaWidget();
ADS_PRINT("isFloating " << dockContainer->isFloating());
ADS_PRINT("startFloating");
DragState = DraggingState;
IFloatingWidget* FloatingWidget = nullptr;
FloatingWidget = createFloatingWidget(DockArea);
auto Size = DockArea->size();
auto StartPos = DragStartMousePosition;
auto AutoHideContainer = DockWidget->autoHideDockContainer();
DragStartOrientation = AutoHideContainer->orientation();
switch (SideBar->sideBarLocation())
{
case SideBarLeft:
StartPos.rx() = AutoHideContainer->rect().left() + 10;
break;
case SideBarRight:
StartPos.rx() = AutoHideContainer->rect().right() - 10;
break;
case SideBarTop:
StartPos.ry() = AutoHideContainer->rect().top() + 10;
break;
case SideBarBottom:
StartPos.ry() = AutoHideContainer->rect().bottom() - 10;
break;
case SideBarNone:
return false;
}
FloatingWidget->startFloating(StartPos, Size, DraggingFloatingWidget, _this);
auto DockManager = DockWidget->dockManager();
auto Overlay = DockManager->containerOverlay();
Overlay->setAllowedAreas(OuterDockAreas);
this->FloatingWidget = FloatingWidget;
qApp->postEvent(DockWidget, new QEvent((QEvent::Type)internal::DockedWidgetDragStartEvent));
return true;
}
//============================================================================
void CAutoHideTab::setSideBar(CAutoHideSideBar* SideTabBar)
{
d->SideBar = SideTabBar;
if (d->SideBar)
{
d->updateOrientation();
}
}
//============================================================================
CAutoHideSideBar* CAutoHideTab::sideBar() const
{
return d->SideBar;
}
//============================================================================
void CAutoHideTab::removeFromSideBar()
{
if (d->SideBar == nullptr)
{
return;
}
d->SideBar->removeTab(this);
setSideBar(nullptr);
}
//============================================================================
CAutoHideTab::CAutoHideTab(QWidget* parent) :
CPushButton(parent),
d(new AutoHideTabPrivate(this))
{
setAttribute(Qt::WA_NoMousePropagation);
setFocusPolicy(Qt::NoFocus);
}
//============================================================================
CAutoHideTab::~CAutoHideTab()
{
ADS_PRINT("~CDockWidgetSideTab()");
delete d;
}
//============================================================================
void CAutoHideTab::updateStyle()
{
internal::repolishStyle(this, internal::RepolishDirectChildren);
update();
}
//============================================================================
SideBarLocation CAutoHideTab::sideBarLocation() const
{
if (d->SideBar)
{
return d->SideBar->sideBarLocation();
}
return SideBarLeft;
}
//============================================================================
void CAutoHideTab::setOrientation(Qt::Orientation Orientation)
{
d->Orientation = Orientation;
if (orientation() == Qt::Horizontal)
{
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed);
}
else
{
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum);
}
CPushButton::setButtonOrientation((Qt::Horizontal == Orientation)
? CPushButton::Horizontal : CPushButton::VerticalTopToBottom);
updateStyle();
}
//============================================================================
Qt::Orientation CAutoHideTab::orientation() const
{
return d->Orientation;
}
//============================================================================
bool CAutoHideTab::isActiveTab() const
{
if (d->DockWidget && d->DockWidget->autoHideDockContainer())
{
return d->DockWidget->autoHideDockContainer()->isVisible();
}
return false;
}
//============================================================================
CDockWidget* CAutoHideTab::dockWidget() const
{
return d->DockWidget;
}
//============================================================================
void CAutoHideTab::setDockWidget(CDockWidget* DockWidget)
{
if (!DockWidget)
{
return;
}
d->DockWidget = DockWidget;
setText(DockWidget->windowTitle());
setIcon(d->DockWidget->icon());
setToolTip(DockWidget->windowTitle());
}
//============================================================================
bool CAutoHideTab::event(QEvent* event)
{
if (!CDockManager::testAutoHideConfigFlag(CDockManager::AutoHideShowOnMouseOver))
{
return Super::event(event);
}
switch (event->type())
{
case QEvent::Enter:
case QEvent::Leave:
d->forwardEventToDockContainer(event);
break;
default:
break;
}
return Super::event(event);
}
//============================================================================
bool CAutoHideTab::iconOnly() const
{
return CDockManager::testAutoHideConfigFlag(CDockManager::AutoHideSideBarsIconOnly) && !icon().isNull();
}
//============================================================================
void CAutoHideTab::contextMenuEvent(QContextMenuEvent* ev)
{
ev->accept();
d->saveDragStartMousePosition(ev->globalPos());
const bool isFloatable = d->DockWidget->features().testFlag(CDockWidget::DockWidgetFloatable);
QAction* Action;
QMenu Menu(this);
Action = Menu.addAction(tr("Detach"), this, SLOT(setDockWidgetFloating()));
Action->setEnabled(isFloatable);
auto IsPinnable = d->DockWidget->features().testFlag(CDockWidget::DockWidgetPinnable);
Action->setEnabled(IsPinnable);
auto menu = Menu.addMenu(tr("Pin To..."));
menu->setEnabled(IsPinnable);
d->createAutoHideToAction(tr("Top"), SideBarTop, menu);
d->createAutoHideToAction(tr("Left"), SideBarLeft, menu);
d->createAutoHideToAction(tr("Right"), SideBarRight, menu);
d->createAutoHideToAction(tr("Bottom"), SideBarBottom, menu);
Action = Menu.addAction(tr("Unpin (Dock)"), this, SLOT(unpinDockWidget()));
Menu.addSeparator();
Action = Menu.addAction(tr("Close"), this, SLOT(requestCloseDockWidget()));
Action->setEnabled(d->DockWidget->features().testFlag(CDockWidget::DockWidgetClosable));
Menu.exec(ev->globalPos());
}
//============================================================================
void CAutoHideTab::setDockWidgetFloating()
{
d->DockWidget->setFloating();
}
//============================================================================
void CAutoHideTab::unpinDockWidget()
{
d->DockWidget->setAutoHide(false);
}
//===========================================================================
void CAutoHideTab::onAutoHideToActionClicked()
{
int Location = sender()->property(LocationProperty).toInt();
d->DockWidget->setAutoHide(true, (SideBarLocation)Location);
}
//============================================================================
void CAutoHideTab::mousePressEvent(QMouseEvent* ev)
{
// If AutoHideShowOnMouseOver is active, then the showing is triggered
// by a MousePressEvent sent to this tab. To prevent accidental hiding
// of the tab by a mouse click, we wait at least 500 ms before we accept
// the mouse click
if (!ev->spontaneous())
{
d->TimeSinceHoverMousePress.restart();
d->forwardEventToDockContainer(ev);
}
else if (d->TimeSinceHoverMousePress.hasExpired(500))
{
d->forwardEventToDockContainer(ev);
}
if (ev->button() == Qt::LeftButton)
{
ev->accept();
d->MousePressed = true;
d->saveDragStartMousePosition(internal::globalPositionOf(ev));
d->DragState = DraggingMousePressed;
}
Super::mousePressEvent(ev);
}
//============================================================================
void CAutoHideTab::mouseReleaseEvent(QMouseEvent* ev)
{
if (ev->button() == Qt::LeftButton)
{
d->MousePressed = false;
auto CurrentDragState = d->DragState;
d->GlobalDragStartMousePosition = QPoint();
d->DragStartMousePosition = QPoint();
d->DragState = DraggingInactive;
switch (CurrentDragState)
{
case DraggingTab:
// End of tab moving, emit signal
/*if (d->DockArea)
{
ev->accept();
Q_EMIT moved(internal::globalPositionOf(ev));
}*/
break;
case DraggingFloatingWidget:
ev->accept();
d->FloatingWidget->finishDragging();
if (d->DockWidget->autoHideDockContainer() && d->DragStartOrientation != orientation())
{
d->DockWidget->autoHideDockContainer()->resetToInitialDockWidgetSize();
}
break;
default:
break; // do nothing
}
}
Super::mouseReleaseEvent(ev);
}
//============================================================================
void CAutoHideTab::mouseMoveEvent(QMouseEvent* ev)
{
if (!(ev->buttons() & Qt::LeftButton) || d->isDraggingState(DraggingInactive))
{
d->DragState = DraggingInactive;
Super::mouseMoveEvent(ev);
return;
}
// move floating window
if (d->isDraggingState(DraggingFloatingWidget))
{
d->FloatingWidget->moveFloating();
Super::mouseMoveEvent(ev);
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(ev);
}
auto MappedPos = mapToParent(ev->pos());
bool MouseOutsideBar = (MappedPos.x() < 0) || (MappedPos.x() > parentWidget()->rect().right());
// Maybe a fixed drag distance is better here ?
int DragDistanceY = qAbs(d->GlobalDragStartMousePosition.y() - internal::globalPositionOf(ev).y());
if (DragDistanceY >= CDockManager::startDragDistance() || MouseOutsideBar)
{
// Floating is only allowed for widgets that are floatable
// We can create the drag preview if the widget is movable.
auto Features = d->DockWidget->features();
if (Features.testFlag(CDockWidget::DockWidgetFloatable) || (Features.testFlag(CDockWidget::DockWidgetMovable)))
{
d->startFloating();
}
return;
}
Super::mouseMoveEvent(ev);
}
//============================================================================
void CAutoHideTab::requestCloseDockWidget()
{
d->DockWidget->requestCloseDockWidget();
}
//============================================================================
int CAutoHideTab::tabIndex() const
{
if (!d->SideBar)
{
return -1;
}
return d->SideBar->indexOfTab(*this);
}
}