mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 13:24:14 +02:00
83b9f32bd0
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.
199 lines
5.6 KiB
C++
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;
|
|
}
|