From 83b9f32bd04d05a1ca80478a1f72f5c4a8d0707f Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 2 Aug 2026 22:11:30 +1200 Subject: [PATCH] Add device-button-to-action bindings, for any 3D mouse backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cmake/qet_compilation_vars.cmake | 4 + sources/qetapp.cpp | 4 + sources/shortcutmanager.cpp | 31 +++ sources/shortcutmanager.h | 14 ++ sources/spacemouse/spacemousebackend.h | 9 + sources/spacemouse/spacemousebuttonmap.cpp | 72 ++++++ sources/spacemouse/spacemousebuttonmap.h | 55 +++++ sources/spacemouse/spacemouselistener.cpp | 16 ++ sources/spacemouse/spacemouselistener.h | 21 +- sources/spacemouse/spnavbackend.cpp | 10 +- .../ui/configpage/spacemouseconfigpage.cpp | 223 ++++++++++++++++++ sources/ui/configpage/spacemouseconfigpage.h | 69 ++++++ 12 files changed, 519 insertions(+), 9 deletions(-) create mode 100644 sources/spacemouse/spacemousebuttonmap.cpp create mode 100644 sources/spacemouse/spacemousebuttonmap.h create mode 100644 sources/ui/configpage/spacemouseconfigpage.cpp create mode 100644 sources/ui/configpage/spacemouseconfigpage.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 17d01d290..62eb24c93 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -797,8 +797,12 @@ endif() if(QET_SPACEMOUSE_ENABLED) list(APPEND QET_SRC_FILES ${QET_DIR}/sources/spacemouse/spacemousebackend.h + ${QET_DIR}/sources/spacemouse/spacemousebuttonmap.cpp + ${QET_DIR}/sources/spacemouse/spacemousebuttonmap.h ${QET_DIR}/sources/spacemouse/spacemouselistener.cpp ${QET_DIR}/sources/spacemouse/spacemouselistener.h + ${QET_DIR}/sources/ui/configpage/spacemouseconfigpage.cpp + ${QET_DIR}/sources/ui/configpage/spacemouseconfigpage.h ) endif() diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp index 5004f1dfe..0673b136a 100644 --- a/sources/qetapp.cpp +++ b/sources/qetapp.cpp @@ -42,6 +42,7 @@ #include "qetversion.h" #ifdef QET_SPACEMOUSE_SUPPORT # include "spacemouse/spacemouselistener.h" +# include "ui/configpage/spacemouseconfigpage.h" #endif #include @@ -2064,6 +2065,9 @@ void QETApp::configureQET() cd.addPage(new ExportConfigPage()); cd.addPage(new PrintConfigPage()); cd.addPage(new ShortcutsConfigPage()); +#ifdef QET_SPACEMOUSE_SUPPORT + cd.addPage(new SpaceMouseConfigPage()); +#endif // associates the dialog with a possible parent widget // associe le dialogue a un eventuel widget parent diff --git a/sources/shortcutmanager.cpp b/sources/shortcutmanager.cpp index 083c4a44a..598b0cbe6 100644 --- a/sources/shortcutmanager.cpp +++ b/sources/shortcutmanager.cpp @@ -17,6 +17,8 @@ */ #include "shortcutmanager.h" +#include +#include #include #include #include @@ -165,3 +167,32 @@ void ShortcutManager::resetAllToDefaults() 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 &target : qAsConst(it->targets)) + { + if (!target) { + continue; + } + if (auto *action = qobject_cast(target.data())) { + action->trigger(); + return true; + } + if (auto *button = qobject_cast(target.data())) { + button->click(); + return true; + } + } + return false; +} diff --git a/sources/shortcutmanager.h b/sources/shortcutmanager.h index 4306895c8..d40448c52 100644 --- a/sources/shortcutmanager.h +++ b/sources/shortcutmanager.h @@ -71,6 +71,20 @@ class ShortcutManager void resetToDefault(const QString &id); void resetAllToDefaults(); + /// Activate the first still-alive target registered under \a id + /// -- QAction::trigger() or QAbstractButton::click(), whichever + /// it turns out to be -- for an input source other than the + /// keyboard (e.g. a 3D mouse button) that wants to invoke a + /// named action without knowing or caring which of the two it + /// is. Deliberately not disambiguated by the currently active + /// window when an id has several live targets: 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; a known simplification in the rarer + /// multi-window case, not a guaranteed-correct dispatch. + /// @return whether a live target was found and triggered. + bool trigger(const QString &id) const; + private: ShortcutManager() = default; ShortcutManager(const ShortcutManager &) = delete; diff --git a/sources/spacemouse/spacemousebackend.h b/sources/spacemouse/spacemousebackend.h index 0eb294521..49578a349 100644 --- a/sources/spacemouse/spacemousebackend.h +++ b/sources/spacemouse/spacemousebackend.h @@ -66,6 +66,15 @@ class SpaceMouseBackend : public QObject /// scale/divisor constants are what turn them into pixels and a /// zoom factor, not this signal. void motion(int dx, int dy, int dz); + + /// One device button was pressed. \a button is whatever index + /// the backend's own driver numbers it as -- there is no + /// portable numbering across device models, which is exactly + /// why the binding from a button to a QET action + /// (SpaceMouseButtonMap) is user-configurable rather than + /// hardcoded. Emitted on press only; release is not reported, + /// since nothing here has a use for it. + void buttonPressed(int button); }; #endif // SPACEMOUSEBACKEND_H diff --git a/sources/spacemouse/spacemousebuttonmap.cpp b/sources/spacemouse/spacemousebuttonmap.cpp new file mode 100644 index 000000000..bb8b0e004 --- /dev/null +++ b/sources/spacemouse/spacemousebuttonmap.cpp @@ -0,0 +1,72 @@ +/* + 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 "spacemousebuttonmap.h" + +#include + +namespace { + const QString SETTINGS_GROUP = QStringLiteral("spacemouse/buttons/"); +} + +/** + @brief SpaceMouseButtonMap::actionId + @param button + @return see the declaration's doc comment +*/ +QString SpaceMouseButtonMap::actionId(int button) +{ + QSettings settings; + return settings.value(SETTINGS_GROUP + QString::number(button)).toString(); +} + +/** + @brief SpaceMouseButtonMap::setActionId + @param button + @param action_id +*/ +void SpaceMouseButtonMap::setActionId(int button, const QString &action_id) +{ + QSettings settings; + const QString key = SETTINGS_GROUP + QString::number(button); + if (action_id.isEmpty()) { + settings.remove(key); + } else { + settings.setValue(key, action_id); + } +} + +/** + @brief SpaceMouseButtonMap::allBindings + @return see the declaration's doc comment +*/ +QMap SpaceMouseButtonMap::allBindings() +{ + QMap bindings; + QSettings settings; + settings.beginGroup(QStringLiteral("spacemouse/buttons")); + for (const QString &key : settings.childKeys()) + { + bool ok = false; + const int button = key.toInt(&ok); + if (ok) { + bindings.insert(button, settings.value(key).toString()); + } + } + settings.endGroup(); + return bindings; +} diff --git a/sources/spacemouse/spacemousebuttonmap.h b/sources/spacemouse/spacemousebuttonmap.h new file mode 100644 index 000000000..ccfa0e9ec --- /dev/null +++ b/sources/spacemouse/spacemousebuttonmap.h @@ -0,0 +1,55 @@ +/* + 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 SPACEMOUSEBUTTONMAP_H +#define SPACEMOUSEBUTTONMAP_H + +#include +#include + +/** + @brief The SpaceMouseButtonMap class + Persisted mapping from a device button's raw index (backend- and + device-specific -- see SpaceMouseBackend::buttonPressed()) to a + ShortcutManager action id. Deliberately just a thin QSettings wrapper, + the same weight as ShortcutManager::savedSequence(): no caching, reads + and writes the setting directly on every call, since this is called at + most once per button press, never in a hot loop. + + Unbound by default for every button on every device: nothing is bound + until the user opens Configuration > 3D Mouse and binds something -- + see spacemouseconfigpage.h. A device compiled in and connected but + never configured here does nothing on any button press, matching the + rest of this feature's "silent until asked for" default. +*/ +class SpaceMouseButtonMap +{ + public: + /// @return the action id bound to \a button, or an empty string + /// if nothing is bound to it. + static QString actionId(int button); + + /// Bind \a button to \a action_id. An empty \a action_id + /// removes the binding (equivalent to it never having been set). + static void setActionId(int button, const QString &action_id); + + /// @return every currently bound button, for the configuration + /// page to list. + static QMap allBindings(); +}; + +#endif // SPACEMOUSEBUTTONMAP_H diff --git a/sources/spacemouse/spacemouselistener.cpp b/sources/spacemouse/spacemouselistener.cpp index 1752fd2bd..770cfd964 100644 --- a/sources/spacemouse/spacemouselistener.cpp +++ b/sources/spacemouse/spacemouselistener.cpp @@ -18,6 +18,7 @@ #include "spacemouselistener.h" #include "spacemousebackend.h" +#include "spacemousebuttonmap.h" #ifdef QET_SPACEMOUSE_BACKEND_SPNAV # include "spnavbackend.h" #endif @@ -25,6 +26,7 @@ #include "../diagramview.h" #include "../projectview.h" #include "../qetdiagrameditor.h" +#include "../shortcutmanager.h" #include #include @@ -68,6 +70,8 @@ SpaceMouseListener::SpaceMouseListener(QObject *parent) : if (m_backend) { connect(m_backend, &SpaceMouseBackend::motion, this, &SpaceMouseListener::applyMotion); + connect(m_backend, &SpaceMouseBackend::buttonPressed, + this, &SpaceMouseListener::applyButton); } } @@ -135,3 +139,15 @@ void SpaceMouseListener::applyMotion(int dx, int dy, int dz) view->zoom(zoomFactorForZAxis(dz)); } } + +/** + @brief SpaceMouseListener::applyButton + @param button +*/ +void SpaceMouseListener::applyButton(int button) +{ + const QString action_id = SpaceMouseButtonMap::actionId(button); + if (!action_id.isEmpty()) { + ShortcutManager::instance().trigger(action_id); + } +} diff --git a/sources/spacemouse/spacemouselistener.h b/sources/spacemouse/spacemouselistener.h index d91cc213d..8ef30da82 100644 --- a/sources/spacemouse/spacemouselistener.h +++ b/sources/spacemouse/spacemouselistener.h @@ -28,13 +28,18 @@ class SpaceMouseBackend; bridges a 3Dconnexion SpaceMouse/SpacePilot 6-DOF device to DiagramView's existing pan/zoom primitives (the same horizontalScrollBar()/verticalScrollBar()/zoom() calls - DiagramView::wheelEvent() already uses for a physical wheel). + DiagramView::wheelEvent() already uses for a physical wheel), and its + buttons to named QET actions via ShortcutManager -- the same registry + keyboard shortcuts already use, so a device button can trigger anything + in that registry (undo, redo, rotate selection, ...) without QET having + a second, device-specific action list. Everything here is platform-independent: which DiagramView to apply - motion to, the pan/zoom calls, and the Z-to-zoom-factor mapping. - Talking to the actual device driver is a SpaceMouseBackend's job (see - its class comment) -- this class owns one and applies whatever it - reports, without knowing or caring which platform it came from. + motion to, the pan/zoom calls, the Z-to-zoom-factor mapping, and button + dispatch via SpaceMouseButtonMap + ShortcutManager. Talking to the + actual device driver is a SpaceMouseBackend's job (see its class + comment) -- this class owns one and applies whatever it reports, + without knowing or caring which platform it came from. Only compiled in when QET_SPACEMOUSE_SUPPORT is defined. Even then, constructing one is always safe: if no backend is available for this @@ -71,6 +76,12 @@ class SpaceMouseListener : public QObject /// -- to whichever DiagramView is currently active. void applyMotion(int dx, int dy, int dz); + /// Look up which action id, if any, SpaceMouseButtonMap binds + /// \a button to, and trigger it via ShortcutManager. Does + /// nothing for an unbound button -- see SpaceMouseButtonMap's + /// class comment on the "unbound by default" contract. + void applyButton(int button); + private: SpaceMouseBackend *m_backend = nullptr; }; diff --git a/sources/spacemouse/spnavbackend.cpp b/sources/spacemouse/spnavbackend.cpp index f27d75107..3fc7e6912 100644 --- a/sources/spacemouse/spnavbackend.cpp +++ b/sources/spacemouse/spnavbackend.cpp @@ -70,9 +70,11 @@ void SpnavBackend::readEvents() //read by nothing here. emit motion(event.motion.x, event.motion.y, event.motion.z); } - //Button events (SPNAV_EVENT_BUTTON) are deliberately not - //handled: mapping device buttons to QET actions is the "Related, - //not proposed here" follow-up in discussion #599, not this - //phase. + else if (event.type == SPNAV_EVENT_BUTTON && event.button.press) { + //Release (press == 0) is not reported -- SpaceMouseListener + //triggers a QET action on press, the same way a keyboard + //shortcut triggers on key-down, and has no use for release. + emit buttonPressed(event.button.bnum); + } } } diff --git a/sources/ui/configpage/spacemouseconfigpage.cpp b/sources/ui/configpage/spacemouseconfigpage.cpp new file mode 100644 index 000000000..8f2841c10 --- /dev/null +++ b/sources/ui/configpage/spacemouseconfigpage.cpp @@ -0,0 +1,223 @@ +/* + 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 "spacemouseconfigpage.h" + +#include "../../qeticons.h" +#include "../../shortcutmanager.h" +#include "../../spacemouse/spacemousebuttonmap.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + constexpr int COL_BUTTON = 0; + constexpr int COL_ACTION = 1; + constexpr int COL_REMOVE = 2; + + //The combo box's first entry: no action bound, i.e. this row does + //nothing. Real action ids are never empty (ShortcutManager assigns + //them from source, not user input), so an empty string can never + //collide with a real one. + const QString UNBOUND_LABEL = QObject::tr("(Aucune action)", "spacemouse config: unbound button"); +} + +/** + @brief SpaceMouseConfigPage::SpaceMouseConfigPage + @param parent +*/ +SpaceMouseConfigPage::SpaceMouseConfigPage(QWidget *parent) : + ConfigPage(parent) +{ + auto *vlayout = new QVBoxLayout(); + + QLabel *title_label = new QLabel(this->title()); + vlayout->addWidget(title_label); + + QFrame *horiz_line = new QFrame(); + horiz_line->setFrameShape(QFrame::HLine); + vlayout->addWidget(horiz_line); + + auto *intro_label = new QLabel( + tr("Associez un numéro de bouton de votre souris 3D (SpaceMouse, SpacePilot...) " + "à une action de QElectroTech. Le numéro de bouton dépend de votre appareil " + "et de son pilote -- reportez-vous à sa documentation, ou essayez successivement " + "les valeurs à partir de 0.", + "spacemouse config page intro")); + intro_label->setWordWrap(true); + vlayout->addWidget(intro_label); + + m_table = new QTableWidget(0, 3, this); + m_table->setHorizontalHeaderLabels({tr("N° bouton"), tr("Action"), QString()}); + m_table->horizontalHeader()->setSectionResizeMode(COL_BUTTON, QHeaderView::ResizeToContents); + m_table->horizontalHeader()->setSectionResizeMode(COL_ACTION, QHeaderView::Stretch); + m_table->horizontalHeader()->setSectionResizeMode(COL_REMOVE, QHeaderView::ResizeToContents); + m_table->verticalHeader()->setVisible(false); + m_table->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_table->setSelectionMode(QAbstractItemView::NoSelection); + vlayout->addWidget(m_table); + + auto *add_button = new QPushButton(QET::Icons::Add, tr("Ajouter une association"), this); + connect(add_button, &QPushButton::clicked, this, &SpaceMouseConfigPage::addRow); + + auto *bottom_layout = new QHBoxLayout(); + bottom_layout->addWidget(add_button); + bottom_layout->addStretch(); + vlayout->addLayout(bottom_layout); + + setLayout(vlayout); + + populateTable(); +} + +SpaceMouseConfigPage::~SpaceMouseConfigPage() +{ +} + +/** + @brief SpaceMouseConfigPage::populateTable + Fill the table with one row per binding currently persisted in + SpaceMouseButtonMap. +*/ +void SpaceMouseConfigPage::populateTable() +{ + const QMap bindings = SpaceMouseButtonMap::allBindings(); + for (auto it = bindings.constBegin(); it != bindings.constEnd(); ++it) { + appendRow(it.key(), it.value()); + } +} + +/** + @brief SpaceMouseConfigPage::appendRow + Add one row bound to \a button / \a action_id (either or both may be + the "unset" value -- 0 and an empty string respectively -- for a fresh + row added via the "+" button). + @param button + @param action_id +*/ +void SpaceMouseConfigPage::appendRow(int button, const QString &action_id) +{ + const int row = m_table->rowCount(); + m_table->setRowCount(row + 1); + + auto *button_spin = new QSpinBox(m_table); + button_spin->setRange(0, 999); + button_spin->setValue(button); + m_table->setCellWidget(row, COL_BUTTON, button_spin); + + auto *action_combo = new QComboBox(m_table); + action_combo->addItem(UNBOUND_LABEL, QString()); + + QList shortcuts = ShortcutManager::instance().allShortcuts(); + std::sort(shortcuts.begin(), shortcuts.end(), + [](const ShortcutManager::ShortcutInfo &a, const ShortcutManager::ShortcutInfo &b) { + if (a.category != b.category) { + return a.category < b.category; + } + return a.description < b.description; + }); + for (const ShortcutManager::ShortcutInfo &info : shortcuts) { + action_combo->addItem( + QStringLiteral("%1 — %2").arg(info.category, info.description), info.id); + } + + int index_for_current = action_combo->findData(action_id); + action_combo->setCurrentIndex(index_for_current >= 0 ? index_for_current : 0); + m_table->setCellWidget(row, COL_ACTION, action_combo); + + auto *remove_button = new QToolButton(m_table); + remove_button->setIcon(QET::Icons::EditTableDeleteRow); + remove_button->setToolTip(tr("Supprimer cette association")); + connect(remove_button, &QToolButton::clicked, this, &SpaceMouseConfigPage::removeSelectedRow); + m_table->setCellWidget(row, COL_REMOVE, remove_button); +} + +/** + @brief SpaceMouseConfigPage::addRow + Add a fresh, unbound row for the user to fill in. +*/ +void SpaceMouseConfigPage::addRow() +{ + appendRow(0, QString()); +} + +/** + @brief SpaceMouseConfigPage::removeSelectedRow + Remove whichever row owns the remove button that was clicked. Found by + looking the sender up in the button column rather than tracked + per-button state, so nothing has to be kept in sync as rows are added + and removed around it. +*/ +void SpaceMouseConfigPage::removeSelectedRow() +{ + auto *button = qobject_cast(sender()); + if (!button) { + return; + } + for (int row = 0; row < m_table->rowCount(); ++row) { + if (m_table->cellWidget(row, COL_REMOVE) == button) { + m_table->removeRow(row); + return; + } + } +} + +/** + @brief SpaceMouseConfigPage::applyConf + Persist exactly what the table currently shows: every previously saved + binding is cleared first, then every row still in the table (after any + adds/edits/removals) is written back. Simpler and less error-prone than + tracking which individual rows changed, and cheap -- this runs once, + when the user validates the surrounding ConfigDialog, not on every + keystroke. +*/ +void SpaceMouseConfigPage::applyConf() +{ + const QMap previous = SpaceMouseButtonMap::allBindings(); + for (auto it = previous.constBegin(); it != previous.constEnd(); ++it) { + SpaceMouseButtonMap::setActionId(it.key(), QString()); + } + + for (int row = 0; row < m_table->rowCount(); ++row) + { + auto *button_spin = qobject_cast(m_table->cellWidget(row, COL_BUTTON)); + auto *action_combo = qobject_cast(m_table->cellWidget(row, COL_ACTION)); + if (!button_spin || !action_combo) { + continue; + } + SpaceMouseButtonMap::setActionId(button_spin->value(), action_combo->currentData().toString()); + } +} + +QString SpaceMouseConfigPage::title() const +{ + return tr("Souris 3D", "configuration page title"); +} + +QIcon SpaceMouseConfigPage::icon() const +{ + return QET::Icons::ConfigureToolbars; +} diff --git a/sources/ui/configpage/spacemouseconfigpage.h b/sources/ui/configpage/spacemouseconfigpage.h new file mode 100644 index 000000000..e8d6ec0ae --- /dev/null +++ b/sources/ui/configpage/spacemouseconfigpage.h @@ -0,0 +1,69 @@ +/* + 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 SPACEMOUSECONFIGPAGE_H +#define SPACEMOUSECONFIGPAGE_H + +#include "configpage.h" + +class QTableWidget; + +/** + @brief The SpaceMouseConfigPage class + Configuration page listing every device-button-to-action binding + (SpaceMouseButtonMap), letting the user add, edit and remove them. + Modelled directly on ShortcutsConfigPage, one row per binding instead + of one row per shortcut, since the two are close cousins: both end in + the same place -- ShortcutManager::trigger()/registerAction() -- just + from a different input source. Bindings are only persisted (via + SpaceMouseButtonMap::setActionId()) when applyConf() runs, i.e. when + the user validates the surrounding ConfigDialog. + + Only compiled in, and only added to the Configuration dialog, when + QET_SPACEMOUSE_SUPPORT is defined -- see qetapp.cpp. + + Button numbering is intentionally not fixed to any particular device: + it is exactly whatever integer SpaceMouseBackend::buttonPressed() + reports for the button pressed, which is backend- and device-specific. + Rows are added/removed freely rather than the page assuming a button + count, since that varies from 2 (SpaceMouse Compact) to over 30 + (SpacePilot Pro). +*/ +class SpaceMouseConfigPage : public ConfigPage +{ + Q_OBJECT + + public: + explicit SpaceMouseConfigPage(QWidget *parent = nullptr); + ~SpaceMouseConfigPage() override; + + void applyConf() override; + QString title() const override; + QIcon icon() const override; + + private slots: + void addRow(); + void removeSelectedRow(); + + private: + void populateTable(); + void appendRow(int button, const QString &action_id); + + QTableWidget *m_table; +}; + +#endif // SPACEMOUSECONFIGPAGE_H