Gui: Add dragging support for custom ToolBarAreas

This adds dragging support for toolbars placed in custom toolbar areas.
It is achived by creating our own custom ToolBar class that handles
logic of providing custom gripper. This is required because Qt hides
gripper when toolbar is not direct child of the main window which cannot
be overriden. Custom class should allow us to create more capable
toolbars in the future so it should be great addition on its own.

Co-Authored-By: Kacper Donat <kacper@kadet.net>
This commit is contained in:
Zheng, Lei
2024-06-02 16:11:14 +02:00
committed by Kacper Donat
parent 53ee68c02d
commit 214322665b
4 changed files with 324 additions and 108 deletions

View File

@@ -813,14 +813,6 @@ QMenu* MainWindow::createPopupMenu ()
populateDockWindowMenu(menu);
menu->addSeparator();
populateToolBarMenu(menu);
QMenu *undockMenu = new QMenu(menu);
ToolBarManager::getInstance()->populateUndockMenu(undockMenu);
if (undockMenu->actions().isEmpty()) {
delete undockMenu;
}
else {
menu->addMenu(undockMenu);
}
menu->addSeparator();
Workbench* wb = WorkbenchManager::instance()->active();
if (wb) {

View File

@@ -27,6 +27,7 @@
#include "MainWindow.h"
#include "ToolBarAreaWidget.h"
#include "ToolBarManager.h"
#include <Base/Tools.h>
using namespace Gui;
@@ -53,6 +54,10 @@ void ToolBarAreaWidget::addWidget(QWidget* widget)
return;
}
if (auto toolbar = qobject_cast<ToolBar*>(widget)) {
toolbar->updateCustomGripVisibility();
}
_layout->addWidget(widget);
adjustParent();
@@ -80,6 +85,10 @@ void ToolBarAreaWidget::insertWidget(int index, QWidget* widget)
_layout->insertWidget(index, widget);
if (auto toolbar = qobject_cast<ToolBar*>(widget)) {
toolbar->updateCustomGripVisibility();
}
adjustParent();
saveState();
}
@@ -88,6 +97,10 @@ void ToolBarAreaWidget::removeWidget(QWidget* widget)
{
_layout->removeWidget(widget);
if (auto toolbar = qobject_cast<ToolBar*>(widget)) {
toolbar->updateCustomGripVisibility();
}
QString name = widget->objectName();
if (!name.isEmpty()) {
Base::ConnectionBlocker block(_conn);

View File

@@ -22,13 +22,17 @@
#include "PreCompiled.h"
#ifndef _PreComp_
#include <QAction>
#include <QApplication>
#include <QHBoxLayout>
#include <QMenuBar>
#include <QMouseEvent>
#include <QStatusBar>
#include <QToolButton>
# include <QAction>
# include <QApplication>
# include <QHBoxLayout>
# include <QMenuBar>
# include <QMouseEvent>
# include <QPainter>
# include <QPointer>
# include <QStatusBar>
# include <QToolBar>
# include <QToolButton>
# include <QStyleOption>
#endif
#include <boost/algorithm/string/predicate.hpp>
@@ -162,6 +166,214 @@ QList<ToolBarItem*> ToolBarItem::getItems() const
// -----------------------------------------------------------
ToolBar::ToolBar()
: QToolBar()
{
setupConnections();
}
ToolBar::ToolBar(QWidget* parent)
: QToolBar(parent)
{
connect(this, &QToolBar::topLevelChanged, this, &ToolBar::updateCustomGripVisibility);
connect(this, &QToolBar::movableChanged, this, &ToolBar::updateCustomGripVisibility);
}
void ToolBar::undock()
{
{
// We want to block only some signals - topLevelChanged should still be propagated
QSignalBlocker blocker(this);
if (auto area = ToolBarManager::getInstance()->toolBarAreaWidget(this)) {
area->removeWidget(this);
getMainWindow()->addToolBar(this);
}
setWindowFlags(Qt::Tool
| Qt::FramelessWindowHint
| Qt::X11BypassWindowManagerHint);
adjustSize();
setVisible(true);
}
Q_EMIT topLevelChanged(true);
}
void ToolBar::updateCustomGripVisibility()
{
auto area = ToolBarManager::getInstance()->toolBarAreaWidget(this);
auto grip = findChild<ToolBarGrip*>();
auto customGripIsRequired = isMovable() && area;
if (grip && !customGripIsRequired) {
grip->detach();
grip->deleteLater();
} else if (!grip && customGripIsRequired) {
grip = new ToolBarGrip(this);
grip->attach();
} else {
// either grip is present and should be present
// or is not present and should not be - nothing to do
return;
}
}
// -----------------------------------------------------------
ToolBarGrip::ToolBarGrip(QToolBar * parent)
: QWidget(parent)
{
updateSize();
}
void ToolBarGrip::attach()
{
if (isAttached()) {
return;
}
auto parent = qobject_cast<ToolBar*>(parentWidget());
if (!parent) {
return;
}
auto actions = parent->actions();
_action = parent->insertWidget(
// ensure that grip is always placed as the first widget in the toolbar
actions.isEmpty() ? nullptr : actions[0],
this
);
setCursor(Qt::OpenHandCursor);
setMouseTracking(true);
setVisible(true);
}
void ToolBarGrip::detach()
{
if (!isAttached()) {
return;
}
auto parent = qobject_cast<ToolBar*>(parentWidget());
if (!parent) {
return;
}
parent->removeAction(_action);
}
bool ToolBarGrip::isAttached() const
{
return _action != nullptr;
}
void ToolBarGrip::paintEvent(QPaintEvent*)
{
QPainter painter(this);
if (auto toolbar = qobject_cast<ToolBar*>(parentWidget())) {
QStyle *style = toolbar->style();
QStyleOptionToolBar opt;
toolbar->initStyleOption(&opt);
opt.features = QStyleOptionToolBar::Movable;
opt.rect = rect();
style->drawPrimitive(QStyle::PE_IndicatorToolBarHandle, &opt, &painter, toolbar);
}
}
void ToolBarGrip::mouseMoveEvent(QMouseEvent *me)
{
auto toolbar = qobject_cast<ToolBar*>(parentWidget());
if (!toolbar) {
return;
}
auto area = ToolBarManager::getInstance()->toolBarAreaWidget(toolbar);
if (!area) {
return;
}
QPoint pos = me->globalPos();
QRect rect(toolbar->mapToGlobal(QPoint(0,0)), toolbar->size());
// if mouse did not leave the area of toolbar do not continue with undocking it
if (rect.contains(pos)) {
return;
}
toolbar->undock();
// After removing from area, this grip will be deleted. In order to
// continue toolbar dragging (because the mouse button is still pressed),
// we fake mouse events and send to toolbar. For some reason,
// send/postEvent() does not work, only timer works.
QPointer tb(toolbar);
QTimer::singleShot(0, [tb] {
auto modifiers = QApplication::queryKeyboardModifiers();
auto buttons = QApplication::mouseButtons();
if (buttons != Qt::LeftButton
|| QWidget::mouseGrabber()
|| modifiers != Qt::NoModifier
|| !tb) {
return;
}
QPoint pos(10, 10);
QPoint globalPos(tb->mapToGlobal(pos));
QMouseEvent mouseEvent(
QEvent::MouseButtonPress,
pos, globalPos, Qt::LeftButton, buttons, modifiers);
QApplication::sendEvent(tb, &mouseEvent);
// Mouse follow the mouse press event with mouse move with some offset
// in order to activate toolbar dragging.
QPoint offset(30, 30);
QMouseEvent mouseMoveEvent(
QEvent::MouseMove,
pos+offset, globalPos+offset,
Qt::LeftButton, buttons, modifiers);
QApplication::sendEvent(tb, &mouseMoveEvent);
});
}
void ToolBarGrip::mousePressEvent(QMouseEvent *)
{
setCursor(Qt::ClosedHandCursor);
}
void ToolBarGrip::mouseReleaseEvent(QMouseEvent *)
{
setCursor(Qt::OpenHandCursor);
}
void ToolBarGrip::updateSize()
{
auto parent = qobject_cast<ToolBar*>(parentWidget());
if (!parent) {
return;
}
QStyle *style = parent->style();
QStyleOptionToolBar opt;
parent->initStyleOption(&opt);
opt.features = QStyleOptionToolBar::Movable;
setFixedWidth(style->subElementRect(QStyle::SE_ToolBarHandle, &opt, parent).width() + 4);
}
// -----------------------------------------------------------
ToolBarManager* ToolBarManager::_instance = nullptr; // NOLINT
ToolBarManager* ToolBarManager::getInstance()
@@ -327,15 +539,24 @@ ToolBarArea ToolBarManager::toolBarArea(QWidget *widget) const
}
}
for (auto &areaWidget : { statusBarAreaWidget, menuBarLeftAreaWidget, menuBarRightAreaWidget }) {
if (areaWidget->indexOf(widget) >= 0) {
return areaWidget->area();
}
if (auto areaWidget = toolBarAreaWidget(widget)) {
return areaWidget->area();
}
return ToolBarArea::NoToolBarArea;
}
ToolBarAreaWidget* ToolBarManager::toolBarAreaWidget(QWidget* widget) const
{
for (auto &areaWidget : { statusBarAreaWidget, menuBarLeftAreaWidget, menuBarRightAreaWidget }) {
if (areaWidget->indexOf(widget) >= 0) {
return areaWidget;
}
}
return nullptr;
}
namespace {
QPointer<QWidget> createActionWidget()
{
@@ -427,23 +648,29 @@ void ToolBarManager::setup(ToolBarItem* toolBarItems)
int top_width = 0;
bool nameAsToolTip = App::GetApplication().GetUserParameter().GetGroup("BaseApp")
->GetGroup("Preferences")->GetGroup("MainWindow")->GetBool("ToolBarNameAsToolTip",true);
->GetGroup("Preferences")
->GetGroup("MainWindow")
->GetBool("ToolBarNameAsToolTip", true);
QList<ToolBarItem*> items = toolBarItems->getItems();
QList<QToolBar*> toolbars = toolBars();
QList<ToolBar*> toolbars = toolBars();
for (ToolBarItem* it : items) {
// search for the toolbar
QString name = QString::fromUtf8(it->command().c_str());
this->toolbarNames << name;
QToolBar* toolbar = findToolBar(toolbars, name);
ToolBar* toolbar = findToolBar(toolbars, name);
std::string toolbarName = it->command();
bool toolbar_added = false;
if (!toolbar) {
toolbar = getMainWindow()->addToolBar(
QApplication::translate("Workbench",
toolbarName.c_str())); // i18n
toolbar = new ToolBar(getMainWindow());
toolbar->setWindowTitle(QApplication::translate("Workbench", toolbarName.c_str()));
toolbar->setObjectName(name);
if (nameAsToolTip){
getMainWindow()->addToolBar(toolbar);
if (nameAsToolTip) {
auto tooltip = QChar::fromLatin1('[')
+ QApplication::translate("Workbench", toolbarName.c_str())
+ QChar::fromLatin1(']');
@@ -470,7 +697,8 @@ void ToolBarManager::setup(ToolBarItem* toolBarItems)
// Enable automatic handling of visibility via, for example, (contextual) menu
toolbar->toggleViewAction()->setVisible(true);
}
else { // ToolBarItem::DefaultVisibility::Unavailable
else {
// ToolBarItem::DefaultVisibility::Unavailable
// Prevent that the action to show/hide a toolbar appears on the (contextual) menus.
// This is also managed by the client code for a toolbar with custom policy
toolbar->toggleViewAction()->setVisible(false);
@@ -595,9 +823,10 @@ void ToolBarManager::saveState() const
return value == ToolBarItem::DefaultVisibility::Unavailable;
};
QList<QToolBar*> toolbars = toolBars();
QList<ToolBar*> toolbars = toolBars();
for (const QString& it : toolbarNames) {
QToolBar* toolbar = findToolBar(toolbars, it);
ToolBar* toolbar = findToolBar(toolbars, it);
if (toolbar) {
if (ignoreSave(toolbar->toggleViewAction())) {
continue;
@@ -614,7 +843,7 @@ void ToolBarManager::restoreState() const
std::map<int, QToolBar*> sbToolBars;
std::map<int, QToolBar*> mbRightToolBars;
std::map<int, QToolBar*> mbLeftToolBars;
QList<QToolBar*> toolbars = toolBars();
QList<ToolBar*> toolbars = toolBars();
for (const QString& it : toolbarNames) {
QToolBar* toolbar = findToolBar(toolbars, it);
if (toolbar) {
@@ -782,67 +1011,9 @@ bool ToolBarManager::addToolBarToArea(QObject *source, QMouseEvent *ev)
return false;
}
void ToolBarManager::populateUndockMenu(QMenu *menu, ToolBarAreaWidget *area)
{
menu->setTitle(tr("Undock toolbars"));
auto tooltip = QObject::tr("Undock from toolbar area");
auto addMenuUndockItem = [&](QToolBar *toolbar, int, ToolBarAreaWidget *area) {
auto toggleViewAction = toolbar->toggleViewAction();
auto undockAction = new QAction(menu);
undockAction->setText(toggleViewAction->text());
undockAction->setToolTip(tooltip);
menu->addAction(undockAction);
QObject::connect(undockAction, &QAction::triggered, [area, toolbar]() {
if (toolbar->parentWidget() == getMainWindow()) {
return;
}
auto pos = toolbar->mapToGlobal(QPoint(0, 0));
auto yOffset = toolbar->height();
// if widget is on the bottom move it up instead
if (area->area() == Gui::ToolBarArea::StatusBarToolBarArea) {
yOffset *= -1;
}
{
// Block signals caused by manually floating the widget
QSignalBlocker blocker(toolbar);
area->removeWidget(toolbar);
getMainWindow()->addToolBar(toolbar);
// this will make toolbar floating, there is no better way to do that.
toolbar->setWindowFlags(Qt::Tool
| Qt::FramelessWindowHint
| Qt::X11BypassWindowManagerHint);
toolbar->move(pos.x(), pos.y() + yOffset);
toolbar->adjustSize();
toolbar->setVisible(true);
}
// but don't block actual information about widget being floated
Q_EMIT toolbar->topLevelChanged(true);
});
};
if (area) {
area->foreachToolBar(addMenuUndockItem);
}
else {
statusBarAreaWidget->foreachToolBar(addMenuUndockItem);
menuBarLeftAreaWidget->foreachToolBar(addMenuUndockItem);
menuBarRightAreaWidget->foreachToolBar(addMenuUndockItem);
}
}
bool ToolBarManager::showContextMenu(QObject *source)
{
QMenu menu;
QMenu menuUndock;
QLayout* layout = nullptr;
ToolBarAreaWidget* area = nullptr;
if (getMainWindow()->statusBar() == source) {
@@ -872,12 +1043,7 @@ bool ToolBarManager::showContextMenu(QObject *source)
}
area->foreachToolBar(addMenuVisibleItem);
populateUndockMenu(&menuUndock, area);
if (!menuUndock.actions().empty()) {
menu.addSeparator();
menu.addMenu(&menuUndock);
}
menu.exec(QCursor::pos());
return true;
}
@@ -990,8 +1156,8 @@ bool ToolBarManager::eventFilter(QObject *source, QEvent *ev)
void ToolBarManager::retranslate() const
{
QList<QToolBar*> toolbars = toolBars();
for (QToolBar* it : toolbars) {
QList<ToolBar*> toolbars = toolBars();
for (ToolBar* it : toolbars) {
QByteArray toolbarName = it->objectName().toUtf8();
it->setWindowTitle(QApplication::translate("Workbench", (const char*)toolbarName));
}
@@ -1009,16 +1175,17 @@ void Gui::ToolBarManager::setToolBarsLocked(bool locked) const
setMovable(!locked);
}
void Gui::ToolBarManager::setMovable(bool moveable) const
void Gui::ToolBarManager::setMovable(bool movable) const
{
for (auto& tb : toolBars()) {
tb->setMovable(moveable);
tb->setMovable(movable);
tb->updateCustomGripVisibility();
}
}
QToolBar* ToolBarManager::findToolBar(const QList<QToolBar*>& toolbars, const QString& item) const
ToolBar* ToolBarManager::findToolBar(const QList<ToolBar*>& toolbars, const QString& item) const
{
for (QToolBar* it : toolbars) {
for (ToolBar* it : toolbars) {
if (it->objectName() == item) {
return it;
}
@@ -1038,12 +1205,14 @@ QAction* ToolBarManager::findAction(const QList<QAction*>& acts, const QString&
return nullptr; // no item with the user data found
}
QList<QToolBar*> ToolBarManager::toolBars() const
QList<ToolBar*> ToolBarManager::toolBars() const
{
auto mw = getMainWindow();
QList<QToolBar*> tb;
QList<QToolBar*> bars = getMainWindow()->findChildren<QToolBar*>();
for (QToolBar* it : bars) {
QList<ToolBar*> tb;
QList<ToolBar*> bars = getMainWindow()->findChildren<ToolBar*>();
for (ToolBar* it : bars) {
auto parent = it->parentWidget();
if (parent == mw
|| parent == mw->statusBar()

View File

@@ -28,6 +28,7 @@
#include <boost_signals2.hpp>
#include <QStringList>
#include <QPointer>
#include <QTimer>
#include <QToolBar>
#include <QPointer>
@@ -88,6 +89,48 @@ private:
QList<ToolBarItem*> _items;
};
class ToolBarGrip: public QWidget
{
Q_OBJECT
public:
explicit ToolBarGrip(QToolBar *);
void attach();
void detach();
bool isAttached() const;
protected:
void paintEvent(QPaintEvent*);
void mouseMoveEvent(QMouseEvent *);
void mousePressEvent(QMouseEvent *);
void mouseReleaseEvent(QMouseEvent *);
void updateSize();
private:
QPointer<QAction> _action = nullptr;
};
/**
* QToolBar from Qt lacks few abilities like ability to float toolbar from code.
* This class allows us to provide custom behaviors for toolbars if needed.
*/
class ToolBar: public QToolBar
{
Q_OBJECT
public:
ToolBar(QWidget* parent);
virtual ~ToolBar() = default;
void undock();
void updateCustomGripVisibility();
friend class ToolBarGrip;
};
/**
* The ToolBarManager class is responsible for the creation of toolbars and appending them
* to the main window.
@@ -122,12 +165,11 @@ public:
void setState(const QList<QString>& names, State state);
void setState(const QString& name, State state);
int toolBarIconSize(QWidget *widget=nullptr) const;
int toolBarIconSize(QWidget *widget = nullptr) const;
void setupToolBarIconSize();
void populateUndockMenu(QMenu *menu, ToolBarAreaWidget *area = nullptr);
ToolBarArea toolBarArea(QWidget* toolBar) const;
ToolBarAreaWidget* toolBarAreaWidget(QWidget* toolBar) const;
protected:
void setup(ToolBarItem*, QToolBar*) const;
@@ -145,8 +187,8 @@ protected:
bool eventFilter(QObject *source, QEvent *ev) override;
/** Returns a list of all currently existing toolbars. */
QList<QToolBar*> toolBars() const;
QToolBar* findToolBar(const QList<QToolBar*>&, const QString&) const;
QList<ToolBar*> toolBars() const;
ToolBar* findToolBar(const QList<ToolBar*>&, const QString&) const;
QAction* findAction(const QList<QAction*>&, const QString&) const;
ToolBarManager();
~ToolBarManager() override;