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