Merge pull request #1057 from ispyisail/feature/mouse-gestures

Add right-drag mouse gestures to run commands
This commit is contained in:
Laurent Trinques
2026-09-27 09:02:44 +02:00
committed by GitHub
7 changed files with 402 additions and 8 deletions
+2
View File
@@ -289,6 +289,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/shortcutbarsettings.h
${QET_DIR}/sources/diagramcontexttoolbar.cpp
${QET_DIR}/sources/diagramcontexttoolbar.h
${QET_DIR}/sources/diagramgestureoverlay.cpp
${QET_DIR}/sources/diagramgestureoverlay.h
${QET_DIR}/sources/commandsearchpopup.cpp
${QET_DIR}/sources/commandsearchpopup.h
${QET_DIR}/sources/titleblockcell.cpp
+185
View File
@@ -0,0 +1,185 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "diagramgestureoverlay.h"
#include <QAction>
#include <QPainter>
#include <QPainterPath>
#include <QSettings>
#include <QtMath>
namespace {
const int outer_radius = 84;
/// Releasing closer than this to the centre cancels
const int inner_radius = 22;
const int icon_radius = 56;
const int icon_size = 22;
/// Wider than the ring, so a long command name under it is not cut
const int overlay_width = 280;
}
/**
@brief DiagramGestureOverlay::DiagramGestureOverlay
@param viewport : the view's viewport, which it is drawn on. It lets the
mouse through, so the view keeps receiving the drag.
*/
DiagramGestureOverlay::DiagramGestureOverlay(QWidget *viewport) :
QWidget(viewport)
{
setAttribute(Qt::WA_TransparentForMouseEvents);
setAttribute(Qt::WA_TranslucentBackground);
setFixedSize(overlay_width, 2 * outer_radius + 2 + 24);
hide();
}
/**
@return whether right-drag gestures are on (a preference)
*/
bool DiagramGestureOverlay::isEnabled()
{
return QSettings().value(QStringLiteral("diagrameditor/mouse_gestures"),
true).toBool();
}
/**
@brief DiagramGestureOverlay::showAt
Show the ring centred on @a center with @a actions, at most eight,
clockwise from the top.
*/
void DiagramGestureOverlay::showAt(const QPoint &center,
const QList<QAction *> &actions)
{
m_center = center;
m_actions = actions.mid(0, sectors);
m_active = -1;
move(center - QPoint(overlay_width / 2, outer_radius + 1));
show();
raise();
}
/**
@brief DiagramGestureOverlay::sectorAt
@return the sector in the direction of @a viewport_pos from the centre,
0 at the top then clockwise, or -1 within the cancel radius
*/
int DiagramGestureOverlay::sectorAt(const QPoint &viewport_pos) const
{
const QPoint d = viewport_pos - m_center;
if (qHypot(d.x(), d.y()) < inner_radius) {
return -1;
}
//Angle clockwise from the top, in degrees
qreal angle = qRadiansToDegrees(qAtan2(d.x(), -d.y()));
if (angle < 0) {
angle += 360;
}
return int(qRound(angle / (360.0 / sectors))) % sectors;
}
/**
@return the command in the direction of @a viewport_pos, or nullptr when
there is none there or it is disabled
*/
QAction *DiagramGestureOverlay::actionAt(const QPoint &viewport_pos) const
{
const int sector = sectorAt(viewport_pos);
if (sector < 0 || sector >= m_actions.count()) {
return nullptr;
}
QAction *action = m_actions.at(sector);
return action->isEnabled() ? action : nullptr;
}
void DiagramGestureOverlay::setPointer(const QPoint &viewport_pos)
{
const int sector = sectorAt(viewport_pos);
if (sector != m_active) {
m_active = sector;
update();
}
}
void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QPointF c(overlay_width / 2, outer_radius + 1);
const qreal step = 360.0 / sectors;
QColor base = palette().color(QPalette::Window);
base.setAlpha(225);
painter.setPen(palette().color(QPalette::Mid));
painter.setBrush(base);
painter.drawEllipse(c, outer_radius, outer_radius);
//Highlight the sector the mouse points at, if it holds a command
if (m_active >= 0 && m_active < m_actions.count()
&& m_actions.at(m_active)->isEnabled())
{
QPainterPath wedge;
wedge.moveTo(c);
//Qt angles run counter-clockwise from 3 o'clock
const qreal start = 90 - m_active * step - step / 2;
wedge.arcTo(QRectF(c.x() - outer_radius, c.y() - outer_radius,
2 * outer_radius, 2 * outer_radius), start, step);
wedge.closeSubpath();
painter.setPen(Qt::NoPen);
painter.setBrush(palette().color(QPalette::Highlight));
painter.drawPath(wedge);
}
painter.setPen(palette().color(QPalette::Mid));
painter.setBrush(palette().color(QPalette::Window));
painter.drawEllipse(c, inner_radius, inner_radius);
for (int i = 0 ; i < m_actions.count() ; ++i)
{
QAction *action = m_actions.at(i);
const qreal a = qDegreesToRadians(i * step);
const QPointF p(c.x() + icon_radius * qSin(a), c.y() - icon_radius * qCos(a));
const QRect r(int(p.x()) - icon_size / 2, int(p.y()) - icon_size / 2,
icon_size, icon_size);
const QIcon::Mode mode = action->isEnabled() ? QIcon::Normal : QIcon::Disabled;
if (!action->icon().isNull()) {
action->icon().paint(&painter, r, Qt::AlignCenter, mode);
} else {
painter.setPen(palette().color(action->isEnabled() ? QPalette::Active : QPalette::Disabled,
QPalette::WindowText));
painter.drawText(r.adjusted(-12, 0, 12, 0), Qt::AlignCenter,
action->text().remove(QLatin1Char('&')).left(3));
}
}
//Name of the highlighted command, under the ring
if (m_active >= 0 && m_active < m_actions.count())
{
const QString name = m_actions.at(m_active)->text().remove(QLatin1Char('&'));
const QRect label(0, 2 * outer_radius + 4, width(), 20);
QFont f = font();
f.setBold(true);
painter.setFont(f);
const QRect text = painter.fontMetrics().boundingRect(label, Qt::AlignCenter, name)
.adjusted(-6, -2, 6, 2);
painter.setPen(Qt::NoPen);
painter.setBrush(base);
painter.drawRoundedRect(text, 4, 4);
painter.setPen(palette().color(QPalette::WindowText));
painter.drawText(label, Qt::AlignCenter, name);
}
}
+60
View File
@@ -0,0 +1,60 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech 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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef DIAGRAMGESTUREOVERLAY_H
#define DIAGRAMGESTUREOVERLAY_H
#include <QList>
#include <QWidget>
class QAction;
/**
@brief The ring shown while right-dragging on a folio: up to eight
commands around the point where the button went down, the one in the
mouse's direction highlighted. Releasing runs it, releasing near the
centre cancels -- SolidWorks' mouse gestures.
Commands are placed clockwise from the top. The ring only draws; the
view tracks the mouse and asks sectorAt() on release.
*/
class DiagramGestureOverlay : public QWidget
{
Q_OBJECT
public:
explicit DiagramGestureOverlay(QWidget *viewport);
void showAt(const QPoint &center, const QList<QAction *> &actions);
void setPointer(const QPoint &viewport_pos);
int sectorAt(const QPoint &viewport_pos) const;
QAction *actionAt(const QPoint &viewport_pos) const;
static bool isEnabled();
static const int sectors = 8;
protected:
void paintEvent(QPaintEvent *event) override;
private:
QPoint m_center;
QList<QAction *> m_actions;
int m_active = -1;
};
#endif // DIAGRAMGESTUREOVERLAY_H
+131 -8
View File
@@ -39,6 +39,7 @@
#include "undocommand/addgraphicsobjectcommand.h"
#include "diagram.h"
#include "diagramcontexttoolbar.h"
#include "diagramgestureoverlay.h"
#include "shortcutbarsettings.h"
#include "shortcutmanager.h"
#include "ElementsCollection/xmlelementcollection.h"
@@ -113,6 +114,7 @@ DiagramView::DiagramView(Diagram *diagram, QWidget *parent) :
}
m_context_toolbar = new DiagramContextToolbar(viewport());
m_gesture_overlay = new DiagramGestureOverlay(viewport());
connect(m_diagram, &QGraphicsScene::selectionChanged, this, [this]() {
if (m_diagram->selectedItems().isEmpty()) {
m_context_toolbar->hide();
@@ -641,6 +643,46 @@ void DiagramView::mousePressEvent(QMouseEvent *e)
m_press_pos = e->position().toPoint();
}
//Right button: a click opens the context menu on release, a drag is
//a gesture (DiagramGestureOverlay). Left alone while a text is edited.
m_swallow_native_menu = false;
m_gesture_over_tool = false;
if (e->button() == Qt::RightButton
&& DiagramGestureOverlay::isEnabled()
&& !m_diagram->focusItem())
{
m_gesture_tracking = true;
m_gesture_origin = e->position().toPoint();
m_context_toolbar->hide();
//A tool is running, often one a gesture just started. A right
//click still goes to it -- it cancels or finishes the tool -- so
//the press carries on to the scene. A drag ends the tool and
//shows the ring (see mouseMoveEvent).
if (m_diagram->eventInterfaceIsRunning()) {
m_gesture_over_tool = true;
}
else
{
m_swallow_native_menu = true;
//Select what is under the mouse, as the context menu does, so
//a gesture acts on it
if (QGraphicsItem *item = m_diagram->itemAt(mapToScene(m_gesture_origin), transform())) {
if (!item->isSelected()) {
m_diagram->clearSelection();
//Clearing the selection can delete handler items, so
//look the item up again (see contextMenuEvent)
if (QGraphicsItem *again = m_diagram->itemAt(mapToScene(m_gesture_origin), transform())) {
again->setSelected(true);
}
}
}
e->accept();
return;
}
}
//Start drag view when hold the middle button
if (e->button() == Qt::MiddleButton)
{
@@ -692,6 +734,31 @@ void DiagramView::mouseMoveEvent(QMouseEvent *e)
setToolTip(tr("X: %1 Y: %2").arg(e->pos().x()).arg(e->pos().y()));
if (m_event_interface && m_event_interface->mouseMoveEvent(e)) return;
if (m_gesture_tracking)
{
const QPoint pos = e->position().toPoint();
if (!m_gesture_overlay->isVisible()
&& (pos - m_gesture_origin).manhattanLength() > 2 * QApplication::startDragDistance()) {
if (m_gesture_over_tool) {
//The drag is a gesture: end the tool, and ignore the
//platform's right-click menu like any other gesture
m_diagram->clearEventInterface();
m_swallow_native_menu = true;
}
m_gesture_overlay->showAt(m_gesture_origin, selectionCommands());
}
if (m_gesture_overlay->isVisible()) {
m_gesture_overlay->setPointer(pos);
e->accept();
return;
}
//Not a drag yet: a running tool keeps following the mouse
if (!m_gesture_over_tool) {
e->accept();
return;
}
}
// Drag the view
if (e->buttons() == Qt::MiddleButton)
{
@@ -753,6 +820,40 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
{
if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return;
//A plain right click on a running tool falls through: the tool
//handles it, as it always has
if (m_gesture_tracking && e->button() == Qt::RightButton
&& !(m_gesture_over_tool && !m_gesture_overlay->isVisible()))
{
m_gesture_tracking = false;
const QPoint pos = e->position().toPoint();
if (m_gesture_overlay->isVisible())
{
//A gesture: run the command it points at, if any
QAction *action = m_gesture_overlay->actionAt(pos);
m_gesture_overlay->hide();
if (action) {
action->trigger();
}
}
else
{
//A plain right click: the context menu, opened here on
//release on every platform
QContextMenuEvent menu_event(QContextMenuEvent::Mouse, pos,
e->globalPosition().toPoint(),
e->modifiers());
m_menu_from_gesture = true;
contextMenuEvent(&menu_event);
m_menu_from_gesture = false;
}
e->accept();
return;
}
if (e->button() == Qt::RightButton) {
m_gesture_tracking = false;
}
// Stop drag view
if (e->button() == Qt::MiddleButton)
{
@@ -810,6 +911,27 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e)
}
}
/**
@brief DiagramView::selectionCommands
@return the shortcut bar's commands for the current selection, as this
window's actions
*/
QList<QAction *> DiagramView::selectionCommands() const
{
QList<QAction *> actions;
QETDiagramEditor *qde = diagramEditor();
if (!qde) {
return actions;
}
const auto context = ShortcutBarSettings::contextFor(m_diagram->selectedItems());
for (const QString &id : ShortcutBarSettings::ids(context)) {
if (QAction *action = ShortcutManager::instance().action(id, qde)) {
actions << action;
}
}
return actions;
}
/**
@brief DiagramView::showContextToolbar
After a click that leaves something selected, show the shortcut bar's
@@ -829,14 +951,7 @@ void DiagramView::showContextToolbar(const QPoint &viewport_pos)
return;
}
QList<QAction *> actions;
const auto context = ShortcutBarSettings::contextFor(selection);
for (const QString &id : ShortcutBarSettings::ids(context)) {
if (QAction *action = ShortcutManager::instance().action(id, qde)) {
actions << action;
}
}
m_context_toolbar->showAt(viewport_pos, actions);
m_context_toolbar->showAt(viewport_pos, selectionCommands());
}
/**
@@ -1689,6 +1804,14 @@ void DiagramView::contextMenuEvent(QContextMenuEvent *e)
//right-click gets.
const bool from_keyboard = e->reason() == QContextMenuEvent::Keyboard;
//With gestures on, a right press is tracked by mousePressEvent and
//the menu opened on release; the platform's own event (sent on press
//on X11, on release on Windows) would open it a second time.
if (!from_keyboard && m_swallow_native_menu && !m_menu_from_gesture) {
e->accept();
return;
}
if (from_keyboard)
{
//Aim at the selection when there is one, so the menu appears
+12
View File
@@ -28,6 +28,7 @@ class CellRuler;
class Conductor;
class Diagram;
class DiagramContextToolbar;
class DiagramGestureOverlay;
class QETDiagramEditor;
class DVEventInterface;
class QInputEvent;
@@ -61,6 +62,16 @@ class DiagramView : public PaletteGraphicsView
QPoint m_paste_here_pos;
QPoint m_press_pos;
DiagramContextToolbar *m_context_toolbar = nullptr;
/// Right-drag gestures: tracking since the right button went down
bool m_gesture_tracking = false;
/// The platform's own right-click menu event is to be ignored:
/// the view opens the menu itself on release
bool m_swallow_native_menu = false;
bool m_menu_from_gesture = false;
/// The right press went to a running tool; a drag ends the tool
bool m_gesture_over_tool = false;
QPoint m_gesture_origin;
DiagramGestureOverlay *m_gesture_overlay = nullptr;
QPoint m_last_mouse_pos = QPoint(-1, -1);
QPointF m_drag_last_pos;
bool m_fresh_focus_in,
@@ -137,6 +148,7 @@ class DiagramView : public PaletteGraphicsView
void updateCellRulers();
void placeCellRulers();
void showContextToolbar(const QPoint &viewport_pos);
QList<QAction *> selectionCommands() const;
/// Lowest and highest allowed value of the view transform scale (m11).
/// Prevents wheel-zoom from driving the transform to overflow, which
@@ -71,6 +71,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
//is the unchecked state -- a preference reads better as an opt-out.
ui->m_collection_dblclick_edits->setChecked(!settings.value("elementscollection/double-click-inserts", true).toBool());
ui->m_context_toolbar_cb->setChecked(settings.value("diagrameditor/context_toolbar", true).toBool());
ui->m_mouse_gestures_cb->setChecked(settings.value("diagrameditor/mouse_gestures", true).toBool());
ui->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt());
ui->DiagramEditor_yGrid_sb->setValue(settings.value("diagrameditor/Ygrid", 10).toInt());
for (const qreal divisor : TextGrid::divisors)
@@ -300,6 +301,7 @@ void GeneralConfigurationPage::applyConf()
settings.setValue("diagrameditor/guides_display_startup", ui->guides_startup_cb->isChecked());
settings.setValue("elementscollection/double-click-inserts", !ui->m_collection_dblclick_edits->isChecked());
settings.setValue("diagrameditor/context_toolbar", ui->m_context_toolbar_cb->isChecked());
settings.setValue("diagrameditor/mouse_gestures", ui->m_mouse_gestures_cb->isChecked());
//Grid step and key navigation
settings.setValue("diagrameditor/Xgrid", ui->DiagramEditor_xGrid_sb->value());
settings.setValue("diagrameditor/Ygrid", ui->DiagramEditor_yGrid_sb->value());
@@ -97,6 +97,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_mouse_gestures_cb">
<property name="toolTip">
<string>Maintenir le bouton droit et glisser dans une direction lance une commande de la barre de raccourcis. Un simple clic droit ouvre toujours le menu contextuel, au relâchement du bouton.</string>
</property>
<property name="text">
<string>Gestes de la souris avec le bouton droit</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="guides_startup_cb">
<property name="text">