Merge pull request #1364 from ispyisail/feature/customise-gesture-list

Add a mouse gesture page: own commands, 4 or 8 directions
This commit is contained in:
ispyisail
2026-10-07 20:56:50 +13:00
committed by GitHub
12 changed files with 1050 additions and 15 deletions
+4
View File
@@ -307,6 +307,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/diagramcontexttoolbar.h
${QET_DIR}/sources/diagramgestureoverlay.cpp
${QET_DIR}/sources/diagramgestureoverlay.h
${QET_DIR}/sources/gesturesettings.cpp
${QET_DIR}/sources/gesturesettings.h
${QET_DIR}/sources/commandsearchpopup.cpp
${QET_DIR}/sources/commandsearchpopup.h
${QET_DIR}/sources/titleblockcell.cpp
@@ -860,6 +862,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/configpage/shortcutbarconfigpage.h
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.cpp
${QET_DIR}/sources/ui/configpage/toolbarsconfigpage.h
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.cpp
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.h
${QET_DIR}/sources/undocommand/addelementtextcommand.cpp
${QET_DIR}/sources/undocommand/addelementtextcommand.h
+14 -9
View File
@@ -58,14 +58,16 @@ bool DiagramGestureOverlay::isEnabled()
/**
@brief DiagramGestureOverlay::showAt
Show the ring centred on @a center with @a actions, at most eight,
clockwise from the top.
Show the ring centred on @a center with @a sectors directions (4 or 8)
and @a actions, one per direction clockwise from the top.
*/
void DiagramGestureOverlay::showAt(const QPoint &center,
const QList<QAction *> &actions)
const QList<QAction *> &actions,
int sectors)
{
m_center = center;
m_actions = actions.mid(0, sectors);
m_sectors = sectors == 4 ? 4 : 8;
m_actions = actions.mid(0, m_sectors);
m_active = -1;
move(center - QPoint(overlay_width / 2, outer_radius + 1));
show();
@@ -88,7 +90,7 @@ int DiagramGestureOverlay::sectorAt(const QPoint &viewport_pos) const
if (angle < 0) {
angle += 360;
}
return int(qRound(angle / (360.0 / sectors))) % sectors;
return int(qRound(angle / (360.0 / m_sectors))) % m_sectors;
}
/**
@@ -102,7 +104,7 @@ QAction *DiagramGestureOverlay::actionAt(const QPoint &viewport_pos) const
return nullptr;
}
QAction *action = m_actions.at(sector);
return action->isEnabled() ? action : nullptr;
return action && action->isEnabled() ? action : nullptr;
}
void DiagramGestureOverlay::setPointer(const QPoint &viewport_pos)
@@ -120,7 +122,7 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QPointF c(overlay_width / 2, outer_radius + 1);
const qreal step = 360.0 / sectors;
const qreal step = 360.0 / m_sectors;
QColor base = palette().color(QPalette::Window);
base.setAlpha(225);
@@ -130,7 +132,7 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
//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())
&& m_actions.at(m_active) && m_actions.at(m_active)->isEnabled())
{
QPainterPath wedge;
wedge.moveTo(c);
@@ -151,6 +153,9 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
for (int i = 0 ; i < m_actions.count() ; ++i)
{
QAction *action = m_actions.at(i);
if (!action) {
continue;
}
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,
@@ -167,7 +172,7 @@ void DiagramGestureOverlay::paintEvent(QPaintEvent *event)
}
//Name of the highlighted command, under the ring
if (m_active >= 0 && m_active < m_actions.count())
if (m_active >= 0 && m_active < m_actions.count() && m_actions.at(m_active))
{
const QString name = m_actions.at(m_active)->text().remove(QLatin1Char('&'));
const QRect label(0, 2 * outer_radius + 4, width(), 20);
+8 -5
View File
@@ -29,8 +29,9 @@ class QAction;
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.
Commands are placed clockwise from the top, one per direction (4 or 8).
A null entry is an empty direction. The ring only draws; the view
tracks the mouse and asks sectorAt() on release.
*/
class DiagramGestureOverlay : public QWidget
{
@@ -39,14 +40,15 @@ class DiagramGestureOverlay : public QWidget
public:
explicit DiagramGestureOverlay(QWidget *viewport);
void showAt(const QPoint &center, const QList<QAction *> &actions);
void showAt(const QPoint &center, const QList<QAction *> &actions,
int sectors = 8);
void setPointer(const QPoint &viewport_pos);
int sectorAt(const QPoint &viewport_pos) const;
QAction *actionAt(const QPoint &viewport_pos) const;
static bool isEnabled();
int sectors() const { return m_sectors; }
static const int sectors = 8;
static bool isEnabled();
protected:
void paintEvent(QPaintEvent *event) override;
@@ -55,6 +57,7 @@ class DiagramGestureOverlay : public QWidget
QPoint m_center;
QList<QAction *> m_actions;
int m_active = -1;
int m_sectors = 8;
};
#endif // DIAGRAMGESTUREOVERLAY_H
+25 -1
View File
@@ -42,6 +42,7 @@
#include "diagram.h"
#include "diagramcontexttoolbar.h"
#include "diagramgestureoverlay.h"
#include "gesturesettings.h"
#include "shortcutbarsettings.h"
#include "shortcutmanager.h"
#include "ElementsCollection/xmlelementcollection.h"
@@ -794,7 +795,8 @@ void DiagramView::mouseMoveEvent(QMouseEvent *e)
m_diagram->clearEventInterface();
m_swallow_native_menu = true;
}
m_gesture_overlay->showAt(m_gesture_origin, selectionCommands());
m_gesture_overlay->showAt(m_gesture_origin, gestureCommands(),
GestureSettings::directions());
}
if (m_gesture_overlay->isVisible()) {
m_gesture_overlay->setPointer(pos);
@@ -981,6 +983,28 @@ QList<QAction *> DiagramView::selectionCommands() const
return actions;
}
/**
@brief DiagramView::gestureCommands
@return the gesture ring's commands for the current selection, as this
window's actions. A list the user picked keeps its directions: a
command this window lacks leaves its direction empty (nullptr).
Otherwise the ring shows the shortcut bar's commands, in order.
*/
QList<QAction *> DiagramView::gestureCommands() const
{
QETDiagramEditor *qde = diagramEditor();
const auto context = ShortcutBarSettings::contextFor(m_diagram->selectedItems());
if (!qde || !GestureSettings::isCustom(context)) {
return selectionCommands();
}
QList<QAction *> actions;
for (const QString &id : GestureSettings::ids(context)) {
actions << (id.isEmpty() ? nullptr
: ShortcutManager::instance().action(id, qde));
}
return actions;
}
/**
@brief DiagramView::showContextToolbar
After a click that leaves something selected, show the shortcut bar's
+1
View File
@@ -154,6 +154,7 @@ class DiagramView : public PaletteGraphicsView
void placeCellRulers();
void showContextToolbar(const QPoint &viewport_pos);
QList<QAction *> selectionCommands() const;
QList<QAction *> gestureCommands() const;
/// Lowest and highest allowed value of the view transform scale (m11).
/// Prevents wheel-zoom from driving the transform to overflow, which
+125
View File
@@ -0,0 +1,125 @@
/*
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 "gesturesettings.h"
#include <QSettings>
namespace {
const QString directions_key = QStringLiteral("diagrameditor/gestures/directions");
QString settingsKey(ShortcutBarSettings::Context context)
{
switch (context)
{
case ShortcutBarSettings::Canvas:
return QStringLiteral("diagrameditor/gestures/canvas");
case ShortcutBarSettings::Selection:
return QStringLiteral("diagrameditor/gestures/selection");
case ShortcutBarSettings::Conductor:
return QStringLiteral("diagrameditor/gestures/conductor");
}
return QString();
}
}
/**
@return how many directions the ring has: 8 (the default) or 4
*/
int GestureSettings::directions()
{
return QSettings().value(directions_key, 8).toInt() == 4 ? 4 : 8;
}
/**
@brief GestureSettings::setDirections
Save 4 or 8 directions. 8 is the default and removes the key.
*/
void GestureSettings::setDirections(int directions)
{
QSettings settings;
if (directions == 4) {
settings.setValue(directions_key, 4);
} else {
settings.remove(directions_key);
}
}
/**
@return true if the user picked the commands for @a context, false
when the ring follows the shortcut bar
*/
bool GestureSettings::isCustom(ShortcutBarSettings::Context context)
{
return QSettings().contains(settingsKey(context));
}
/**
@return the ids on the ring for @a context: the user's list if they
picked one, the defaults otherwise
*/
QStringList GestureSettings::ids(ShortcutBarSettings::Context context)
{
QSettings settings;
const QString key = settingsKey(context);
if (!settings.contains(key)) {
return defaultIds(context);
}
return settings.value(key).toStringList();
}
/**
@return the shortcut bar's commands for @a context, without its pinned
elements: what the ring has shown since it was added
*/
QStringList GestureSettings::defaultIds(ShortcutBarSettings::Context context)
{
QStringList ids;
for (const QString &id : ShortcutBarSettings::ids(context)) {
if (!ShortcutBarSettings::isElement(id)) {
ids << id;
}
}
return ids;
}
/**
@brief GestureSettings::setIds
Save @a ids for @a context; trailing empty directions are dropped.
Saving the defaults removes the key, so the ring follows the shortcut
bar again.
*/
void GestureSettings::setIds(ShortcutBarSettings::Context context, QStringList ids)
{
while (!ids.isEmpty() && ids.constLast().isEmpty()) {
ids.removeLast();
}
if (ids == defaultIds(context)) {
reset(context);
} else {
QSettings().setValue(settingsKey(context), ids);
}
}
/**
@brief GestureSettings::reset
Make the ring for @a context follow the shortcut bar again.
*/
void GestureSettings::reset(ShortcutBarSettings::Context context)
{
QSettings().remove(settingsKey(context));
}
+47
View File
@@ -0,0 +1,47 @@
/*
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 GESTURESETTINGS_H
#define GESTURESETTINGS_H
#include "shortcutbarsettings.h"
#include <QStringList>
/**
@brief The commands on the mouse gesture ring (right-drag on a folio).
Until the user picks them, the ring shows the shortcut bar's commands
for the selection, as it always has. A picked list is stored in
QSettings, one key per context, and is positional: entry i is the
command in direction i, clockwise from the top, and an empty entry is
an empty direction. Pinned elements are never on the ring.
*/
class GestureSettings
{
public:
static int directions();
static void setDirections(int directions);
static bool isCustom(ShortcutBarSettings::Context context);
static QStringList ids(ShortcutBarSettings::Context context);
static QStringList defaultIds(ShortcutBarSettings::Context context);
static void setIds(ShortcutBarSettings::Context context, QStringList ids);
static void reset(ShortcutBarSettings::Context context);
};
#endif // GESTURESETTINGS_H
+2
View File
@@ -41,6 +41,7 @@
#include "ui/aboutqetdialog.h"
#include "ui/configpage/generalconfigurationpage.h"
#include "ui/configpage/shortcutsconfigpage.h"
#include "ui/configpage/gesturesconfigpage.h"
#include "ui/configpage/shortcutbarconfigpage.h"
#include "ui/configpage/toolbarsconfigpage.h"
#include "machine_info.h"
@@ -2226,6 +2227,7 @@ void QETApp::configureQET()
cd.addPage(new PrintConfigPage());
cd.addPage(new ShortcutsConfigPage());
cd.addPage(new ShortcutBarConfigPage());
cd.addPage(new GesturesConfigPage());
cd.addPage(new ToolbarsConfigPage());
#ifdef QET_SPACEMOUSE_SUPPORT
cd.addPage(new SpaceMouseConfigPage());
@@ -0,0 +1,475 @@
/*
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 "gesturesconfigpage.h"
#include "../../gesturesettings.h"
#include "../../qeticons.h"
#include "../../shortcutmanager.h"
#include <QAction>
#include <QComboBox>
#include <QDataStream>
#include <QDragEnterEvent>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QListWidget>
#include <QMimeData>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QPainterPath>
#include <QPushButton>
#include <QVBoxLayout>
#include <QtMath>
namespace {
const int ring_radius = 100;
const int ring_inner = 26;
const int slot_radius = 68;
const int slot_icon = 24;
/// What QListWidget puts on a drag
const QString list_mime = QStringLiteral("application/x-qabstractitemmodeldatalist");
/**
@return the command id carried by a drag from the command list, or an
empty string
*/
QString droppedId(const QMimeData *mime)
{
if (!mime || !mime->hasFormat(list_mime)) {
return QString();
}
QByteArray data = mime->data(list_mime);
QDataStream stream(&data, QIODevice::ReadOnly);
int row, column;
QMap<int, QVariant> roles;
stream >> row >> column >> roles;
return roles.value(Qt::UserRole).toString();
}
}
GestureRingEditor::GestureRingEditor(QWidget *parent) :
QWidget(parent)
{
setAcceptDrops(true);
setFocusPolicy(Qt::StrongFocus);
setMinimumSize(sizeHint());
}
QSize GestureRingEditor::sizeHint() const
{
return QSize(2 * ring_radius + 8, 2 * ring_radius + 8);
}
void GestureRingEditor::setIds(const QStringList &ids)
{
m_ids = ids;
update();
}
void GestureRingEditor::setDirections(int directions)
{
m_directions = directions == 4 ? 4 : 8;
setCurrentSlot(qMin(m_current, m_directions - 1));
update();
}
void GestureRingEditor::setDescriptions(const QHash<QString, QString> &descriptions)
{
m_descriptions = descriptions;
update();
}
void GestureRingEditor::setCurrentSlot(int slot)
{
if (slot < 0 || slot >= m_directions) {
return;
}
const bool changed = slot != m_current;
m_current = slot;
update();
if (changed) {
emit currentSlotChanged(slot);
}
}
/**
@return the slot in the direction of @a pos from the centre, 0 at the
top then clockwise, or -1 at the centre or outside the ring
*/
int GestureRingEditor::slotAt(const QPoint &pos) const
{
const QPointF d = QPointF(pos) - QPointF(rect().center());
const qreal distance = qHypot(d.x(), d.y());
if (distance < ring_inner || distance > ring_radius) {
return -1;
}
qreal angle = qRadiansToDegrees(qAtan2(d.x(), -d.y()));
if (angle < 0) {
angle += 360;
}
return int(qRound(angle / (360.0 / m_directions))) % m_directions;
}
/**
@brief GestureRingEditor::place
Put @a id in the current slot. A command already elsewhere on the ring
moves here, so a command has one direction.
*/
void GestureRingEditor::place(const QString &id)
{
if (id.isEmpty()) {
return;
}
const int old = m_ids.indexOf(id);
if (old >= 0) {
m_ids[old].clear();
}
while (m_ids.count() <= m_current) {
m_ids << QString();
}
m_ids[m_current] = id;
update();
emit edited();
}
void GestureRingEditor::clearCurrent()
{
if (m_current < m_ids.count() && !m_ids.at(m_current).isEmpty()) {
m_ids[m_current].clear();
update();
emit edited();
}
}
QString GestureRingEditor::idAt(int slot) const
{
return slot < m_ids.count() ? m_ids.at(slot) : QString();
}
QPointF GestureRingEditor::slotCenter(int slot) const
{
const qreal a = qDegreesToRadians(slot * 360.0 / m_directions);
const QPointF c = QRectF(rect()).center();
return QPointF(c.x() + slot_radius * qSin(a), c.y() - slot_radius * qCos(a));
}
void GestureRingEditor::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
const QPointF c = QRectF(rect()).center();
const qreal step = 360.0 / m_directions;
const QRectF outer(c.x() - ring_radius, c.y() - ring_radius,
2 * ring_radius, 2 * ring_radius);
QColor line = palette().color(QPalette::Text);
line.setAlpha(110);
painter.setPen(line);
painter.setBrush(palette().color(QPalette::Base));
painter.drawEllipse(outer);
//The chosen slot
QPainterPath wedge;
wedge.moveTo(c);
wedge.arcTo(outer, 90 - m_current * step - step / 2, step);
wedge.closeSubpath();
painter.setPen(Qt::NoPen);
QColor highlight = palette().color(QPalette::Highlight);
if (!hasFocus()) {
highlight.setAlpha(140);
}
painter.setBrush(highlight);
painter.drawPath(wedge);
//Lines between the slots
painter.setPen(line);
for (int i = 0 ; i < m_directions ; ++i) {
const qreal a = qDegreesToRadians(i * step + step / 2);
painter.drawLine(QPointF(c.x() + ring_inner * qSin(a), c.y() - ring_inner * qCos(a)),
QPointF(c.x() + ring_radius * qSin(a), c.y() - ring_radius * qCos(a)));
}
painter.setBrush(palette().color(QPalette::Window));
painter.drawEllipse(c, ring_inner, ring_inner);
for (int i = 0 ; i < m_directions ; ++i)
{
const QString id = idAt(i);
if (id.isEmpty()) {
continue;
}
const QPointF p = slotCenter(i);
const QRect r(int(p.x()) - slot_icon / 2, int(p.y()) - slot_icon / 2,
slot_icon, slot_icon);
QAction *action = ShortcutManager::instance().action(id, nullptr);
if (action && !action->icon().isNull()) {
action->icon().paint(&painter, r);
} else {
painter.setPen(palette().color(i == m_current ? QPalette::HighlightedText
: QPalette::Text));
painter.drawText(r.adjusted(-12, 0, 12, 0), Qt::AlignCenter,
m_descriptions.value(id, id).left(3));
}
}
}
void GestureRingEditor::mousePressEvent(QMouseEvent *event)
{
setCurrentSlot(slotAt(event->position().toPoint()));
event->accept();
}
void GestureRingEditor::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Delete || event->key() == Qt::Key_Backspace) {
clearCurrent();
} else if (event->key() == Qt::Key_Right || event->key() == Qt::Key_Down) {
setCurrentSlot((m_current + 1) % m_directions);
} else if (event->key() == Qt::Key_Left || event->key() == Qt::Key_Up) {
setCurrentSlot((m_current + m_directions - 1) % m_directions);
} else {
QWidget::keyPressEvent(event);
}
}
void GestureRingEditor::dragEnterEvent(QDragEnterEvent *event)
{
if (!droppedId(event->mimeData()).isEmpty()) {
event->acceptProposedAction();
}
}
void GestureRingEditor::dragMoveEvent(QDragMoveEvent *event)
{
const int slot = slotAt(event->position().toPoint());
if (slot < 0) {
event->ignore();
return;
}
setCurrentSlot(slot);
event->acceptProposedAction();
}
void GestureRingEditor::dropEvent(QDropEvent *event)
{
const int slot = slotAt(event->position().toPoint());
const QString id = droppedId(event->mimeData());
if (slot < 0 || id.isEmpty()) {
event->ignore();
return;
}
setCurrentSlot(slot);
place(id);
event->acceptProposedAction();
}
/**
@brief GesturesConfigPage::GesturesConfigPage
@param parent
*/
GesturesConfigPage::GesturesConfigPage(QWidget *parent) :
ConfigPage(parent)
{
const QStringList available = ShortcutBarSettings::availableIds();
for (const ShortcutManager::ShortcutInfo &info :
ShortcutManager::instance().allShortcuts()) {
if (available.contains(info.id)) {
m_descriptions.insert(info.id, info.description);
}
}
for (const ShortcutBarSettings::Context c : ShortcutBarSettings::contexts()) {
m_pending.insert(c, GestureSettings::ids(c));
m_custom.insert(c, GestureSettings::isCustom(c));
}
auto *explanation = new QLabel(
tr("Faites glisser avec le bouton droit sur un folio : un anneau "
"de commandes s'ouvre, relâchez vers celle à lancer. Tant que "
"vous ne changez pas l'anneau d'un contexte, il montre les "
"commandes de la barre de raccourcis. Les gestes s'activent "
"ou se désactivent dans la page Général."), this);
explanation->setWordWrap(true);
m_directions = new QComboBox(this);
m_directions->setObjectName(QStringLiteral("directionsCombo"));
m_directions->addItem(tr("8 directions"), 8);
m_directions->addItem(tr("4 directions"), 4);
m_directions->setCurrentIndex(m_directions->findData(GestureSettings::directions()));
m_context = new QComboBox(this);
m_context->setObjectName(QStringLiteral("contextCombo"));
for (const ShortcutBarSettings::Context c : ShortcutBarSettings::contexts()) {
m_context->addItem(ShortcutBarSettings::title(c), c);
}
m_available = new QListWidget(this);
m_available->setObjectName(QStringLiteral("availableList"));
m_available->setSortingEnabled(true);
m_available->setDragEnabled(true);
m_available->setDragDropMode(QAbstractItemView::DragOnly);
for (auto it = m_descriptions.cbegin(); it != m_descriptions.cend(); ++it) {
QIcon icon;
if (QAction *action = ShortcutManager::instance().action(it.key(), nullptr)) {
icon = action->icon();
}
auto *item = new QListWidgetItem(icon, it.value(), m_available);
item->setData(Qt::UserRole, it.key());
}
m_ring = new GestureRingEditor(this);
m_ring->setObjectName(QStringLiteral("ringEditor"));
m_ring->setDescriptions(m_descriptions);
m_ring->setDirections(GestureSettings::directions());
m_slot_name = new QLabel(this);
m_slot_name->setAlignment(Qt::AlignCenter);
m_slot_name->setWordWrap(true);
m_state = new QLabel(this);
m_state->setWordWrap(true);
auto *place = new QPushButton(tr("Placer →"), this);
place->setObjectName(QStringLiteral("placeButton"));
auto *clear = new QPushButton(tr("Vider la direction"), this);
auto *follow = new QPushButton(tr("Comme la barre de raccourcis"), this);
follow->setObjectName(QStringLiteral("followBarButton"));
auto *ring_column = new QVBoxLayout();
ring_column->addWidget(m_ring, 0, Qt::AlignHCenter);
ring_column->addWidget(m_slot_name);
ring_column->addWidget(clear);
ring_column->addStretch();
auto *grid = new QGridLayout();
grid->addWidget(new QLabel(tr("Commandes disponibles"), this), 0, 0);
grid->addWidget(new QLabel(tr("Anneau, la direction choisie en surbrillance"), this), 0, 2);
grid->addWidget(m_available, 1, 0);
grid->addWidget(place, 1, 1, Qt::AlignVCenter);
grid->addLayout(ring_column, 1, 2);
grid->setColumnStretch(0, 1);
grid->setColumnStretch(2, 1);
auto *top_row = new QHBoxLayout();
top_row->addWidget(new QLabel(tr("Directions :"), this));
top_row->addWidget(m_directions);
top_row->addSpacing(12);
top_row->addWidget(new QLabel(tr("Contexte :"), this));
top_row->addWidget(m_context, 1);
auto *state_row = new QHBoxLayout();
state_row->addWidget(m_state, 1);
state_row->addWidget(follow);
auto *layout = new QVBoxLayout(this);
layout->addWidget(explanation);
layout->addLayout(top_row);
layout->addLayout(state_row);
layout->addLayout(grid);
connect(m_directions, qOverload<int>(&QComboBox::currentIndexChanged), this, [this]() {
m_ring->setDirections(m_directions->currentData().toInt());
updateState();
});
connect(m_context, qOverload<int>(&QComboBox::currentIndexChanged),
this, &GesturesConfigPage::showContext);
connect(m_ring, &GestureRingEditor::edited, this, [this]() {
m_pending.insert(m_shown, m_ring->ids());
m_custom.insert(m_shown, true);
updateState();
});
connect(m_ring, &GestureRingEditor::currentSlotChanged,
this, &GesturesConfigPage::updateState);
connect(place, &QPushButton::clicked, this, &GesturesConfigPage::placeSelected);
connect(m_available, &QListWidget::itemDoubleClicked,
this, &GesturesConfigPage::placeSelected);
connect(clear, &QPushButton::clicked, m_ring, &GestureRingEditor::clearCurrent);
connect(follow, &QPushButton::clicked, this, &GesturesConfigPage::followBar);
showContext();
}
/**
@brief GesturesConfigPage::applyConf
Save the directions and every context's ring.
*/
void GesturesConfigPage::applyConf()
{
GestureSettings::setDirections(m_directions->currentData().toInt());
for (const ShortcutBarSettings::Context c : ShortcutBarSettings::contexts()) {
if (m_custom.value(c)) {
GestureSettings::setIds(c, m_pending.value(c));
} else {
GestureSettings::reset(c);
}
}
}
QString GesturesConfigPage::title() const
{
return tr("Gestes de la souris", "configuration page title");
}
QIcon GesturesConfigPage::icon() const
{
return QET::Icons::ConfigureShortcuts;
}
void GesturesConfigPage::showContext()
{
m_shown = static_cast<ShortcutBarSettings::Context>(
m_context->currentData().toInt());
m_ring->setIds(m_pending.value(m_shown));
updateState();
}
void GesturesConfigPage::placeSelected()
{
if (QListWidgetItem *item = m_available->currentItem()) {
m_ring->place(item->data(Qt::UserRole).toString());
}
}
/**
@brief GesturesConfigPage::followBar
Make the shown context's ring follow the shortcut bar again.
*/
void GesturesConfigPage::followBar()
{
m_custom.insert(m_shown, false);
m_pending.insert(m_shown, GestureSettings::defaultIds(m_shown));
m_ring->setIds(m_pending.value(m_shown));
updateState();
}
/**
@brief GesturesConfigPage::updateState
Name the chosen direction's command, and say whether the shown ring is
the user's or the shortcut bar's.
*/
void GesturesConfigPage::updateState()
{
const int slot = m_ring->currentSlot();
const QStringList ids = m_ring->ids();
const QString id = slot < ids.count() ? ids.at(slot) : QString();
m_slot_name->setText(id.isEmpty() ? tr("(direction vide)")
: m_descriptions.value(id, id));
m_state->setText(m_custom.value(m_shown)
? tr("Anneau personnalisé pour ce contexte.")
: tr("Cet anneau suit la barre de raccourcis."));
}
+112
View File
@@ -0,0 +1,112 @@
/*
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 GESTURESCONFIGPAGE_H
#define GESTURESCONFIGPAGE_H
#include "configpage.h"
#include "../../shortcutbarsettings.h"
#include <QHash>
#include <QStringList>
class QComboBox;
class QLabel;
class QListWidget;
/**
@brief The gesture ring as an editor: one slot per direction, clockwise
from the top. Click a slot to choose it; drop a command from the
command list on a slot to put it there; Delete empties the chosen slot.
*/
class GestureRingEditor : public QWidget
{
Q_OBJECT
public:
explicit GestureRingEditor(QWidget *parent = nullptr);
void setIds(const QStringList &ids);
QStringList ids() const { return m_ids; }
void setDirections(int directions);
void setDescriptions(const QHash<QString, QString> &descriptions);
int currentSlot() const { return m_current; }
void setCurrentSlot(int slot);
int slotAt(const QPoint &pos) const;
void place(const QString &id);
void clearCurrent();
QSize sizeHint() const override;
signals:
void edited();
void currentSlotChanged(int slot);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dropEvent(QDropEvent *event) override;
private:
QString idAt(int slot) const;
QPointF slotCenter(int slot) const;
QStringList m_ids;
QHash<QString, QString> m_descriptions;
int m_directions = 8;
int m_current = 0;
};
/**
@brief The GesturesConfigPage class
Choose 4 or 8 directions for the mouse gesture ring, and which command
sits in each direction for each selection context. A context the user
does not edit keeps following the shortcut bar. Saved by applyConf().
*/
class GesturesConfigPage : public ConfigPage
{
Q_OBJECT
public:
explicit GesturesConfigPage(QWidget *parent = nullptr);
void applyConf() override;
QString title() const override;
QIcon icon() const override;
private:
void showContext();
void placeSelected();
void followBar();
void updateState();
QComboBox *m_directions;
QComboBox *m_context;
QListWidget *m_available;
GestureRingEditor *m_ring;
QLabel *m_slot_name;
QLabel *m_state;
ShortcutBarSettings::Context m_shown = ShortcutBarSettings::Canvas;
QHash<int, QStringList> m_pending;
QHash<int, bool> m_custom;
QHash<QString, QString> m_descriptions;
};
#endif // GESTURESCONFIGPAGE_H
+23
View File
@@ -298,6 +298,29 @@ set_tests_properties(tst_toolbarsettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM
target_include_directories(tst_toolbarsettings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_toolbarsettings PRIVATE Qt::Test Qt::Widgets Qt::Svg)
# GestureSettings, the gesture ring's 4 / 8 directions and empty slots, and
# its configuration page: the ring follows the shortcut bar until the user
# picks commands, then keeps each one in its direction.
add_executable(
tst_gesturesettings
tst_gesturesettings.cpp
${QET_DIR}/sources/gesturesettings.cpp
${QET_DIR}/sources/gesturesettings.h
${QET_DIR}/sources/shortcutbarsettings.cpp
${QET_DIR}/sources/diagramgestureoverlay.cpp
${QET_DIR}/sources/diagramgestureoverlay.h
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.cpp
${QET_DIR}/sources/ui/configpage/gesturesconfigpage.h
${QET_DIR}/sources/ui/configpage/configpage.h
${QET_DIR}/sources/shortcutmanager.cpp
${QET_DIR}/sources/qeticons.cpp
${QET_DIR}/sources/qetpalette.cpp
${QET_DIR}/sources/qetstyle.cpp)
add_test(NAME tst_gesturesettings COMMAND tst_gesturesettings)
set_tests_properties(tst_gesturesettings PROPERTIES ENVIRONMENT "QT_QPA_PLATFORM=offscreen")
target_include_directories(tst_gesturesettings PRIVATE ${QET_DIR}/sources)
target_link_libraries(tst_gesturesettings PRIVATE Qt::Test Qt::Widgets Qt::Svg Qt::Xml pugixml::pugixml)
# scriptheader.h is header-only: the // ==QETScript== block that turns a
# stored script into a button; and ShortcutManager::unregisterAction(), which
# takes a deleted script's command out of the lists again.
+214
View File
@@ -0,0 +1,214 @@
/*
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 "gesturesettings.h"
#include "shortcutmanager.h"
#include "ui/configpage/gesturesconfigpage.h"
#include <QAction>
#include <QComboBox>
#include <QDropEvent>
#include <QListWidget>
#include <QMainWindow>
#include <QMimeData>
#include <QPushButton>
#include <QSettings>
#include <QStandardPaths>
#include <QTest>
/**
The mouse gesture ring's own command list: until the user picks one it
follows the shortcut bar as before; a picked list keeps its directions;
4 or 8 directions; and the configuration page that edits it.
*/
class tst_gesturesettings : public QObject
{
Q_OBJECT
QMainWindow m_window;
QAction *addCommand(const QString &id, const QString &text)
{
auto *action = new QAction(text, &m_window);
ShortcutManager::instance().registerAction(
action, id, QStringLiteral("Éditeur de schémas"), QKeySequence());
return action;
}
QListWidgetItem *listItem(QListWidget *list, const QString &id)
{
for (int i = 0 ; i < list->count() ; ++i) {
if (list->item(i)->data(Qt::UserRole).toString() == id) {
return list->item(i);
}
}
return nullptr;
}
private slots:
void initTestCase()
{
QStandardPaths::setTestModeEnabled(true);
QSettings().remove(QStringLiteral("diagrameditor"));
addCommand(QStringLiteral("diagrameditor.copy"), QStringLiteral("Copier"));
addCommand(QStringLiteral("diagrameditor.cut"), QStringLiteral("Couper"));
addCommand(QStringLiteral("diagrameditor.paste"), QStringLiteral("Coller"));
}
void cleanup()
{
QSettings().remove(QStringLiteral("diagrameditor"));
}
void defaultsFollowTheBar()
{
QCOMPARE(GestureSettings::directions(), 8);
for (const auto c : ShortcutBarSettings::contexts()) {
QVERIFY(!GestureSettings::isCustom(c));
QCOMPARE(GestureSettings::ids(c), ShortcutBarSettings::ids(c));
}
//A changed bar still reaches the ring, without its elements
ShortcutBarSettings::setIds(ShortcutBarSettings::Selection,
{QStringLiteral("diagrameditor.copy"),
QStringLiteral("common://10_electric/relay.elmt"),
QStringLiteral("diagrameditor.cut")});
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Selection),
QStringList({QStringLiteral("diagrameditor.copy"),
QStringLiteral("diagrameditor.cut")}));
}
void pickedListKeepsDirections()
{
GestureSettings::setIds(ShortcutBarSettings::Canvas,
{QStringLiteral("diagrameditor.copy"), QString(),
QStringLiteral("diagrameditor.paste"), QString(), QString()});
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Canvas));
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas),
QStringList({QStringLiteral("diagrameditor.copy"), QString(),
QStringLiteral("diagrameditor.paste")}));
//The bar no longer drives it
ShortcutBarSettings::setIds(ShortcutBarSettings::Canvas,
{QStringLiteral("diagrameditor.cut")});
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas).first(),
QStringLiteral("diagrameditor.copy"));
//Other contexts still follow the bar
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Selection));
}
void savingTheDefaultsFollowsTheBarAgain()
{
GestureSettings::setIds(ShortcutBarSettings::Conductor, {QStringLiteral("diagrameditor.cut")});
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Conductor));
GestureSettings::setIds(ShortcutBarSettings::Conductor,
GestureSettings::defaultIds(ShortcutBarSettings::Conductor));
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Conductor));
}
void directions()
{
GestureSettings::setDirections(4);
QCOMPARE(GestureSettings::directions(), 4);
GestureSettings::setDirections(8);
QVERIFY(!QSettings().contains(QStringLiteral("diagrameditor/gestures/directions")));
QSettings().setValue(QStringLiteral("diagrameditor/gestures/directions"), 5);
QCOMPARE(GestureSettings::directions(), 8);
}
void overlayDirectionsAndEmptySlots()
{
QWidget viewport;
viewport.resize(400, 400);
DiagramGestureOverlay overlay(&viewport);
QAction *copy = ShortcutManager::instance().action(QStringLiteral("diagrameditor.copy"), &m_window);
QAction *paste = ShortcutManager::instance().action(QStringLiteral("diagrameditor.paste"), &m_window);
const QPoint c(200, 200);
const QPoint right(260, 200), down(200, 260);
overlay.showAt(c, {copy, nullptr, paste}, 4);
QCOMPARE(overlay.sectors(), 4);
QCOMPARE(overlay.sectorAt(right), 1);
QCOMPARE(overlay.actionAt(right), nullptr);
QCOMPARE(overlay.actionAt(down), paste);
overlay.setPointer(right);
QVERIFY(!overlay.grab().isNull());
//Today's ring: eight directions
overlay.showAt(c, {copy, nullptr, paste});
QCOMPARE(overlay.sectors(), 8);
QCOMPARE(overlay.sectorAt(right), 2);
QCOMPARE(overlay.actionAt(right), paste);
}
void pagePlacesAndSaves()
{
{
GesturesConfigPage page;
auto *directions = page.findChild<QComboBox *>(QStringLiteral("directionsCombo"));
auto *list = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *ring = page.findChild<GestureRingEditor *>(QStringLiteral("ringEditor"));
QCOMPARE(directions->currentData().toInt(), 8);
QCOMPARE(ring->ids(), GestureSettings::defaultIds(ShortcutBarSettings::Canvas));
directions->setCurrentIndex(directions->findData(4));
ring->setCurrentSlot(1);
list->setCurrentItem(listItem(list, QStringLiteral("diagrameditor.cut")));
page.findChild<QPushButton *>(QStringLiteral("placeButton"))->click();
page.applyConf();
}
QCOMPARE(GestureSettings::directions(), 4);
QVERIFY(GestureSettings::isCustom(ShortcutBarSettings::Canvas));
QCOMPARE(GestureSettings::ids(ShortcutBarSettings::Canvas).at(1),
QStringLiteral("diagrameditor.cut"));
//Contexts left alone still follow the bar
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Selection));
{
GesturesConfigPage page;
page.findChild<QPushButton *>(QStringLiteral("followBarButton"))->click();
page.applyConf();
}
QVERIFY(!GestureSettings::isCustom(ShortcutBarSettings::Canvas));
}
void dropOnASlot()
{
GesturesConfigPage page;
auto *list = page.findChild<QListWidget *>(QStringLiteral("availableList"));
auto *ring = page.findChild<GestureRingEditor *>(QStringLiteral("ringEditor"));
ring->resize(ring->sizeHint());
const QModelIndex index = list->model()->index(
list->row(listItem(list, QStringLiteral("diagrameditor.paste"))), 0);
QMimeData *mime = list->model()->mimeData({index});
//The right-hand slot of eight
const QPointF target = QRectF(ring->rect()).center() + QPointF(68, 0);
QCOMPARE(ring->slotAt(target.toPoint()), 2);
QDropEvent drop(target, Qt::CopyAction, mime, Qt::LeftButton, Qt::NoModifier);
//Straight to the widget: QApplication drops drag events when
//no real drag is running
QVERIFY(static_cast<QObject *>(ring)->event(&drop));
QVERIFY(drop.isAccepted());
QCOMPARE(ring->currentSlot(), 2);
QCOMPARE(ring->ids().value(2), QStringLiteral("diagrameditor.paste"));
//Moved, not copied: paste was already on the default canvas ring
QCOMPARE(ring->ids().count(QStringLiteral("diagrameditor.paste")), 1);
delete mime;
}
};
QTEST_MAIN(tst_gesturesettings)
#include "tst_gesturesettings.moc"