diff --git a/CMakeLists.txt b/CMakeLists.txt
index 537c68ddd..5b9ab0212 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -63,6 +63,7 @@ include(cmake/git_last_commit_sha.cmake)
include(cmake/fetch_kdeaddons.cmake)
include(cmake/fetch_singleapplication.cmake)
include(cmake/fetch_pugixml.cmake)
+include(cmake/find_spacemouse.cmake)
include(cmake/qet_compilation_vars.cmake)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
@@ -145,6 +146,10 @@ else()
)
endif()
+if(QET_SPACEMOUSE_ENABLED)
+ set(QET_SPACEMOUSE_LIBRARIES PkgConfig::SPNAV)
+endif()
+
target_link_libraries(
${PROJECT_NAME}
PUBLIC
@@ -154,6 +159,7 @@ target_link_libraries(
SQLite3::SQLite3
${KF5_PRIVATE_LIBRARIES}
${QET_PRIVATE_LIBRARIES}
+ ${QET_SPACEMOUSE_LIBRARIES}
)
target_include_directories(
diff --git a/cmake/developer_options.cmake b/cmake/developer_options.cmake
index 04f002da4..e1769aaae 100644
--- a/cmake/developer_options.cmake
+++ b/cmake/developer_options.cmake
@@ -33,3 +33,8 @@ add_definitions(-DQT_MESSAGELOGCONTEXT)
# Build with KF5
option(BUILD_WITH_KF5 "Build with KF5" ON)
+
+# Phase 1 (Linux, libspnav) of discussion #599: 3Dconnexion SpaceMouse/
+# SpacePilot pan/zoom support. Off by default -- see cmake/find_spacemouse.cmake
+# for what happens when it is on but libspnav isn't found.
+option(QET_ENABLE_SPACEMOUSE "Build with 3Dconnexion/libspnav 3D mouse support for pan/zoom (Linux, requires libspnav-dev and spacenavd)" OFF)
diff --git a/cmake/find_spacemouse.cmake b/cmake/find_spacemouse.cmake
new file mode 100644
index 000000000..8275b50aa
--- /dev/null
+++ b/cmake/find_spacemouse.cmake
@@ -0,0 +1,47 @@
+# Copyright 2006 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 .
+
+message(" - find_spacemouse")
+
+# QET_ENABLE_SPACEMOUSE (cmake/developer_options.cmake) is off by default,
+# 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.
+set(QET_SPACEMOUSE_ENABLED FALSE)
+
+if(QET_ENABLE_SPACEMOUSE)
+ 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)")
+ 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()
+endif()
diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 57a012916..7f3ec60c3 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -794,6 +794,13 @@ if(NOT BUILD_WITH_KF5)
)
endif()
+if(QET_SPACEMOUSE_ENABLED)
+ list(APPEND QET_SRC_FILES
+ ${QET_DIR}/sources/spacemouse/spacemouselistener.cpp
+ ${QET_DIR}/sources/spacemouse/spacemouselistener.h
+ )
+endif()
+
set(TS_FILES
${QET_DIR}/lang/qet_ar.ts
${QET_DIR}/lang/qet_ca.ts
diff --git a/sources/qetapp.cpp b/sources/qetapp.cpp
index 8545f99e7..5004f1dfe 100644
--- a/sources/qetapp.cpp
+++ b/sources/qetapp.cpp
@@ -40,6 +40,9 @@
#include "machine_info.h"
#include "TerminalStrip/ui/terminalstripeditorwindow.h"
#include "qetversion.h"
+#ifdef QET_SPACEMOUSE_SUPPORT
+# include "spacemouse/spacemouselistener.h"
+#endif
#include
#include
@@ -156,6 +159,14 @@ QETApp::QETApp() :
}
checkBackupFiles();
+
+#ifdef QET_SPACEMOUSE_SUPPORT
+ //Always safe to construct: it silently does nothing when spacenavd
+ //isn't running or no device is attached, which is the common case
+ //even in a build with this feature compiled in. See
+ //SpaceMouseListener's class comment.
+ m_space_mouse_listener = new SpaceMouseListener(this);
+#endif
}
/**
diff --git a/sources/qetapp.h b/sources/qetapp.h
index 512fff961..6ef548883 100644
--- a/sources/qetapp.h
+++ b/sources/qetapp.h
@@ -47,6 +47,9 @@ class QETProject;
class QETTitleBlockTemplateEditor;
class QTextOrientationSpinBoxWidget;
class RecentFiles;
+#ifdef QET_SPACEMOUSE_SUPPORT
+class SpaceMouseListener;
+#endif
/**
@brief The QETApp class
@@ -225,6 +228,13 @@ class QETApp : public QObject
static TitleBlockTemplatesFilesCollection *m_company_tbt_collection;
static TitleBlockTemplatesFilesCollection *m_custom_tbt_collection;
static ElementsCollectionCache *collections_cache_;
+#ifdef QET_SPACEMOUSE_SUPPORT
+ /// One per application, not per window: a physical 6-DOF device
+ /// is a single ambient input source, and motion is applied to
+ /// whichever DiagramView is currently active. See
+ /// SpaceMouseListener's class comment.
+ SpaceMouseListener *m_space_mouse_listener = nullptr;
+#endif
static QMap registered_projects_;
static uint next_project_id;
static RecentFiles *m_projects_recent_files;
diff --git a/sources/spacemouse/spacemouselistener.cpp b/sources/spacemouse/spacemouselistener.cpp
new file mode 100644
index 000000000..e66df5338
--- /dev/null
+++ b/sources/spacemouse/spacemouselistener.cpp
@@ -0,0 +1,155 @@
+/*
+ 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 "spacemouselistener.h"
+
+#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
+ //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.
+ //
+ //Neither this divisor nor PAN_SCALE below has been calibrated
+ //against real hardware -- there is none in the environment this was
+ //built in. Both are named constants specifically so that is a
+ //one-line fix once someone with a device tries it.
+ constexpr qreal ZOOM_DIVISOR = 1000.0;
+ constexpr qreal PAN_SCALE = 1.0;
+}
+
+/**
+ @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.
+ @param parent
+*/
+SpaceMouseListener::SpaceMouseListener(QObject *parent) :
+ QObject(parent)
+{
+ if (spnav_open() == -1) {
+ return;
+ }
+
+ m_available = true;
+ m_notifier = new QSocketNotifier(spnav_fd(), QSocketNotifier::Read, this);
+ connect(m_notifier, &QSocketNotifier::activated,
+ this, &SpaceMouseListener::readEvents);
+}
+
+/**
+ @brief SpaceMouseListener::~SpaceMouseListener
+*/
+SpaceMouseListener::~SpaceMouseListener()
+{
+ if (m_available) {
+ spnav_close();
+ }
+}
+
+/**
+ @brief SpaceMouseListener::zoomFactorForZAxis
+ @param z : raw Z-axis (push/pull) delta from a spnav motion event
+ @return the multiplicative factor DiagramView::zoom() expects
+*/
+qreal SpaceMouseListener::zoomFactorForZAxis(int z)
+{
+ return 1.0 + (static_cast(z) / ZOOM_DIVISOR);
+}
+
+/**
+ @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.
+ 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
+*/
+void SpaceMouseListener::dispatchMotion(const spnav_event_motion &motion)
+{
+ auto *editor = qobject_cast(qApp->activeWindow());
+ if (!editor) {
+ return;
+ }
+
+ ProjectView *project_view = editor->currentProjectView();
+ if (!project_view) {
+ return;
+ }
+
+ DiagramView *view = project_view->currentDiagram();
+ if (!view) {
+ return;
+ }
+
+ if (motion.x || motion.y)
+ {
+ view->horizontalScrollBar()->setValue(
+ view->horizontalScrollBar()->value() - qRound(motion.x * PAN_SCALE));
+ view->verticalScrollBar()->setValue(
+ view->verticalScrollBar()->value() - qRound(motion.y * PAN_SCALE));
+ }
+
+ if (motion.z) {
+ view->zoom(zoomFactorForZAxis(motion.z));
+ }
+}
diff --git a/sources/spacemouse/spacemouselistener.h b/sources/spacemouse/spacemouselistener.h
new file mode 100644
index 000000000..5424a07f9
--- /dev/null
+++ b/sources/spacemouse/spacemouselistener.h
@@ -0,0 +1,79 @@
+/*
+ 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 SPACEMOUSELISTENER_H
+#define SPACEMOUSELISTENER_H
+
+#include
+
+class QSocketNotifier;
+struct spnav_event_motion;
+
+/**
+ @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).
+
+ 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()).
+*/
+class SpaceMouseListener : public QObject
+{
+ Q_OBJECT
+
+ public:
+ explicit SpaceMouseListener(QObject *parent = nullptr);
+ ~SpaceMouseListener() override;
+
+ /// 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;}
+
+ /// 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.
+ 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();
+
+ private:
+ void dispatchMotion(const spnav_event_motion &motion);
+
+ bool m_available = false;
+ QSocketNotifier *m_notifier = nullptr;
+};
+
+#endif // SPACEMOUSELISTENER_H