diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index b3d973e29..69e987e9b 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -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/titleblockcell.cpp ${QET_DIR}/sources/titleblockcell.h ${QET_DIR}/sources/titleblockproperties.cpp diff --git a/sources/diagramgestureoverlay.cpp b/sources/diagramgestureoverlay.cpp new file mode 100644 index 000000000..03d2606d1 --- /dev/null +++ b/sources/diagramgestureoverlay.cpp @@ -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 . +*/ +#include "diagramgestureoverlay.h" + +#include +#include +#include +#include +#include + +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 ¢er, + const QList &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); + } +} diff --git a/sources/diagramgestureoverlay.h b/sources/diagramgestureoverlay.h new file mode 100644 index 000000000..5fca8dfc2 --- /dev/null +++ b/sources/diagramgestureoverlay.h @@ -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 . +*/ +#ifndef DIAGRAMGESTUREOVERLAY_H +#define DIAGRAMGESTUREOVERLAY_H + +#include +#include + +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 ¢er, const QList &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 m_actions; + int m_active = -1; +}; + +#endif // DIAGRAMGESTUREOVERLAY_H diff --git a/sources/diagramview.cpp b/sources/diagramview.cpp index e6be0675a..5e2ef56fa 100644 --- a/sources/diagramview.cpp +++ b/sources/diagramview.cpp @@ -38,6 +38,7 @@ #include "undocommand/addgraphicsobjectcommand.h" #include "diagram.h" #include "diagramcontexttoolbar.h" +#include "diagramgestureoverlay.h" #include "shortcutbarsettings.h" #include "shortcutmanager.h" #include "ElementsCollection/xmlelementcollection.h" @@ -108,6 +109,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(); @@ -635,6 +637,36 @@ 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 tool runs -- + //a right click cancels or finishes it -- and while a text is edited. + m_swallow_native_menu = false; + if (e->button() == Qt::RightButton + && DiagramGestureOverlay::isEnabled() + && !m_diagram->eventInterfaceIsRunning() + && !m_diagram->focusItem()) + { + m_gesture_tracking = true; + m_swallow_native_menu = true; + m_gesture_origin = e->position().toPoint(); + m_context_toolbar->hide(); + + //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) { @@ -686,6 +718,20 @@ 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()) { + m_gesture_overlay->showAt(m_gesture_origin, selectionCommands()); + } + if (m_gesture_overlay->isVisible()) { + m_gesture_overlay->setPointer(pos); + } + e->accept(); + return; + } + // Drag the view if (e->buttons() == Qt::MiddleButton) { @@ -747,6 +793,34 @@ void DiagramView::mouseReleaseEvent(QMouseEvent *e) { if (m_event_interface && m_event_interface->mouseReleaseEvent(e)) return; + if (m_gesture_tracking && e->button() == Qt::RightButton) + { + 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; + } + // Stop drag view if (e->button() == Qt::MiddleButton) { @@ -804,6 +878,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 DiagramView::selectionCommands() const +{ + QList 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 @@ -823,14 +918,7 @@ void DiagramView::showContextToolbar(const QPoint &viewport_pos) return; } - QList 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()); } /** @@ -1599,6 +1687,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 diff --git a/sources/diagramview.h b/sources/diagramview.h index a0c311cd1..18e4d3ddc 100644 --- a/sources/diagramview.h +++ b/sources/diagramview.h @@ -28,6 +28,7 @@ class CellRuler; class Conductor; class Diagram; class DiagramContextToolbar; +class DiagramGestureOverlay; class QETDiagramEditor; class DVEventInterface; class QInputEvent; @@ -59,6 +60,14 @@ 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; + 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, @@ -131,6 +140,7 @@ class DiagramView : public PaletteGraphicsView void updateCellRulers(); void placeCellRulers(); void showContextToolbar(const QPoint &viewport_pos); + QList selectionCommands() const; /// Lowest and highest allowed value of the view transform scale (m11). /// Prevents wheel-zoom from driving the transform to overflow, which diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp index bd58a2e2a..1340b9b66 100644 --- a/sources/ui/configpage/generalconfigurationpage.cpp +++ b/sources/ui/configpage/generalconfigurationpage.cpp @@ -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()); diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui index de4b7445c..177314d93 100644 --- a/sources/ui/configpage/generalconfigurationpage.ui +++ b/sources/ui/configpage/generalconfigurationpage.ui @@ -97,6 +97,16 @@ + + + + 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. + + + Gestes de la souris avec le bouton droit + + +