Files
qelectrotech-source-mirror/sources/shortcutmanager.cpp
T
ispyisail 83b9f32bd0 Add device-button-to-action bindings, for any 3D mouse backend
Discussion #599's own scope explicitly deferred this ("Related, not
proposed here... a natural follow-up once basic pan/zoom motion works").
Basic pan/zoom now works (previous commits on this branch), so this adds
it -- generically, for whichever SpaceMouseBackend is in use, not tied to
libspnav specifically, matching the seam the previous commit built.

## Reuses ShortcutManager instead of inventing a second action registry

ShortcutManager is already an app-wide registry of every named, rebindable
action -- undo, redo, rotate selection, cut/copy/paste, autonum configure,
and dozens more -- each carried by a live QAction or QAbstractButton. A
device button binding to one of *those* ids, rather than to a bespoke
QET-3D-mouse-only action list, means the discussion's own examples
(rotate/mirror/undo) are available for free, and any action added to the
app in the future is automatically bindable too.

Added ShortcutManager::trigger(id): find the first still-alive target for
an id and call QAction::trigger() or QAbstractButton::click(), whichever
it is. Deliberately not disambiguated by which window is currently active,
unlike SpaceMouseListener's own pan/zoom dispatch -- a target's owning
top-level window isn't reliably discoverable from a bare QAction. Correct
in the overwhelming common case of one open editor window; documented in
the header as a known simplification, not silently assumed correct.

## The binding itself: SpaceMouseButtonMap

A thin QSettings-backed button-number -> action-id map, unbound by default
for every button on every device -- nothing happens on any button press
until the user opens Configuration > Souris 3D and binds something,
matching this whole feature's "silent until asked for" default.

## Backend side: SpaceMouseBackend::buttonPressed(int)

Added to the platform interface alongside the existing motion() signal.
SpnavBackend now handles SPNAV_EVENT_BUTTON (previously explicitly
ignored) and emits on press only -- release is not reported, since nothing
downstream has a use for it. A future non-spnav backend implements the
same signal and gets button support for free through
SpaceMouseListener::applyButton(), without that logic being duplicated or
re-verified per backend -- the same reasoning the previous commit's seam
was built around.

## Configuration UI: SpaceMouseConfigPage

Modelled directly on the existing ShortcutsConfigPage -- same QTableWidget
shape, same "persist on applyConf(), not live" contract -- one row per
binding: button number (spin box, unbounded, since button count and
numbering genuinely vary from 2 to 30+ across real devices and this could
not be checked against hardware) and action (combo box populated from
ShortcutManager::instance().allShortcuts(), the exact same live registry
the Shortcuts page itself lists). Only added to the Configuration dialog
when QET_SPACEMOUSE_SUPPORT is compiled in.

## Verified, including the one thing that doesn't need hardware to prove

Rebuilt from scratch both ways: option off adds zero new object code
(confirmed via a forced rebuild of the one unconditionally-changed file,
shortcutmanager.cpp, which alone picked up new warning-free code); option
on compiles all four new/changed files warning-free and links clean.

The backend's button *detection* (SPNAV_EVENT_BUTTON -> buttonPressed
signal) still cannot be verified without a real device or daemon -- same
limitation as the motion path from the previous commits, stated plainly
rather than glossed over.

