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