Merge pull request #14455 from kadet1090/toolbar-dragging

Gui: Add dragging support for custom ToolBarAreas
This commit is contained in:
Chris Hennes
2024-07-01 10:49:27 -05:00
committed by GitHub
9 changed files with 350 additions and 113 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>
@@ -41,6 +45,7 @@
#include "Command.h"
#include "MainWindow.h"
#include "OverlayWidgets.h"
#include "WidgetFactory.h"
using namespace Gui;
@@ -162,6 +167,219 @@ QList<ToolBarItem*> ToolBarItem::getItems() const
// -----------------------------------------------------------
ToolBar::ToolBar()
: QToolBar()
{
setupConnections();
}
ToolBar::ToolBar(QWidget* parent)
: QToolBar(parent)
{
setupConnections();
}
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;
}
}
void Gui::ToolBar::setupConnections()
{
connect(this, &QToolBar::topLevelChanged, this, &ToolBar::updateCustomGripVisibility);
connect(this, &QToolBar::movableChanged, this, &ToolBar::updateCustomGripVisibility);
}
// -----------------------------------------------------------
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()
@@ -189,6 +407,8 @@ ToolBarManager::ToolBarManager()
setupConnection();
setupTimer();
setupMenuBarTimer();
setupWidgetProducers();
}
ToolBarManager::~ToolBarManager() = default;
@@ -304,6 +524,11 @@ void ToolBarManager::setupMenuBarTimer()
});
}
void Gui::ToolBarManager::setupWidgetProducers()
{
new WidgetProducer<Gui::ToolBar>;
}
ToolBarArea ToolBarManager::toolBarArea(QWidget *widget) const
{
if (auto toolBar = qobject_cast<QToolBar*>(widget)) {
@@ -327,15 +552,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 +661,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 +710,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 +836,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 +856,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 +1024,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 +1056,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 +1169,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 +1188,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 +1218,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,53 @@ 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 GuiExport ToolBar: public QToolBar
{
Q_OBJECT
friend class ToolBarGrip;
public:
ToolBar();
explicit ToolBar(QWidget* parent);
virtual ~ToolBar() = default;
void undock();
void updateCustomGripVisibility();
protected:
void setupConnections();
};
/**
* The ToolBarManager class is responsible for the creation of toolbars and appending them
* to the main window.
@@ -121,13 +169,12 @@ 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 +192,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;
@@ -160,6 +207,8 @@ private:
void setupSizeTimer();
void setupResizeTimer();
void setupMenuBarTimer();
void setupWidgetProducers();
void addToMenu(QLayout* layout, QWidget* area, QMenu* menu);
QLayout* findLayoutOfObject(QObject* source, QWidget* area) const;
ToolBarAreaWidget* findToolBarAreaWidget() const;

View File

@@ -59,6 +59,7 @@
#include "InputField.h"
#include "QuantitySpinBox.h"
#include "PrefWidgets.h"
#include "ToolBarManager.h"
using namespace Gui;
using namespace Gui::Dialog;

View File

@@ -117,7 +117,7 @@ def setStatusIcons(show=True):
if statuswidget:
statuswidget.show()
else:
statuswidget = QtGui.QToolBar()
statuswidget = FreeCADGui.UiLoader().createWidget("Gui::ToolBar")
statuswidget.setObjectName("BIMStatusWidget")
s = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/General").GetInt("ToolbarIconSize", 24)
statuswidget.setIconSize(QtCore.QSize(s,s))

View File

@@ -106,7 +106,7 @@ class BIM_IfcExplorer:
self.dialog.setObjectName("IfcExplorer")
self.dialog.setWindowTitle(translate("BIM", "Ifc Explorer"))
self.dialog.resize(720, 540)
toolbar = QtGui.QToolBar()
toolbar = FreeCADGui.UiLoader().createWidget("Gui::ToolBar")
layout = QtGui.QVBoxLayout(self.dialog)
layout.addWidget(toolbar)

View File

@@ -81,7 +81,7 @@ class BIM_Views:
size = FreeCAD.ParamGet(
"User parameter:BaseApp/Preferences/General"
).GetInt("ToolbarIconSize", 24)
toolbar = QtGui.QToolBar()
toolbar = FreeCADGui.UiLoader().createWidget("Gui::ToolBar")
toolbar.setIconSize(QtCore.QSize(size, size))
dialog.horizontalLayout.addWidget(toolbar)
for button in [

View File

@@ -205,7 +205,7 @@ class DraftToolBar:
# add only a dummy widget, since widgets are created on demand
self.baseWidget = DraftBaseWidget()
self.tray = QtWidgets.QToolBar(None)
self.tray = FreeCADGui.UiLoader().createWidget("Gui::ToolBar")
self.tray.setObjectName("Draft tray")
self.tray.setWindowTitle("Draft tray")
self.toptray = self.tray