diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b9ab0212..1923f6f36 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,9 +146,11 @@ else() ) endif() -if(QET_SPACEMOUSE_ENABLED) - set(QET_SPACEMOUSE_LIBRARIES PkgConfig::SPNAV) +if(QET_SPACEMOUSE_BACKEND_SPNAV_ENABLED) + list(APPEND QET_SPACEMOUSE_LIBRARIES PkgConfig::SPNAV) endif() +# A future Windows/macOS (3DxWare) backend would list.APPEND its own SDK +# libraries here, behind its own *_ENABLED guard, alongside this one. target_link_libraries( ${PROJECT_NAME} diff --git a/cmake/find_spacemouse.cmake b/cmake/find_spacemouse.cmake index 8275b50aa..6f47ae803 100644 --- a/cmake/find_spacemouse.cmake +++ b/cmake/find_spacemouse.cmake @@ -20,28 +20,44 @@ message(" - find_spacemouse") # so none of this runs, and the default build is entirely unaffected: no new # dependency, no new source files, no new symbols. # -# When it is on, libspnav is looked for via its pkg-config file (present on -# every distro package of it this was checked against: Debian/Ubuntu's -# libspnav-dev ships /usr/share/pkgconfig/spnav.pc). If it isn't found, this -# does not hard-fail the whole configure -- it downgrades the option back to -# off with a clear message, so a developer who doesn't have the library -# installed still gets a normal build instead of a configure error for an -# opt-in feature they didn't ask to block on. +# When it is on, the platform decides which backend (if any) is available -- +# see sources/spacemouse/spacemousebackend.h for what a backend is. Only one +# exists today: SpnavBackend, Linux, via libspnav. A Windows/macOS backend +# would need 3Dconnexion's proprietary 3DxWare SDK and is not implemented +# (see discussion #599's own phase split); until it is, turning this option +# on there downgrades cleanly with a warning rather than failing configure. set(QET_SPACEMOUSE_ENABLED FALSE) +set(QET_SPACEMOUSE_BACKEND_SPNAV_ENABLED FALSE) if(QET_ENABLE_SPACEMOUSE) - find_package(PkgConfig) - if(PkgConfig_FOUND) - pkg_check_modules(SPNAV IMPORTED_TARGET spnav) - endif() + if(UNIX AND NOT APPLE) + # libspnav is looked for via its pkg-config file (present on every + # distro package of it this was checked against: Debian/Ubuntu's + # libspnav-dev ships /usr/share/pkgconfig/spnav.pc). Not found does + # not hard-fail the whole configure -- it downgrades the option back + # to off with a clear message, so a developer who doesn't have the + # library installed still gets a normal build instead of a configure + # error for an opt-in feature they didn't ask to block on. + find_package(PkgConfig) + if(PkgConfig_FOUND) + pkg_check_modules(SPNAV IMPORTED_TARGET spnav) + endif() - if(SPNAV_FOUND) - set(QET_SPACEMOUSE_ENABLED TRUE) - add_definitions(-DQET_SPACEMOUSE_SUPPORT) - message("QET_ENABLE_SPACEMOUSE ON (libspnav ${SPNAV_VERSION} found)") + if(SPNAV_FOUND) + set(QET_SPACEMOUSE_ENABLED TRUE) + set(QET_SPACEMOUSE_BACKEND_SPNAV_ENABLED TRUE) + add_definitions(-DQET_SPACEMOUSE_SUPPORT) + add_definitions(-DQET_SPACEMOUSE_BACKEND_SPNAV) + message("QET_ENABLE_SPACEMOUSE ON (backend: libspnav ${SPNAV_VERSION})") + else() + message(WARNING "QET_ENABLE_SPACEMOUSE is ON but libspnav was not found via pkg-config " + "(install libspnav-dev, or the equivalent for your distribution) -- " + "building WITHOUT 3D mouse support.") + endif() else() - message(WARNING "QET_ENABLE_SPACEMOUSE is ON but libspnav was not found via pkg-config " - "(install libspnav-dev, or the equivalent for your distribution) -- " - "building WITHOUT 3D mouse support.") + message(WARNING "QET_ENABLE_SPACEMOUSE is ON but no 3D mouse backend is implemented yet " + "for this platform (only Linux/libspnav exists today -- see " + "https://github.com/qelectrotech/qelectrotech-source-mirror/discussions/599) " + "-- building WITHOUT 3D mouse support.") endif() endif() diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 7f3ec60c3..17d01d290 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -796,11 +796,19 @@ endif() if(QET_SPACEMOUSE_ENABLED) list(APPEND QET_SRC_FILES + ${QET_DIR}/sources/spacemouse/spacemousebackend.h ${QET_DIR}/sources/spacemouse/spacemouselistener.cpp ${QET_DIR}/sources/spacemouse/spacemouselistener.h ) endif() +if(QET_SPACEMOUSE_BACKEND_SPNAV_ENABLED) + list(APPEND QET_SRC_FILES + ${QET_DIR}/sources/spacemouse/spnavbackend.cpp + ${QET_DIR}/sources/spacemouse/spnavbackend.h + ) +endif() + set(TS_FILES ${QET_DIR}/lang/qet_ar.ts ${QET_DIR}/lang/qet_ca.ts diff --git a/sources/spacemouse/spacemousebackend.h b/sources/spacemouse/spacemousebackend.h new file mode 100644 index 000000000..0eb294521 --- /dev/null +++ b/sources/spacemouse/spacemousebackend.h @@ -0,0 +1,71 @@ +/* + 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 SPACEMOUSEBACKEND_H +#define SPACEMOUSEBACKEND_H + +#include + +/** + @brief The SpaceMouseBackend class + Platform seam for discussion #599's 3D mouse support. A backend owns + one platform's connection to the actual 6-DOF device driver -- opening + it, pumping whatever event source that platform uses, closing it -- and + reports motion through the one signal below. Everything platform- + independent (which DiagramView to apply motion to, the pan/zoom + primitives to call, the Z-to-zoom-factor mapping) lives in + SpaceMouseListener instead, once, so it does not have to be duplicated + or re-verified per backend. + + SpnavBackend (Linux, spacenavd/libspnav) is the only implementation so + far -- built, linked, and its "no daemon/device present" path actually + run in the environment this was written in. A Windows/macOS backend + (3Dconnexion's proprietary 3DxWare SDK) would implement this same + interface and be selected in SpaceMouseListener's constructor, without + changing SpnavBackend or anything downstream of the motion signal. + Deliberately not attempted here: this was written on Linux with no + 3DxWare SDK and no Windows/macOS toolchain available to compile, + link, or run a single line of it against, and shipping platform code + that has never even built would be a materially different, weaker + thing than everything else in this feature. +*/ +class SpaceMouseBackend : public QObject +{ + Q_OBJECT + + public: + explicit SpaceMouseBackend(QObject *parent = nullptr) : QObject(parent) {} + ~SpaceMouseBackend() override = default; + + /// True once this backend actually has a live connection to a + /// driver/daemon. False is the ordinary case -- no daemon + /// running, no device attached -- not an error; see + /// SpaceMouseListener's class comment for why that distinction + /// matters. + virtual bool isAvailable() const = 0; + + signals: + /// One raw device sample. dx/dy are the two axes + /// SpaceMouseListener maps to horizontal/vertical pan, dz the + /// one it maps to zoom. Units and range are whatever the + /// backend's own driver reports -- SpaceMouseListener's own + /// scale/divisor constants are what turn them into pixels and a + /// zoom factor, not this signal. + void motion(int dx, int dy, int dz); +}; + +#endif // SPACEMOUSEBACKEND_H diff --git a/sources/spacemouse/spacemouselistener.cpp b/sources/spacemouse/spacemouselistener.cpp index e66df5338..1752fd2bd 100644 --- a/sources/spacemouse/spacemouselistener.cpp +++ b/sources/spacemouse/spacemouselistener.cpp @@ -17,24 +17,27 @@ */ #include "spacemouselistener.h" +#include "spacemousebackend.h" +#ifdef QET_SPACEMOUSE_BACKEND_SPNAV +# include "spnavbackend.h" +#endif + #include "../diagramview.h" #include "../projectview.h" #include "../qetdiagrameditor.h" #include #include -#include - -#include namespace { - //A spnav motion delta is an integer on roughly the same order of + //A device motion delta is an integer on roughly the same order of //magnitude as a QWheelEvent::angleDelta() tick (about +-120 per //detent, more under a hard push/twist -- exact range depends on the - //user's spacenavd sensitivity setting, which is configured once - //outside QET and out of scope here). DiagramView::wheelEvent() - //already turns such a tick into a small per-event zoom step via - //zoom(1 + value/1000); reused as a starting point. + //backend and the user's own driver-level sensitivity setting, which + //is configured outside QET and out of scope here). + //DiagramView::wheelEvent() already turns such a tick into a small + //per-event zoom step via zoom(1 + value/1000); reused as a starting + //point. // //Neither this divisor nor PAN_SCALE below has been calibrated //against real hardware -- there is none in the environment this was @@ -46,38 +49,40 @@ namespace { /** @brief SpaceMouseListener::SpaceMouseListener - Try to connect to spacenavd. Failure -- no daemon running, no device - attached -- is left silent: it is the expected state for most users and - must never surface as an error dialog or a log warning on every - ordinary startup. + Construct whichever backend is available for this platform and connect + its motion() signal. If none is compiled in, or the one that is can't + reach a device, isAvailable() simply stays false -- see the class + comment. @param parent */ SpaceMouseListener::SpaceMouseListener(QObject *parent) : QObject(parent) { - if (spnav_open() == -1) { - return; - } +#ifdef QET_SPACEMOUSE_BACKEND_SPNAV + m_backend = new SpnavBackend(this); +#endif + //A future Windows/macOS backend (3Dconnexion's proprietary 3DxWare + //SDK) slots in here behind its own QET_SPACEMOUSE_BACKEND_3DXWARE + //guard, without changing anything below this constructor. - m_available = true; - m_notifier = new QSocketNotifier(spnav_fd(), QSocketNotifier::Read, this); - connect(m_notifier, &QSocketNotifier::activated, - this, &SpaceMouseListener::readEvents); + if (m_backend) { + connect(m_backend, &SpaceMouseBackend::motion, + this, &SpaceMouseListener::applyMotion); + } } /** - @brief SpaceMouseListener::~SpaceMouseListener + @brief SpaceMouseListener::isAvailable + @return whether the platform backend has a live device connection */ -SpaceMouseListener::~SpaceMouseListener() +bool SpaceMouseListener::isAvailable() const { - if (m_available) { - spnav_close(); - } + return m_backend && m_backend->isAvailable(); } /** @brief SpaceMouseListener::zoomFactorForZAxis - @param z : raw Z-axis (push/pull) delta from a spnav motion event + @param z : raw Z-axis (push/pull) delta from a device motion sample @return the multiplicative factor DiagramView::zoom() expects */ qreal SpaceMouseListener::zoomFactorForZAxis(int z) @@ -86,45 +91,22 @@ qreal SpaceMouseListener::zoomFactorForZAxis(int z) } /** - @brief SpaceMouseListener::readEvents - Called when the spacenavd socket has data available. Drains every event - currently queued -- spnav_poll_event() returns 0 once the queue is - empty -- rather than handling just one per activation, so events cannot - silently back up if several arrive between two Qt event loop turns. -*/ -void SpaceMouseListener::readEvents() -{ - spnav_event event; - while (spnav_poll_event(&event)) - { - if (event.type == SPNAV_EVENT_MOTION) { - dispatchMotion(event.motion); - } - //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. - } -} - -/** - @brief SpaceMouseListener::dispatchMotion - Apply one motion event to whichever DiagramView is currently active. + @brief SpaceMouseListener::applyMotion + Apply one motion sample to whichever DiagramView is currently active. X/Y translation pans it, Z translation zooms it -- the same two primitives (scrollbars, DiagramView::zoom()) DiagramView::wheelEvent() already drives from a physical wheel, so there is no new navigation logic here, only a new input source feeding the existing one. - A 6-DOF device also reports rotation (rx, ry, rz); QET's view has - nothing rotation maps to, so those three axes are read by nothing here. - - Which of the three translation axes is "left/right" vs "forward/back" - vs "up/down" on the physical device, and their sign, is a hardware - convention this could not be checked against real hardware while - writing it -- see the PR description. - @param motion + Which of a device's three translation axes is "left/right" vs + "forward/back" vs "up/down", and their sign, is a hardware convention + this could not be checked against real hardware while writing it -- see + the PR description. + @param dx + @param dy + @param dz */ -void SpaceMouseListener::dispatchMotion(const spnav_event_motion &motion) +void SpaceMouseListener::applyMotion(int dx, int dy, int dz) { auto *editor = qobject_cast(qApp->activeWindow()); if (!editor) { @@ -141,15 +123,15 @@ void SpaceMouseListener::dispatchMotion(const spnav_event_motion &motion) return; } - if (motion.x || motion.y) + if (dx || dy) { view->horizontalScrollBar()->setValue( - view->horizontalScrollBar()->value() - qRound(motion.x * PAN_SCALE)); + view->horizontalScrollBar()->value() - qRound(dx * PAN_SCALE)); view->verticalScrollBar()->setValue( - view->verticalScrollBar()->value() - qRound(motion.y * PAN_SCALE)); + view->verticalScrollBar()->value() - qRound(dy * PAN_SCALE)); } - if (motion.z) { - view->zoom(zoomFactorForZAxis(motion.z)); + if (dz) { + view->zoom(zoomFactorForZAxis(dz)); } } diff --git a/sources/spacemouse/spacemouselistener.h b/sources/spacemouse/spacemouselistener.h index 5424a07f9..d91cc213d 100644 --- a/sources/spacemouse/spacemouselistener.h +++ b/sources/spacemouse/spacemouselistener.h @@ -20,27 +20,31 @@ #include -class QSocketNotifier; -struct spnav_event_motion; +class SpaceMouseBackend; /** @brief The SpaceMouseListener class - Phase 1 (Linux, libspnav) of https://github.com/qelectrotech/qelectrotech-source-mirror/discussions/599 : - bridges a 3Dconnexion SpaceMouse/SpacePilot 6-DOF device, via the - spacenavd daemon and libspnav, to DiagramView's existing pan/zoom - primitives (the same horizontalScrollBar()/verticalScrollBar()/zoom() - calls DiagramView::wheelEvent() already uses for a physical wheel). + 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). - Only compiled in when the QET_ENABLE_SPACEMOUSE CMake option is on. Even - then, constructing one is always safe: when no spacenavd is running or - no device is attached -- the expected state for the overwhelming - majority of users, even of a build with the option on -- it silently - does nothing rather than failing or nagging the user. There is exactly - one of these, owned by QETApp, because a physical 6-DOF device is a - single ambient input source for the whole application, not something - tied to one window; motion is applied to whichever DiagramView is - currently active (see targetView()). + 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. + + Only compiled in when QET_SPACEMOUSE_SUPPORT is defined. Even then, + constructing one is always safe: if no backend is available for this + platform, or the one that exists can't reach a driver/daemon (no + device attached -- the expected state for the overwhelming majority of + users, even of a build with the option on), this silently does nothing + rather than failing or nagging the user. There is exactly one of + these, owned by QETApp, because a physical 6-DOF device is a single + ambient input source for the whole application, not something tied to + one window. */ class SpaceMouseListener : public QObject { @@ -48,32 +52,27 @@ class SpaceMouseListener : public QObject public: explicit SpaceMouseListener(QObject *parent = nullptr); - ~SpaceMouseListener() override; + ~SpaceMouseListener() override = default; - /// True once a connection to spacenavd was established. - /// False is the common case, not an error -- see the class - /// comment -- so callers should not warn the user when this - /// is false. - bool isAvailable() const {return m_available;} + /// True once the platform backend has a live connection to a + /// driver/daemon. False is the common case, not an error -- see + /// the class comment -- so callers should not warn the user + /// when this is false. + bool isAvailable() const; /// Pure translation from a device Z-axis delta to the /// multiplicative factor DiagramView::zoom() expects. A free /// function so the mapping can be unit-tested without a live - /// spacenavd connection or a real device. + /// backend connection or a real device. static qreal zoomFactorForZAxis(int z); private slots: - /// Drain and dispatch every event currently queued on the - /// spacenavd socket. Connected to a QSocketNotifier on that - /// socket's fd rather than polled on a timer, so this is - /// idle-cost-free between events. - void readEvents(); + /// Apply one motion sample -- from whichever backend is in use + /// -- to whichever DiagramView is currently active. + void applyMotion(int dx, int dy, int dz); private: - void dispatchMotion(const spnav_event_motion &motion); - - bool m_available = false; - QSocketNotifier *m_notifier = nullptr; + SpaceMouseBackend *m_backend = nullptr; }; #endif // SPACEMOUSELISTENER_H diff --git a/sources/spacemouse/spnavbackend.cpp b/sources/spacemouse/spnavbackend.cpp new file mode 100644 index 000000000..f27d75107 --- /dev/null +++ b/sources/spacemouse/spnavbackend.cpp @@ -0,0 +1,78 @@ +/* + 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 "spnavbackend.h" + +#include + +#include + +/** + @brief SpnavBackend::SpnavBackend + Try to connect to spacenavd. Failure -- no daemon running, no device + attached -- is left silent: it is the expected state for most users and + must never surface as an error dialog or a log warning on every + ordinary startup. + @param parent +*/ +SpnavBackend::SpnavBackend(QObject *parent) : + SpaceMouseBackend(parent) +{ + if (spnav_open() == -1) { + return; + } + + m_available = true; + m_notifier = new QSocketNotifier(spnav_fd(), QSocketNotifier::Read, this); + connect(m_notifier, &QSocketNotifier::activated, + this, &SpnavBackend::readEvents); +} + +/** + @brief SpnavBackend::~SpnavBackend +*/ +SpnavBackend::~SpnavBackend() +{ + if (m_available) { + spnav_close(); + } +} + +/** + @brief SpnavBackend::readEvents + Called when the spacenavd socket has data available. Drains every event + currently queued -- spnav_poll_event() returns 0 once the queue is + empty -- rather than handling just one per activation, so events cannot + silently back up if several arrive between two Qt event loop turns. +*/ +void SpnavBackend::readEvents() +{ + spnav_event event; + while (spnav_poll_event(&event)) + { + if (event.type == SPNAV_EVENT_MOTION) { + //A 6-DOF device also reports rotation (rx, ry, rz); QET's + //view has nothing rotation maps to, so those three axes are + //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. + } +} diff --git a/sources/spacemouse/spnavbackend.h b/sources/spacemouse/spnavbackend.h new file mode 100644 index 000000000..76c85f63f --- /dev/null +++ b/sources/spacemouse/spnavbackend.h @@ -0,0 +1,58 @@ +/* + 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 SPNAVBACKEND_H +#define SPNAVBACKEND_H + +#include "spacemousebackend.h" + +class QSocketNotifier; + +/** + @brief The SpnavBackend class + Linux SpaceMouseBackend, via the spacenavd daemon and libspnav. Only + compiled in when QET_SPACEMOUSE_BACKEND_SPNAV is defined (set by + cmake/find_spacemouse.cmake once it has actually found libspnav). + + Connecting is always safe even when no daemon is running or no device + is attached -- the expected state for the overwhelming majority of + users, even of a build with 3D mouse support compiled in -- this + silently leaves isAvailable() false rather than failing loudly. +*/ +class SpnavBackend : public SpaceMouseBackend +{ + Q_OBJECT + + public: + explicit SpnavBackend(QObject *parent = nullptr); + ~SpnavBackend() override; + + bool isAvailable() const override {return m_available;} + + private slots: + /// Drain and emit every event currently queued on the spacenavd + /// socket. Connected to a QSocketNotifier on that socket's fd + /// rather than polled on a timer, so this is idle-cost-free + /// between events. + void readEvents(); + + private: + bool m_available = false; + QSocketNotifier *m_notifier = nullptr; +}; + +#endif // SPNAVBACKEND_H