Merge branch 'feature/customise-gesture-list' into feature/customise-dialog

# Conflicts:
#	tests/qttest/CMakeLists.txt
This commit is contained in:
ispyisail
2026-10-07 18:41:40 +13:00
12 changed files with 1050 additions and 15 deletions
@@ -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