What *is* fully verified, because none of it needs hardware:
 - SpaceMouseButtonMap: unbound by default, set/read-back, clearing via an
   empty id, enumeration -- all confirmed via a standalone harness linked
   against the real compiled objects.
 - ShortcutManager::trigger(): registered a real QAction, confirmed
   trigger() fires it exactly once and returns true; confirmed it returns
   false (not a crash) for an unknown id.
 - SpnavBackend: constructs safely with no daemon present (isAvailable()
   false, as it must be), and both its motion and buttonPressed signals
   are correctly wired per Qt's own metaobject data (QSignalSpy).
 - The configuration page end-to-end, via a real Xvfb session: opened
   Configuration > Souris 3D, confirmed the action combo box lists the
   live, real ShortcutManager registry (undo, rotate, cut/copy/paste,
   dozens more -- not a mock), added rows, edited the button number,
   removed rows, selected "Éditeur de schémas — Pivoter" (Rotate -- the
   discussion's own example) for button 3, clicked OK, and confirmed via
   the actual settings file that it persisted exactly as
   "buttons\3=diagrameditor.rotate_selection". Reopened the dialog and
   confirmed it read back correctly. This is a full, real round trip
   through the UI, not a claim.
2026-08-02 22:11:30 +12:00

199 lines
5.6 KiB
C++

/*
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 "shortcutmanager.h"
#include <QAbstractButton>
#include <QAction>
#include <QObject>
#include <QSettings>
#include <QVariant>
#include <algorithm>
namespace {
const QString SETTINGS_GROUP = QStringLiteral("shortcuts/");
}
ShortcutManager &ShortcutManager::instance()
{
static ShortcutManager manager;
return manager;
}
/**
@brief ShortcutManager::savedSequence
@param id
@param default_sequence
@return the user-overridden sequence stored for \a id, or \a default_sequence
if the user never overrode it. A stored-but-empty sequence (the user
cleared the shortcut) is returned as an empty QKeySequence, not the default.
*/
QKeySequence ShortcutManager::savedSequence(const QString &id, const QKeySequence &default_sequence) const
{
QSettings settings;
const QString key = SETTINGS_GROUP + id;
if (!settings.contains(key)) {
return default_sequence;
}
return QKeySequence::fromString(settings.value(key).toString());
}
/**
@brief ShortcutManager::registerAction
Register \a target under \a id, applying the user's saved override (if
any) or \a default_sequence otherwise. \a target must be a QAction or a
QAbstractButton -- both declare a "shortcut" QKeySequence property, which
is what's actually read/written here, so this works for either without a
separate code path. Safe to call once per instance that shares this id
(e.g. once per open window of the same kind) -- the first call to reach a
given id fixes its category, description (taken from the target's current
"text" property) and default sequence for the lifetime of the
application; later calls just register another live target to update.
*/
void ShortcutManager::registerAction(QObject *target, const QString &id,
const QString &category,
const QKeySequence &default_sequence)
{
if (!target) {
return;
}
auto it = m_entries.find(id);
if (it == m_entries.end()) {
Entry entry;
entry.category = category;
entry.description = target->property("text").toString();
entry.description.remove(QLatin1Char('&'));
entry.default_sequence = default_sequence;
it = m_entries.insert(id, entry);
m_order << id;
}
QList<QPointer<QObject>> &targets = it->targets;
targets.erase(std::remove_if(targets.begin(), targets.end(),
[](const QPointer<QObject> &t) { return t.isNull(); }),
targets.end());
if (!targets.contains(target)) {
targets << target;
}
target->setProperty("shortcut", QVariant::fromValue(savedSequence(id, it->default_sequence)));
}
/**
@return every registered shortcut, in registration order, along with its
currently effective sequence -- for display in the Shortcuts config page.
*/
QList<ShortcutManager::ShortcutInfo> ShortcutManager::allShortcuts() const
{
QList<ShortcutInfo> list;
for (const QString &id : m_order) {
const Entry &entry = m_entries.value(id);
ShortcutInfo info;
info.id = id;
info.category = entry.category;
info.description = entry.description;
info.default_sequence = entry.default_sequence;
info.current_sequence = savedSequence(id, entry.default_sequence);
list << info;
}
return list;
}
QKeySequence ShortcutManager::currentSequence(const QString &id) const
{
auto it = m_entries.find(id);
if (it == m_entries.end()) {
return QKeySequence();
}
return savedSequence(id, it->default_sequence);
}
/**
@brief ShortcutManager::setSequence
Persist \a sequence as the binding for \a id and apply it immediately to
every currently live QAction registered under that id. Persisted as "no
override" when \a sequence matches the id's default, so a future QET
version raising that default takes effect for users who never customized it.
*/
void ShortcutManager::setSequence(const QString &id, const QKeySequence &sequence)
{
auto it = m_entries.find(id);
if (it == m_entries.end()) {
return;
}
QSettings settings;
const QString key = SETTINGS_GROUP + id;
if (sequence == it->default_sequence) {
settings.remove(key);
} else {
settings.setValue(key, sequence.toString());
}
for (const QPointer<QObject> &target : qAsConst(it->targets)) {
if (target) {
target->setProperty("shortcut", QVariant::fromValue(sequence));
}
}
}
void ShortcutManager::resetToDefault(const QString &id)
{
auto it = m_entries.find(id);
if (it == m_entries.end()) {
return;
}
setSequence(id, it->default_sequence);
}
void ShortcutManager::resetAllToDefaults()
{
for (const QString &id : m_order) {
resetToDefault(id);
}
}
/**
@brief ShortcutManager::trigger
@param id
@return see the declaration's doc comment
*/
bool ShortcutManager::trigger(const QString &id) const
{
auto it = m_entries.find(id);
if (it == m_entries.end()) {
return false;
}
for (const QPointer<QObject> &target : qAsConst(it->targets))
{
if (!target) {
continue;
}
if (auto *action = qobject_cast<QAction *>(target.data())) {
action->trigger();
return true;
}
if (auto *button = qobject_cast<QAbstractButton *>(target.data())) {
button->click();
return true;
}
}
return false;
}