diff --git a/vclib/render/include/vclib/bgfx/drawers/viewer_drawer_bgfx.h b/vclib/render/include/vclib/bgfx/drawers/viewer_drawer_bgfx.h index ec49b8202e..bb12216591 100644 --- a/vclib/render/include/vclib/bgfx/drawers/viewer_drawer_bgfx.h +++ b/vclib/render/include/vclib/bgfx/drawers/viewer_drawer_bgfx.h @@ -161,21 +161,12 @@ class ViewerDrawerBGFX : public AbstractViewerDrawer return block; } - bool onMouseDoubleClick( - MouseButton::Enum button, - double x, - double y, - const KeyModifiers& modifiers) override + // shadows Base::readDepthRequest: the requested homogeneousNDC is always + // overridden with the value required by the current bgfx backend + void readDepthRequest(double x, double y, bool homogeneousNDC = true) { - bool block = Base::onMouseDoubleClick(button, x, y, modifiers); - - if (!block && button == MouseButton::LEFT) { - const bool homogeneousNDC = - Context::instance().capabilites().homogeneousDepth; - - Base::readDepthRequest(x, y, homogeneousNDC); - } - return block; + homogeneousNDC = Context::instance().capabilites().homogeneousDepth; + Base::readDepthRequest(x, y, homogeneousNDC); } private: diff --git a/vclib/render/include/vclib/bgfx/editors/selection_editor_bgfx.h b/vclib/render/include/vclib/bgfx/editors/selection_editor_bgfx.h index 639513e77b..f53d6fa5b7 100644 --- a/vclib/render/include/vclib/bgfx/editors/selection_editor_bgfx.h +++ b/vclib/render/include/vclib/bgfx/editors/selection_editor_bgfx.h @@ -337,25 +337,17 @@ class SelectionEditorBGFX : public Editor double y, const vcl::KeyModifiers& modifiers) override { - if (!isSelectionActive()) - return false; - - auto actionOpt = mSettings.mouseBindings.action({button, modifiers}); - if (actionOpt.has_value() && !mSelectionInProgress) { - SelectionDragAction action = actionOpt.value(); + return mousePress(button, x, y, modifiers, false); + } - if (!mActionCreationPending) { - savePreSelectionStates(); - mActionCreationPending = true; - } - mSelectionInProgress = true; - mSelectionAnchor = Point2d {x, y}; - mSelectionBox = Box2d({x, y}); - mCurrentMouseAction = actionOpt.value(); - mCurrentSelectionModes = actionModesForSettings(action); - return true; // Smart blocking - } - return false; + bool onMouseDoubleClick( + vcl::MouseButton::Enum button, + double x, + double y, + const vcl::KeyModifiers& modifiers) override + { + // Treat double-click as a press for selection + return mousePress(button, x, y, modifiers, true); } bool onMouseRelease( @@ -418,6 +410,35 @@ class SelectionEditorBGFX : public Editor return mSettings.selectVertices || mSettings.selectFaces; } + bool mousePress( + vcl::MouseButton::Enum button, + double x, + double y, + const vcl::KeyModifiers& modifiers, + bool doubleClick) + { + if (!isSelectionActive()) + return false; + + auto actionOpt = + mSettings.mouseBindings.action({button, modifiers, doubleClick}); + if (actionOpt.has_value() && !mSelectionInProgress) { + SelectionDragAction action = actionOpt.value(); + + if (!mActionCreationPending) { + savePreSelectionStates(); + mActionCreationPending = true; + } + mSelectionInProgress = true; + mSelectionAnchor = Point2d {x, y}; + mSelectionBox = Box2d({x, y}); + mCurrentMouseAction = actionOpt.value(); + mCurrentSelectionModes = actionModesForSettings(action); + return true; // Smart blocking + } + return false; + } + std::vector actionModesForSettings( SelectionAtomicAction action) const { diff --git a/vclib/render/include/vclib/opengl2/drawers/viewer_drawer_opengl2.h b/vclib/render/include/vclib/opengl2/drawers/viewer_drawer_opengl2.h index 4b817840e4..3a27fd3009 100644 --- a/vclib/render/include/vclib/opengl2/drawers/viewer_drawer_opengl2.h +++ b/vclib/render/include/vclib/opengl2/drawers/viewer_drawer_opengl2.h @@ -79,20 +79,6 @@ class ViewerDrawerOpenGL2 : public AbstractViewerDrawer for (auto& obj : *(ParentViewer::mDrawList)) obj->draw(); } - - // events - bool onMouseDoubleClick( - MouseButton::Enum button, - double x, - double y, - const KeyModifiers& modifiers) override - { - bool block = ParentViewer::onMouseDoubleClick(button, x, y, modifiers); - if (!block && button == MouseButton::LEFT) { - ParentViewer::readDepthRequest(x, y); - } - return block; - } }; } // namespace vcl diff --git a/vclib/render/include/vclib/qt/gui/settings_dialog/input_bindings_widget.h b/vclib/render/include/vclib/qt/gui/settings_dialog/input_bindings_widget.h new file mode 100644 index 0000000000..4a1af60b57 --- /dev/null +++ b/vclib/render/include/vclib/qt/gui/settings_dialog/input_bindings_widget.h @@ -0,0 +1,83 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef VCL_QT_GUI_SETTINGS_DIALOG_INPUT_BINDINGS_WIDGET_H +#define VCL_QT_GUI_SETTINGS_DIALOG_INPUT_BINDINGS_WIDGET_H + +#include + +#include +#include +#include +#include + +namespace vcl { +class AbstractInputActionMap; +} // namespace vcl + +namespace vcl::qt { + +namespace Ui { +class InputBindingsWidget; +} // namespace Ui + +/** + * @brief A widget listing all the actions of a single AbstractInputActionMap, + * each with a ShortcutButton to view and reassign its current binding. + * + * Edits made by the user are buffered in mPendingBindings and are only + * written back to the underlying action map when applySettings() is called, + * so that closing the settings dialog without applying discards the changes. + */ +class InputBindingsWidget : public QWidget +{ + Q_OBJECT + + std::unique_ptr mUI; + std::reference_wrapper mMap; + // actionId -> pending input strings, not yet applied to mMap + std::map> mPendingBindings; + +public: + struct ActionInfo + { + std::string id; + std::string name; + }; + + explicit InputBindingsWidget( + std::reference_wrapper map, + QWidget* parent = nullptr); + ~InputBindingsWidget() override; + + void applySettings(); + + // Conflict resolution interface + int inputType() const; + std::string mapName() const; + + std::vector getActions() const; + + std::vector currentInputs(const std::string& actionId) const; + + void setConflict( + const std::string& actionId, + bool hasConflict, + const QString& tooltip = ""); + + void clearAllConflicts(); + +signals: + void bindingsChanged(); + +private: + void populateTable(); +}; + +} // namespace vcl::qt + +#endif // VCL_QT_GUI_SETTINGS_DIALOG_INPUT_BINDINGS_WIDGET_H diff --git a/vclib/render/include/vclib/qt/gui/settings_dialog/settings_dialog_tab.h b/vclib/render/include/vclib/qt/gui/settings_dialog/settings_dialog_tab.h index 15548820f5..0556e921cb 100644 --- a/vclib/render/include/vclib/qt/gui/settings_dialog/settings_dialog_tab.h +++ b/vclib/render/include/vclib/qt/gui/settings_dialog/settings_dialog_tab.h @@ -16,6 +16,13 @@ #include #include +#include +#include + +namespace vcl { +class AbstractInputActionMap; +} // namespace vcl + namespace vcl::qt { /** diff --git a/vclib/render/include/vclib/qt/gui/settings_dialog/shortcuts_settings_tab.h b/vclib/render/include/vclib/qt/gui/settings_dialog/shortcuts_settings_tab.h new file mode 100644 index 0000000000..a77873f26c --- /dev/null +++ b/vclib/render/include/vclib/qt/gui/settings_dialog/shortcuts_settings_tab.h @@ -0,0 +1,67 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef VCL_QT_SHORTCUTS_SETTINGS_TAB_H +#define VCL_QT_SHORTCUTS_SETTINGS_TAB_H + +#include +#include + +#include +#include +#include + +#include + +namespace vcl::qt { + +class InputBindingsWidget; + +/** + * @brief The SettingsDialogTab that lets the user browse and customize all + * the input bindings (shortcuts) exposed by the viewer and its active + * editors. + * + * The available ActionMapGroup%s are obtained on demand from mProvider (e.g. + * a callback into the viewer), so the tab always reflects the editors that + * are currently pushed into the viewer. + */ +class ShortcutsSettingsTab : public SettingsDialogTab +{ + std::function()> mProvider; + // one InputBindingsWidget per action map, paired with its owning group + // name, used by checkConflicts() to scope conflict detection + std::vector> mWidgets; + +public: + explicit ShortcutsSettingsTab( + std::function()> provider) : + mProvider(std::move(provider)) + { + } + + ~ShortcutsSettingsTab() override = default; + + QString category() const override; + + QString name() const override; + + QWidget* createWidget(QWidget* parent) override; + + void applySettings() override; + + void saveSettings(nlohmann::json& j) const override; + + void updateToolbarFrames(QToolBar* /*toolbar*/) override {} + +private: + void checkConflicts(); +}; + +} // namespace vcl::qt + +#endif // VCL_QT_SHORTCUTS_SETTINGS_TAB_H diff --git a/vclib/render/include/vclib/qt/gui/shortcut_button.h b/vclib/render/include/vclib/qt/gui/shortcut_button.h new file mode 100644 index 0000000000..418b371780 --- /dev/null +++ b/vclib/render/include/vclib/qt/gui/shortcut_button.h @@ -0,0 +1,61 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef VCL_QT_SHORTCUT_BUTTON_H +#define VCL_QT_SHORTCUT_BUTTON_H + +#include + +#include +#include + +#include +#include + +namespace vcl::qt { + +/** + * @brief A push button that, when clicked, listens for the next key or mouse + * input and reports it as a string via the onInputCaptured callback. + * + * Used by InputBindingsWidget to let the user interactively (re)assign a + * shortcut to an action. The kind of input it listens for (key, mouse button + * or scroll axis) is restricted by \p mExpectedType, so that e.g. a button + * editing a mouse binding ignores keyboard events. + */ +class ShortcutButton : public QPushButton +{ + AbstractInputActionMap::InputType mExpectedType; + bool mListening = false; + QString mOriginalText; + // used to distinguish a single click from the first click of a double + // click when capturing mouse bindings + QTimer* mDoubleClickTimer = nullptr; + Qt::MouseButton mPendingButton; + Qt::KeyboardModifiers mPendingModifiers; + +public: + std::function onInputCaptured; + + explicit ShortcutButton( + AbstractInputActionMap::InputType expectedType, + const QString& text, + QWidget* parent = nullptr); + + void startListening(); + +protected: + void keyPressEvent(QKeyEvent* event) override; + void mousePressEvent(QMouseEvent* event) override; + void mouseDoubleClickEvent(QMouseEvent* event) override; + void wheelEvent(QWheelEvent* event) override; + void focusOutEvent(QFocusEvent* event) override; +}; + +} // namespace vcl::qt + +#endif // VCL_QT_SHORTCUT_BUTTON_H diff --git a/vclib/render/include/vclib/qt/mesh_viewer.h b/vclib/render/include/vclib/qt/mesh_viewer.h index 4a626335a0..1b734772da 100644 --- a/vclib/render/include/vclib/qt/mesh_viewer.h +++ b/vclib/render/include/vclib/qt/mesh_viewer.h @@ -40,17 +40,6 @@ class MeshViewer; class ViewerSettingsFrame; -class KeyFilter : public QObject -{ - Q_OBJECT - -public: - KeyFilter(QObject* parent = nullptr) : QObject(parent) {} - -protected: - bool eventFilter(QObject* obj, QEvent* event) override; -}; - class MeshViewer : public QMainWindow { Q_OBJECT @@ -322,8 +311,6 @@ public slots: void addEditorFrame(QWidget* frame); - void keyPressEvent(QKeyEvent* event) override; - private: void setupSettingsButton(); diff --git a/vclib/render/include/vclib/render/drawers/abstract_viewer_drawer.h b/vclib/render/include/vclib/render/drawers/abstract_viewer_drawer.h index a2e0655ed6..09d32bac5b 100644 --- a/vclib/render/include/vclib/render/drawers/abstract_viewer_drawer.h +++ b/vclib/render/include/vclib/render/drawers/abstract_viewer_drawer.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -93,6 +94,7 @@ class AbstractViewerDrawer : public TrackBallEventDrawer AbstractViewerDrawer(uint width = 1024, uint height = 768) : Base(width, height) { + registerGlobalActions(); } ~AbstractViewerDrawer() = default; @@ -158,16 +160,26 @@ class AbstractViewerDrawer : public TrackBallEventDrawer * @param[in] callback: The callback to execute when the action is * triggered. */ + void registerGlobalAction( + const std::string& name, + const std::vector>& defaultShortcuts, + GlobalActionCallback callback) + { + mGlobalActionRegistry[name] = std::move(callback); + mViewerSettings.globalActionMap.registerActions({ + {name, name, defaultShortcuts} + }); + } + void registerGlobalAction( const std::string& name, std::pair defaultShortcut, GlobalActionCallback callback) { - mGlobalActionRegistry[name] = std::move(callback); - // Set the default shortcut only if the action isn't already mapped - if (!mViewerSettings.globalActionMap.input(name).has_value()) { - mViewerSettings.globalActionMap.setBinding(name, defaultShortcut); - } + registerGlobalAction( + name, + std::vector> {defaultShortcut}, + std::move(callback)); } const DrawableObjectVector& drawableObjectVector() const @@ -196,6 +208,25 @@ class AbstractViewerDrawer : public TrackBallEventDrawer const ViewerSettings& viewerSettings() const { return mViewerSettings; } + std::vector actionMapGroups() + { + std::vector groups; + + auto viewerMaps = mViewerSettings.actionMaps(); + if (!viewerMaps.empty()) { + groups.push_back({"Viewer", std::move(viewerMaps)}); + } + + for (auto& editor : mEditors) { + auto editorMaps = editor->settings().actionMaps(); + if (!editorMaps.empty()) { + groups.push_back({editor->name(), std::move(editorMaps)}); + } + } + + return groups; + } + void setViewerSettings(const ViewerSettings& settings) { mViewerSettings = settings; @@ -205,9 +236,9 @@ class AbstractViewerDrawer : public TrackBallEventDrawer void loadSettings(const nlohmann::json& j) { - if (j.contains("ViewerSettings")) { + if (j.contains("Viewer")) { ViewerSettings settings = mViewerSettings; - settings.loadSettings(j); + settings.loadSettings(j["Viewer"]); derived()->setViewerSettings(settings); } if (j.contains("Editors")) { @@ -220,7 +251,8 @@ class AbstractViewerDrawer : public TrackBallEventDrawer void saveSettings(nlohmann::json& j) const { - mViewerSettings.saveSettings(j); + // must be nested under "Viewer" to mirror loadSettings() + mViewerSettings.saveSettings(j["Viewer"]); for (const auto& editor : mEditors) { if (editor) editor->saveSettings(j["Editors"]); @@ -473,17 +505,20 @@ class AbstractViewerDrawer : public TrackBallEventDrawer { bool block = false; - if (mEditorsEventsEnabled) { - auto actionNameOpt = - mViewerSettings.globalActionMap.action({key, modifiers}); - if (actionNameOpt.has_value()) { - auto it = mGlobalActionRegistry.find(actionNameOpt.value()); - if (it != mGlobalActionRegistry.end()) { - it->second(); - return true; - } + // global actions (e.g. Escape to toggle editor events) always fire, + // even when editor events are disabled, otherwise they could never + // be used to re-enable them + auto actionNameOpt = + mViewerSettings.globalActionMap.action({key, modifiers}); + if (actionNameOpt.has_value()) { + auto it = mGlobalActionRegistry.find(actionNameOpt.value()); + if (it != mGlobalActionRegistry.end()) { + it->second(); + return true; } + } + if (mEditorsEventsEnabled) { for (const auto& editor : mEditors) { if (!block && editor->isActive()) block = editor->onKeyPress(key, modifiers); @@ -493,48 +528,6 @@ class AbstractViewerDrawer : public TrackBallEventDrawer if (!block) block = Base::onKeyPress(key, modifiers); - if (!block) { - switch (key) { - case Key::ESCAPE: - if (modifiers[KeyModifier::NO_MODIFIER]) { - toggleEditorsEventsEnabled(); - block = true; - } - break; - case Key::R: - if (modifiers[KeyModifier::NO_MODIFIER]) - fitScene(); - break; - case Key::S: - if (modifiers[KeyModifier::CONTROL]) - DRA::DRW::screenshot(derived(), "viewer_screenshot.png"); - break; - case Key::A: - if (modifiers[KeyModifier::NO_MODIFIER]) { - if (mCustomShortcutToggleAxisCallback) - mCustomShortcutToggleAxisCallback(); - } - break; - case Key::Z: - if (modifiers[KeyModifier::CONTROL] && - modifiers[KeyModifier::SHIFT]) { - redo(); - block = true; - } - else if (modifiers[KeyModifier::CONTROL]) { - undo(); - block = true; - } - break; - case Key::Y: - if (modifiers[KeyModifier::CONTROL]) { - redo(); - block = true; - } - break; - default: break; - } - } return block; } @@ -587,6 +580,16 @@ class AbstractViewerDrawer : public TrackBallEventDrawer } } + if (!block) { + auto actionOpt = + Base::mouseAtomicMap().action({button, modifiers, false}); + if (actionOpt.has_value() && + actionOpt.value() == TrackballMotionType::FOCUS) { + derived()->readDepthRequest(x, y, true); + block = true; + } + } + if (!block) block = Base::onMousePress(button, x, y, modifiers); @@ -632,6 +635,16 @@ class AbstractViewerDrawer : public TrackBallEventDrawer if (!block) block = Base::onMouseDoubleClick(button, x, y, modifiers); + if (!block) { + auto actionOpt = + Base::mouseAtomicMap().action({button, modifiers, true}); + if (actionOpt.has_value() && + actionOpt.value() == TrackballMotionType::FOCUS) { + derived()->readDepthRequest(x, y, true); + block = true; + } + } + return block; } @@ -748,6 +761,53 @@ class AbstractViewerDrawer : public TrackBallEventDrawer } private: + void registerGlobalActions() + { + registerGlobalAction( + "Toggle Editors Events", + {Key::ESCAPE, {KeyModifier::NO_MODIFIER}}, + [this]() { + toggleEditorsEventsEnabled(); + }); + + registerGlobalAction( + "Fit Scene", {Key::R, {KeyModifier::NO_MODIFIER}}, [this]() { + fitScene(); + }); + + registerGlobalAction( + "Toggle Axis", {Key::A, {KeyModifier::NO_MODIFIER}}, [this]() { + if (mCustomShortcutToggleAxisCallback) + mCustomShortcutToggleAxisCallback(); + }); + + registerGlobalAction( + "Take Screenshot", {Key::S, {KeyModifier::CONTROL}}, [this]() { + DRA::DRW::screenshot(derived(), "viewer_screenshot.png"); + }); + + registerGlobalAction( + "Undo", {Key::Z, {KeyModifier::CONTROL}}, [this]() { + undo(); + }); + + registerGlobalAction( + "Redo", + { + {Key::Y, {KeyModifier::CONTROL} }, + {Key::Z, {KeyModifier::CONTROL, KeyModifier::SHIFT}} + }, + [this]() { + redo(); + }); + + registerGlobalAction( + "Toggle Trackball", {Key::T, {KeyModifier::NO_MODIFIER}}, [this]() { + if (this->mCustomShortcutToggleTrackballCallback) + this->mCustomShortcutToggleTrackballCallback(); + }); + } + auto* derived() { return static_cast(this); } const auto* derived() const { return static_cast(this); } diff --git a/vclib/render/include/vclib/render/drawers/trackball_event_drawer.h b/vclib/render/include/vclib/render/drawers/trackball_event_drawer.h index a6f179f317..5fb549dd1d 100644 --- a/vclib/render/include/vclib/render/drawers/trackball_event_drawer.h +++ b/vclib/render/include/vclib/render/drawers/trackball_event_drawer.h @@ -42,6 +42,7 @@ class TrackBallEventDrawerT : public EventDrawer using DragMotionMap = TrackballSettings::DragMotionMap; using ScrollAtomicMap = TrackballSettings::ScrollAtomicMap; + using MouseAtomicMap = TrackballSettings::MouseAtomicMap; using KeyAtomicMap = TrackballSettings::KeyAtomicMap; using AtomicActionCallback = std::function; @@ -59,9 +60,11 @@ class TrackBallEventDrawerT : public EventDrawer // current modifiers state (must be updated using setKeyModifiers) KeyModifiers mCurrentKeyModifiers = {KeyModifier::NO_MODIFIER}; + // current mouse button (automatically updated) // if dragging holds the current mouse button MouseButton::Enum mCurrentMouseButton = MouseButton::NO_BUTTON; + bool mIsDoubleClick = false; // trackball gizmo TrackballGizmo mTrackballGizmo; @@ -69,16 +72,17 @@ class TrackBallEventDrawerT : public EventDrawer // directional light gizmo DirectionalLightGizmo mDirectionalLightGizmo; - std::function mCustomShortcutToggleTrackballCallback = - [this]() { - toggleTrackBallVisibility(); - }; - TrackballSettings mDefaultTrackballSettings; std::map mAtomicActionRegistry = defaultAtomicActionRegistry(); +protected: + std::function mCustomShortcutToggleTrackballCallback = + [this]() { + toggleTrackBallVisibility(); + }; + public: TrackBallEventDrawerT(uint width = 1024, uint height = 768) : Base(width, height) @@ -240,6 +244,20 @@ class TrackBallEventDrawerT : public EventDrawer return trackballSettings().scrollAtomicMap; } + /** + * @brief Returns a reference to the MouseAtomicMap to allow reading or + * modifying mouse bindings. + */ + MouseAtomicMap& mouseAtomicMap() + { + return trackballSettings().mouseAtomicMap; + } + + const MouseAtomicMap& mouseAtomicMap() const + { + return trackballSettings().mouseAtomicMap; + } + /** * @brief Returns a reference to the KeyAtomicMap to allow reading or * modifying key bindings. @@ -295,12 +313,6 @@ class TrackBallEventDrawerT : public EventDrawer bool onKeyPress(Key::Enum key, const KeyModifiers& modifiers) override { setKeyModifiers(modifiers); - // handle shortcut for trackball visibility - if (key == Key::T && modifiers[KeyModifier::NO_MODIFIER]) { - if (mCustomShortcutToggleTrackballCallback) - mCustomShortcutToggleTrackballCallback(); - return true; - } keyPress(key); return false; } @@ -327,7 +339,7 @@ class TrackBallEventDrawerT : public EventDrawer { setKeyModifiers(modifiers); moveMouse(x, y); - pressMouse(button); + pressMouse({button, modifiers, false}); return false; } @@ -339,7 +351,24 @@ class TrackBallEventDrawerT : public EventDrawer { setKeyModifiers(modifiers); moveMouse(x, y); - releaseMouse(button); + + bool isDbl = (button == mCurrentMouseButton) ? mIsDoubleClick : false; + MouseInput input = {button, modifiers, isDbl}; + releaseMouse(input); + + return false; + } + + bool onMouseDoubleClick( + MouseButton::Enum button, + double x, + double y, + const KeyModifiers& modifiers) override + { + setKeyModifiers(modifiers); + moveMouse(x, y); + + pressMouse({button, modifiers, true}); return false; } @@ -373,27 +402,37 @@ class TrackBallEventDrawerT : public EventDrawer void moveMouse(int x, int y) { - // ugly AF - auto actionOpt = dragMotionMap().action( - std::make_pair(mCurrentMouseButton, mCurrentKeyModifiers)); + MouseInput currentInput = { + mCurrentMouseButton, mCurrentKeyModifiers, mIsDoubleClick}; + auto actionOpt = dragMotionMap().action(currentInput); if (actionOpt.has_value()) { mTrackball.beginDragMotion(actionOpt.value()); } + else if (mTrackball.isDragging()) { + mTrackball.endDragMotion(currentMotion()); + } mTrackball.setMousePosition(x, y); mTrackball.update(); } - void pressMouse(MouseButton::Enum button) + void pressMouse(const MouseInput& input) { // if dragging, do not update the current mouse button if (mTrackball.isDragging()) { return; } - mCurrentMouseButton = button; + // ignore spurious single-click press events emitted by some systems + // immediately after a double-click event + if (mCurrentMouseButton == input.button && mIsDoubleClick && + !input.isDoubleClick) { + return; + } + + mCurrentMouseButton = input.button; + mIsDoubleClick = input.isDoubleClick; - auto actionOpt = dragMotionMap().action( - std::make_pair(button, mCurrentKeyModifiers)); + auto actionOpt = dragMotionMap().action(input); if (actionOpt.has_value()) { mTrackball.beginDragMotion(actionOpt.value()); // no need to update here, it will be updated in moveMouse @@ -402,19 +441,23 @@ class TrackBallEventDrawerT : public EventDrawer } } - void releaseMouse(MouseButton::Enum button) + void releaseMouse(const MouseInput& input) { - // if dragging, update the current mouse button only if it matches - if (mTrackball.isDragging() && mCurrentMouseButton == button) { + // update the current mouse button only if it matches + if (mCurrentMouseButton == input.button) { mCurrentMouseButton = MouseButton::NO_BUTTON; + mIsDoubleClick = false; } - auto actionOpt = dragMotionMap().action( - std::make_pair(button, mCurrentKeyModifiers)); + auto actionOpt = dragMotionMap().action(input); if (actionOpt.has_value()) { mTrackball.endDragMotion(actionOpt.value()); mTrackball.update(); } + else if (mTrackball.isDragging()) { + mTrackball.endDragMotion(currentMotion()); + mTrackball.update(); + } } void scroll(Scalar pixelDeltaX, Scalar pixelDeltaY) @@ -452,8 +495,9 @@ class TrackBallEventDrawerT : public EventDrawer } // dragging - auto actionOpt = dragMotionMap().action( - std::make_pair(mCurrentMouseButton, mCurrentKeyModifiers)); + MouseInput currentInput = { + mCurrentMouseButton, mCurrentKeyModifiers, mIsDoubleClick}; + auto actionOpt = dragMotionMap().action(currentInput); if (actionOpt.has_value()) { mTrackball.beginDragMotion(actionOpt.value()); } @@ -470,8 +514,9 @@ class TrackBallEventDrawerT : public EventDrawer return; // dragging - auto actionOpt = dragMotionMap().action( - std::make_pair(mCurrentMouseButton, mCurrentKeyModifiers)); + MouseInput currentInput = { + mCurrentMouseButton, mCurrentKeyModifiers, mIsDoubleClick}; + auto actionOpt = dragMotionMap().action(currentInput); if (actionOpt.has_value()) { mTrackball.beginDragMotion(actionOpt.value()); } diff --git a/vclib/render/include/vclib/render/editors/mesh_selector_editor.h b/vclib/render/include/vclib/render/editors/mesh_selector_editor.h index 682f69870c..35b55aed4c 100644 --- a/vclib/render/include/vclib/render/editors/mesh_selector_editor.h +++ b/vclib/render/include/vclib/render/editors/mesh_selector_editor.h @@ -10,39 +10,31 @@ #include "editor.h" +#include + namespace vcl { template class MeshSelectorEditor : public Editor { -public: - enum class MeshSelectorAction { SELECT_MESH }; - using MouseMap = BindingMap< - std::pair, - MeshSelectorAction>; - -private: using Base = Editor; // a callback function called when an object is selected std::function mOnObjectSelectedFunction = nullptr; - EditorSettings mSettings; - - MouseMap mMouseBindings = { - {{MouseButton::RIGHT, {KeyModifier::NO_MODIFIER}}, - MeshSelectorAction::SELECT_MESH} - }; + MeshSelectorEditorSettings mSettings; public: + using MouseMap = MeshSelectorEditorSettings::MouseMap; + void setOnObjectSelectedFunction(const std::function& f) { mOnObjectSelectedFunction = f; } - MouseMap& mouseBindings() { return mMouseBindings; } + MouseMap& mouseBindings() { return mSettings.mouseBindings; } - const MouseMap& mouseBindings() const { return mMouseBindings; } + const MouseMap& mouseBindings() const { return mSettings.mouseBindings; } // Editor implementation @@ -75,7 +67,37 @@ class MeshSelectorEditor : public Editor if (block) return true; - auto action = mMouseBindings.action({button, modifiers}); + auto action = mSettings.mouseBindings.action({button, modifiers}); + if (action.has_value() && + action.value() == MeshSelectorAction::SELECT_MESH) { + auto callback = [&](uint id) { + if (id == vcl::UINT_NULL) + return; + + if (mOnObjectSelectedFunction) + mOnObjectSelectedFunction(id); + else + Base::drawList()->setSelectedObjectId(id); + }; + + Base::viewerReadIdRequest(x, y, callback); + return true; // Smart blocking: consumed event + } + + return false; + } + + bool onMouseDoubleClick( + vcl::MouseButton::Enum button, + double x, + double y, + const vcl::KeyModifiers& modifiers) override + { + bool block = Base::onMouseDoubleClick(button, x, y, modifiers); + if (block) + return true; + + auto action = mSettings.mouseBindings.action({button, modifiers, true}); if (action.has_value() && action.value() == MeshSelectorAction::SELECT_MESH) { auto callback = [&](uint id) { diff --git a/vclib/render/include/vclib/render/input.h b/vclib/render/include/vclib/render/input.h index 375ee40491..f7c85800cc 100644 --- a/vclib/render/include/vclib/render/input.h +++ b/vclib/render/include/vclib/render/input.h @@ -8,7 +8,8 @@ #ifndef VCL_RENDER_INPUT_H #define VCL_RENDER_INPUT_H -#include "input/binding_map.h" +#include "input/abstract_input_action_map.h" #include "input/input.h" +#include "input/input_action_map.h" #endif // VCL_RENDER_INPUT_H diff --git a/vclib/render/include/vclib/render/input/abstract_input_action_map.h b/vclib/render/include/vclib/render/input/abstract_input_action_map.h new file mode 100644 index 0000000000..c217686cec --- /dev/null +++ b/vclib/render/include/vclib/render/input/abstract_input_action_map.h @@ -0,0 +1,114 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef VCL_RENDER_INPUT_ABSTRACT_INPUT_ACTION_MAP_H +#define VCL_RENDER_INPUT_ABSTRACT_INPUT_ACTION_MAP_H + +#include + +#include +#include + +namespace vcl { + +/** + * @brief Base polymorphic interface for action maps. + * + * This class provides a type-erased interface for the UI to interact with + * input bindings. It allows retrieving a list of actions and assigning new + * string-based inputs to them, without knowing the specific Input or ActionID + * types used internally. + */ +class AbstractInputActionMap +{ +public: + /** + * @brief Identifies the family of physical inputs this map accepts. + */ + enum class InputType { KEY, MOUSE_BUTTON, SCROLL_AXIS, UNKNOWN }; + + /** + * @brief A Data Transfer Object (DTO) containing string-based information + * about a registered action. Used by the UI to list and edit bindings + * without knowing the underlying types. + */ + struct ActionInfo + { + std::string id; + std::string name; + std::vector inputs; + std::vector defaultInputs; + }; + + virtual ~AbstractInputActionMap() = default; + + /** + * @brief Returns the human-readable name of this action map. + * + * This name is typically used by the UI to group related actions + * (e.g., "Viewer Actions", "Mouse Bindings"). + * + * @return The name of the map as a string. + */ + virtual std::string mapName() const = 0; + + /** + * @brief Returns the type of physical input this map accepts. + * This is used by the UI to filter incoming events when listening for a new + * shortcut. + */ + virtual InputType inputType() const = 0; + + /** + * @brief Retrieves the list of all registered actions and their current + * input bindings. + * + * Implementations should return a vector of `ActionInfo` describing each + * action. The `inputs` vector in `ActionInfo` should represent the current + * physical inputs bound to the action, or be empty if no input is currently + * bound. + * + * @return A vector containing the information for all registered actions. + */ + virtual std::vector actions() const = 0; + + /** + * @brief Assigns a new set of physical inputs to a specific action. + * + * This method is called by the UI when the user assigns new shortcuts to + * an action. Implementations should parse each string in `inputStrs` back + * into their physical input type and update the internal binding map. + * + * @param[in] actionId: The unique string identifier of the action (from + * `ActionInfo::id`). + * @param[in] inputStrs: The string representations of the physical inputs + * (e.g. ["Ctrl+Left", "Middle Click"]). If empty, the bindings should be + * removed. + */ + virtual void setBindings( + const std::string& actionId, + const std::vector& inputStrs) = 0; + + /** + * @brief Restores all bindings in the map to their original default values. + */ + virtual void resetToDefaults() = 0; + + /** + * @brief Loads the bindings from a JSON object. + */ + virtual void loadSettings(const nlohmann::json& j) = 0; + + /** + * @brief Saves the bindings to a JSON object. + */ + virtual void saveSettings(nlohmann::json& j) const = 0; +}; + +} // namespace vcl + +#endif // VCL_RENDER_INPUT_ABSTRACT_INPUT_ACTION_MAP_H diff --git a/vclib/render/include/vclib/render/input/action_map_group.h b/vclib/render/include/vclib/render/input/action_map_group.h new file mode 100644 index 0000000000..09105dbe6e --- /dev/null +++ b/vclib/render/include/vclib/render/input/action_map_group.h @@ -0,0 +1,44 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef VCL_RENDER_INPUT_ACTION_MAP_GROUP_H +#define VCL_RENDER_INPUT_ACTION_MAP_GROUP_H + +#include +#include +#include + +namespace vcl { + +class AbstractInputActionMap; + +/** + * @brief A struct that groups a list of action maps under a common owner name. + * + * This structure is primarily used to aggregate all the action maps exported + * by a specific owner (such as the Viewer or a specific Editor) so that + * they can be dynamically exposed to the UI (e.g., the Settings Dialog) + * in an organized and categorized manner. + */ +struct ActionMapGroup { + /** + * @brief The name of the group (e.g., "Viewer", "Mesh Selector Editor"). + * This is used as the category title in the UI. + */ + std::string name; + + /** + * @brief A list of mutable references to the action maps owned by this group. + * The UI can use these references to display and modify the key bindings + * in real time. + */ + std::vector> maps; +}; + +} // namespace vcl + +#endif // VCL_RENDER_INPUT_ACTION_MAP_GROUP_H diff --git a/vclib/render/include/vclib/render/input/binding_map.h b/vclib/render/include/vclib/render/input/binding_map.h deleted file mode 100644 index 7a779f0ef4..0000000000 --- a/vclib/render/include/vclib/render/input/binding_map.h +++ /dev/null @@ -1,120 +0,0 @@ -// VCLib - Visual Computing Library -// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. -// -// This Source Code Form is subject to the terms of the Mozilla Public License, -// v. 2.0. If a copy of the MPL was not distributed with this file, You can -// obtain one at https://mozilla.org/MPL/2.0/. - -#ifndef VCL_RENDER_INPUT_BINDING_MAP_H -#define VCL_RENDER_INPUT_BINDING_MAP_H - -#include -#include - -namespace vcl { - -/** - * @brief A class that maps input events to actions. - * - * This class provides a bidirectional mapping between input events (like key - * presses or mouse clicks) and the corresponding actions they trigger. It is - * designed to be efficient for event handling (Input -> Action lookup) while - * also providing the ability to retrieve and modify bindings easily for user - * interfaces (Action -> Input lookup). - * - * Internally, it uses a single std::map to ensure that event handling lookups - * are performed in O(log N) time, while inverse lookups and updates are - * performed linearly, which is perfectly acceptable given the typically small - * number of bindings. - * - * @tparam Input The type representing the input event (e.g., a pair of key and - * modifiers). - * @tparam Action The type representing the action triggered by the input. - */ -template -class BindingMap -{ -private: - std::map mMap; - -public: - /** - * @brief Default constructor. - */ - BindingMap() = default; - - /** - * @brief Constructor with an initializer list of bindings. - * - * @param[in] init: Initializer list of input-action pairs. - */ - BindingMap(std::initializer_list> init) : - mMap(init) - { - } - - /** - * @brief Gets the action associated with a specific input. - * - * @param[in] input: The input event. - * @return An std::optional containing the associated action, or - * std::nullopt if the input is not bound to any action. - */ - std::optional action(const Input& input) const - { - auto it = mMap.find(input); - if (it != mMap.end()) - return it->second; - return std::nullopt; - } - - /** - * @brief Gets the input associated with a specific action. - * - * @param[in] action: The action to look up. - * @return An std::optional containing the associated input, or std::nullopt - * if the action has no binding. - */ - std::optional input(const Action& action) const - { - for (const auto& [inp, act] : mMap) { - if (act == action) - return inp; - } - return std::nullopt; - } - - /** - * @brief Sets or updates the binding for a specific action. - * - * If the action was previously bound to another input, the old binding is - * removed. If the new input was already bound to a different action, it - * will be reassigned to the new action (collision resolution). - * - * @param[in] action: The action to bind. - * @param[in] input: The input event to associate with the action. - */ - void setBinding(const Action& action, const Input& input) - { - for (auto it = mMap.begin(); it != mMap.end();) { - if (it->second == action) { - it = mMap.erase(it); - } - else { - ++it; - } - } - mMap[input] = action; - } - - /** - * @brief Returns the underlying map of input-action bindings. - * - * @return A constant reference to the underlying std::map. - */ - const std::map& map() const { return mMap; } -}; - -} // namespace vcl - -#endif // VCL_RENDER_INPUT_BINDING_MAP_H diff --git a/vclib/render/include/vclib/render/input/input.h b/vclib/render/include/vclib/render/input/input.h index c69082ce1b..28b68edcd8 100644 --- a/vclib/render/include/vclib/render/input/input.h +++ b/vclib/render/include/vclib/render/input/input.h @@ -14,16 +14,6 @@ namespace vcl { -struct MouseButton -{ - enum Enum { - LEFT = 0, - RIGHT = 1, - MIDDLE = 2, - NO_BUTTON = 3, - }; -}; - struct KeyModifier { enum Enum { @@ -124,6 +114,38 @@ struct Key }; }; +struct MouseButton +{ + enum Enum { + LEFT = 0, + RIGHT = 1, + MIDDLE = 2, + NO_BUTTON = 3, + }; +}; + +struct MouseInput +{ + MouseButton::Enum button; + KeyModifiers modifiers; + bool isDoubleClick = false; + + bool operator==(const MouseInput& other) const + { + return button == other.button && modifiers == other.modifiers && + isDoubleClick == other.isDoubleClick; + } + + bool operator<(const MouseInput& other) const + { + if (button != other.button) + return button < other.button; + if (modifiers != other.modifiers) + return modifiers.underlying() < other.modifiers.underlying(); + return isDoubleClick < other.isDoubleClick; + } +}; + struct ScrollAxis { enum Enum { @@ -358,6 +380,110 @@ inline void fromString(const std::string& str, ScrollAxis::Enum& out) } } +// --- std::pair conversions --- + +inline std::string toString(const MouseInput& m) +{ + std::string res = toString(m.modifiers); + if (!res.empty()) + res += "+"; + if (m.isDoubleClick) + res += "Double "; + res += toString(m.button); + return res; +} + +inline void fromString(const std::string& str, MouseInput& out) +{ + out = {MouseButton::NO_BUTTON, {KeyModifier::NO_MODIFIER}, false}; + + if (str.find("Double") != std::string::npos) + out.isDoubleClick = true; + + if (str.find("Left Click") != std::string::npos) + out.button = MouseButton::LEFT; + else if (str.find("Right Click") != std::string::npos) + out.button = MouseButton::RIGHT; + else if (str.find("Middle Click") != std::string::npos) + out.button = MouseButton::MIDDLE; + + fromString(str, out.modifiers); +} + +inline std::string toString( + const std::pair& input) +{ + std::string modStr = toString(input.second); + std::string btnStr = toString(input.first); + if (modStr.empty()) + return btnStr; + return modStr + "+" + btnStr; +} + +inline void fromString( + const std::string& str, + std::pair& out) +{ + size_t lastPlus = str.find_last_of('+'); + if (lastPlus == std::string::npos) { + fromString("", out.second); + fromString(str, out.first); + } + else { + fromString(str.substr(0, lastPlus), out.second); + fromString(str.substr(lastPlus + 1), out.first); + } +} + +inline std::string toString(const std::pair& input) +{ + std::string modStr = toString(input.second); + std::string keyStr = toString(input.first); + if (modStr.empty()) + return keyStr; + return modStr + "+" + keyStr; +} + +inline void fromString( + const std::string& str, + std::pair& out) +{ + size_t lastPlus = str.find_last_of('+'); + if (lastPlus == std::string::npos) { + fromString("", out.second); + fromString(str, out.first); + } + else { + fromString(str.substr(0, lastPlus), out.second); + fromString(str.substr(lastPlus + 1), out.first); + } +} + +inline std::string toString( + const std::pair& input) +{ + std::string modStr = toString(input.second); + std::string axisStr = toString(input.first); + if (modStr.empty()) + return axisStr; + return modStr + "+" + axisStr; +} + +inline void fromString( + const std::string& str, + std::pair& out) +{ + size_t lastPlus = str.find_last_of('+'); + if (lastPlus == std::string::npos) { + fromString("", out.second); + fromString(str, out.first); + } + else { + fromString(str.substr(0, lastPlus), out.second); + fromString(str.substr(lastPlus + 1), out.first); + } +} + } // namespace vcl #endif // VCL_RENDER_INPUT_INPUT_H diff --git a/vclib/render/include/vclib/render/input/input_action_map.h b/vclib/render/include/vclib/render/input/input_action_map.h new file mode 100644 index 0000000000..60aca79059 --- /dev/null +++ b/vclib/render/include/vclib/render/input/input_action_map.h @@ -0,0 +1,245 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef VCL_RENDER_INPUT_INPUT_ACTION_MAP_H +#define VCL_RENDER_INPUT_INPUT_ACTION_MAP_H + +#include +#include + +#include + +#include + +#include +#include +#include +#include + +namespace vcl { + +/** + * @brief A map that associates specific Input events to Action identifiers. + * + * This class acts as the single source of truth for input actions, mapping a + * physical input (like a mouse click or a keyboard key) to a specific action. + * Actions must be explicitly registered using `registerAction()`. + * + * @tparam Input The type of the input event (e.g. `std::pair`). + * It must support `vcl::toString()` and `vcl::fromString()`. + * @tparam ActionID The type identifying the action. It can be an `enum class` + * (e.g., `TrackballMotionType`), a `std::string`, or any other type. + * It must support `vcl::toString()` and `vcl::fromString()`. + */ +template +class InputActionMap : public AbstractInputActionMap +{ + struct ActionDef + { + ActionID id; + std::string name; + std::vector inputs; + std::vector defaultInputs; + }; + + std::string mMapName; + std::vector mDefs; + std::map mBindings; + +public: + struct InitData + { + ActionID id; + std::string name; + std::vector defaultInputs = {}; + }; + + InputActionMap(const std::string& name = "") : mMapName(name) {} + + void registerAction( + const ActionID& id, + const std::string& name, + const std::vector& defaultInputs = {}) + { + for (auto& def : mDefs) { + if (def.id == id) { + def.name = name; + def.defaultInputs = defaultInputs; + if (def.inputs.empty() && !defaultInputs.empty()) { + def.inputs = defaultInputs; + } + updateBindings(); + return; + } + } + mDefs.push_back({id, name, defaultInputs, defaultInputs}); + updateBindings(); + } + + void registerActions(std::initializer_list actions) + { + for (const auto& a : actions) { + registerAction(a.id, a.name, a.defaultInputs); + } + } + + void resetToDefaults() override + { + for (auto& def : mDefs) { + def.inputs = def.defaultInputs; + } + updateBindings(); + } + + std::string mapName() const override { return mMapName; } + + InputType inputType() const override + { + if constexpr ( + std::is_same_v> || + std::is_same_v) { + return InputType::KEY; + } + else if constexpr ( + std::is_same_v> || + std::is_same_v || + std::is_same_v) { + return InputType::MOUSE_BUTTON; + } + else if constexpr ( + std::is_same_v> || + std::is_same_v) { + return InputType::SCROLL_AXIS; + } + else { + return InputType::UNKNOWN; + } + } + + std::vector actions() const override + { + std::vector res; + res.reserve(mDefs.size()); + for (const auto& def : mDefs) { + AbstractInputActionMap::ActionInfo info; + info.id = toString(def.id); + info.name = def.name; + for (const auto& in : def.inputs) { + info.inputs.push_back(toString(in)); + } + for (const auto& in : def.defaultInputs) { + info.defaultInputs.push_back(toString(in)); + } + res.push_back(info); + } + return res; + } + + void setBindings( + const std::string& actionId, + const std::vector& inputStrs) override + { + ActionID id; + try { + id = vcl::fromString(actionId); + } catch (...) { + return; + } + + for (auto& def : mDefs) { + if (def.id == id) { + def.inputs.clear(); + for (const auto& inStr : inputStrs) { + if (!inStr.empty()) { + try { + def.inputs.push_back(vcl::fromString(inStr)); + } catch (...) {} + } + } + updateBindings(); + return; + } + } + } + + bool hasBinding(const Input& in) const + { + return mBindings.find(in) != mBindings.end(); + } + + std::optional action(const Input& in) const + { + auto it = mBindings.find(in); + if (it != mBindings.end()) { + return it->second; + } + return std::nullopt; + } + + std::vector inputs(const ActionID& id) const + { + for (const auto& def : mDefs) { + if (def.id == id) { + return def.inputs; + } + } + return {}; + } + + void loadSettings(const nlohmann::json& j) override + { + if (j.contains(mMapName)) { + for (const auto& [actionId, jValue] : j[mMapName].items()) { + std::vector inputStrs; + if (jValue.is_array()) { + for (const auto& item : jValue) { + inputStrs.push_back(item.template get()); + } + } + else if (jValue.is_string()) { + std::string str = jValue.template get(); + if (!str.empty()) { + inputStrs.push_back(str); + } + } + setBindings(actionId, inputStrs); + } + } + } + + void saveSettings(nlohmann::json& j) const override + { + auto& mapJson = j[mMapName]; + for (const auto& def : mDefs) { + std::vector strs; + for (const auto& in : def.inputs) { + strs.push_back(toString(in)); + } + mapJson[toString(def.id)] = strs; + } + } + +private: + // rebuilds the reverse (input -> action) lookup table used by action(). + // Each action can be bound to multiple Inputs; if two actions are bound to + // the same Input, the last one in mDefs silently wins the lookup (the UI + // is expected to warn about such conflicts, see checkConflicts()). + void updateBindings() + { + mBindings.clear(); + for (const auto& def : mDefs) { + for (const auto& in : def.inputs) { + mBindings[in] = def.id; + } + } + } +}; + +} // namespace vcl + +#endif // VCL_RENDER_INPUT_INPUT_ACTION_MAP_H diff --git a/vclib/render/include/vclib/render/selection/selection_mode.h b/vclib/render/include/vclib/render/selection/selection_mode.h index 9f79a0313c..777f84ab7d 100644 --- a/vclib/render/include/vclib/render/selection/selection_mode.h +++ b/vclib/render/include/vclib/render/selection/selection_mode.h @@ -8,6 +8,8 @@ #ifndef VCL_RENDER_SELECTION_SELECTION_MODE_H #define VCL_RENDER_SELECTION_SELECTION_MODE_H +#include +#include #include namespace vcl { @@ -39,6 +41,29 @@ enum class SelectionAtomicAction { ///< everything except what is currently chosen. }; +inline std::string toString(SelectionAtomicAction action) +{ + switch (action) { + case SelectionAtomicAction::ALL: return "Select All"; + case SelectionAtomicAction::NONE: return "Deselect All"; + case SelectionAtomicAction::INVERT: return "Invert Selection"; + default: return "Unknown"; + } +} + +inline void fromString(const std::string& str, SelectionAtomicAction& out) +{ + if (str == "Select All") + out = SelectionAtomicAction::ALL; + else if (str == "Deselect All") + out = SelectionAtomicAction::NONE; + else if (str == "Invert Selection") + out = SelectionAtomicAction::INVERT; + else + throw std::invalid_argument( + "Invalid SelectionAtomicAction string: " + str); +} + /** * @brief Box-based selection operations. * @@ -62,6 +87,29 @@ enum class SelectionDragAction { ///< with a modifier key (e.g. Ctrl+Shift+drag). }; +inline std::string toString(SelectionDragAction action) +{ + switch (action) { + case SelectionDragAction::REGULAR: return "Regular Selection"; + case SelectionDragAction::ADD: return "Add to Selection"; + case SelectionDragAction::SUBTRACT: return "Subtract from Selection"; + default: return "Unknown"; + } +} + +inline void fromString(const std::string& str, SelectionDragAction& out) +{ + if (str == "Regular Selection") + out = SelectionDragAction::REGULAR; + else if (str == "Add to Selection") + out = SelectionDragAction::ADD; + else if (str == "Subtract from Selection") + out = SelectionDragAction::SUBTRACT; + else + throw std::invalid_argument( + "Invalid SelectionDragAction string: " + str); +} + /** * @brief Describes a single selection operation. * diff --git a/vclib/render/include/vclib/render/settings/bounding_box_editor_settings.h b/vclib/render/include/vclib/render/settings/bounding_box_editor_settings.h index a7cb49ed6a..0edbf1f938 100644 --- a/vclib/render/include/vclib/render/settings/bounding_box_editor_settings.h +++ b/vclib/render/include/vclib/render/settings/bounding_box_editor_settings.h @@ -35,14 +35,16 @@ struct BoundingBoxEditorSettings : public EditorSettings * @brief Loads the settings from a JSON object. * @param[in] j: the JSON object to read from. */ - void loadSettings(const nlohmann::json& j) + void loadSettings(const nlohmann::json& j) override { - if (j.contains("BoundingBoxEditor")) { - const auto& jBox = j["BoundingBoxEditor"]; - color = jBox.value("color", color); - thickness = jBox.value("thickness", thickness); - editMode = static_cast( - jBox.value("editMode", static_cast(editMode))); + EditorSettings::loadSettings(j); + + if (j.contains("Bounding Box Editor")) { + const auto& jbb = j["Bounding Box Editor"]; + color = jbb.value("color", color); + thickness = jbb.value("thickness", thickness); + editMode = static_cast( + jbb.value("editMode", static_cast(editMode))); } } @@ -50,11 +52,14 @@ struct BoundingBoxEditorSettings : public EditorSettings * @brief Saves the settings to a JSON object. * @param[out] j: the JSON object to write to. */ - void saveSettings(nlohmann::json& j) const + void saveSettings(nlohmann::json& j) const override { - j["BoundingBoxEditor"]["color"] = color; - j["BoundingBoxEditor"]["thickness"] = thickness; - j["BoundingBoxEditor"]["editMode"] = static_cast(editMode); + EditorSettings::saveSettings(j); + + auto& jbb = j["Bounding Box Editor"]; + jbb["color"] = color; + jbb["thickness"] = thickness; + jbb["editMode"] = static_cast(editMode); } }; diff --git a/vclib/render/include/vclib/render/settings/editor_settings.h b/vclib/render/include/vclib/render/settings/editor_settings.h index aaf523b67b..ad96198ad0 100644 --- a/vclib/render/include/vclib/render/settings/editor_settings.h +++ b/vclib/render/include/vclib/render/settings/editor_settings.h @@ -8,8 +8,15 @@ #ifndef VCL_RENDER_SETTINGS_EDITOR_SETTINGS_H #define VCL_RENDER_SETTINGS_EDITOR_SETTINGS_H +#include + #include +#include + +#include +#include + namespace vcl { struct EditorSettings @@ -24,6 +31,56 @@ struct EditorSettings /**< @brief The edit mode of the editor. */ EditMode editMode = EditMode::CURRENT_OBJECT; + /** + * @brief Retrieves the action maps associated with this editor. + * + * This method allows the editor to expose its input action maps (which bind + * physical inputs like keys or mouse buttons to specific logical actions) + * to the outside world. + * + * The primary use case is for the UI (such as the Settings Dialog) to + * collect all the action maps from active editors and present them to the + * user for customization. Modifying the returned maps will dynamically + * update the editor's input bindings. + * + * @return A vector of mutable references to the editor's + * AbstractInputActionMaps. Returns an empty vector by default if the editor + * has no custom bindings. + */ + virtual std::vector> + actionMaps() + { + return {}; + } + + virtual std::vector> + actionMaps() const + { + return {}; + } + + /** + * @brief Loads the settings from a JSON object. + * @param[in] j: the JSON object to read from. + */ + virtual void loadSettings(const nlohmann::json& j) + { + for (auto& map : actionMaps()) { + map.get().loadSettings(j); + } + } + + /** + * @brief Saves the settings to a JSON object. + * @param[out] j: the JSON object to write to. + */ + virtual void saveSettings(nlohmann::json& j) const + { + for (const auto& map : actionMaps()) { + map.get().saveSettings(j); + } + } + virtual ~EditorSettings() = default; }; diff --git a/vclib/render/include/vclib/render/settings/mesh_selector_editor_settings.h b/vclib/render/include/vclib/render/settings/mesh_selector_editor_settings.h new file mode 100644 index 0000000000..6c86d32bd3 --- /dev/null +++ b/vclib/render/include/vclib/render/settings/mesh_selector_editor_settings.h @@ -0,0 +1,95 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#ifndef VCL_RENDER_SETTINGS_MESH_SELECTOR_EDITOR_SETTINGS_H +#define VCL_RENDER_SETTINGS_MESH_SELECTOR_EDITOR_SETTINGS_H + +#include +#include +#include + +#include + +namespace vcl { + +enum class MeshSelectorAction { SELECT_MESH }; + +inline std::string toString(MeshSelectorAction action) +{ + switch (action) { + case MeshSelectorAction::SELECT_MESH: return "Select Mesh"; + default: return "Unknown"; + } +} + +inline void fromString(const std::string& str, MeshSelectorAction& out) +{ + if (str == "Select Mesh") + out = MeshSelectorAction::SELECT_MESH; + else + throw std::invalid_argument( + "Invalid MeshSelectorAction string: " + str); +} + +struct MeshSelectorEditorSettings : public EditorSettings +{ + using MouseMap = InputActionMap; + + /** + * @brief The mouse input action map for this editor. + */ + MouseMap mouseBindings = defaultMouseMap(); + + /** + * @brief Resets the settings and bindings to their default values. + */ + void resetDefaults() { mouseBindings.resetToDefaults(); } + + /** + * @brief Retrieves the action maps associated with this editor. + */ + std::vector> actionMaps() + override + { + return {mouseBindings}; + } + + /** + * @brief Retrieves the action maps associated with this editor. + */ + std::vector> + actionMaps() const override + { + return {mouseBindings}; + } + + /** + * @brief Loads the settings from a JSON object. + * @param[in] j: the JSON object to read from. + */ + void loadSettings(const nlohmann::json& j) override + { + EditorSettings::loadSettings(j); + } + +private: + static MouseMap defaultMouseMap() + { + using enum MouseButton::Enum; + using enum KeyModifier::Enum; + MouseMap map("Mesh Selector Mouse Actions"); + map.registerActions({ + {MeshSelectorAction::SELECT_MESH, + "Select Mesh", {MouseInput {RIGHT, {NO_MODIFIER}, false}}} + }); + return map; + } +}; + +} // namespace vcl + +#endif // VCL_RENDER_SETTINGS_MESH_SELECTOR_EDITOR_SETTINGS_H diff --git a/vclib/render/include/vclib/render/settings/selection_editor_settings.h b/vclib/render/include/vclib/render/settings/selection_editor_settings.h index 899b3faeb1..b3239d63be 100644 --- a/vclib/render/include/vclib/render/settings/selection_editor_settings.h +++ b/vclib/render/include/vclib/render/settings/selection_editor_settings.h @@ -8,8 +8,8 @@ #ifndef VCL_RENDER_SETTINGS_SELECTION_EDITOR_SETTINGS_H #define VCL_RENDER_SETTINGS_SELECTION_EDITOR_SETTINGS_H -#include #include +#include #include #include @@ -21,11 +21,10 @@ namespace vcl { struct SelectionEditorSettings : public EditorSettings { - using KeyMap = - BindingMap, SelectionAtomicAction>; - using MouseMap = BindingMap< - std::pair, - SelectionDragAction>; + using KeyMap = InputActionMap< + std::pair, + SelectionAtomicAction>; + using MouseMap = InputActionMap; bool selectVertices = false; bool selectFaces = false; @@ -43,16 +42,38 @@ struct SelectionEditorSettings : public EditorSettings onlyVisible = false; selectionBoxColor = vcl::Color(27, 120, 249, 64); editMode = EditMode::CURRENT_OBJECT; + keyBindings.resetToDefaults(); + mouseBindings.resetToDefaults(); + } + + /** + * @brief Retrieves the action maps associated with this editor. + */ + std::vector> actionMaps() + override + { + return {keyBindings, mouseBindings}; + } + + /** + * @brief Retrieves the action maps associated with this editor. + */ + std::vector> + actionMaps() const override + { + return {keyBindings, mouseBindings}; } /** * @brief Loads the settings from a JSON object. * @param[in] j: the JSON object to read from. */ - void loadSettings(const nlohmann::json& j) + void loadSettings(const nlohmann::json& j) override { - if (j.contains("SelectionEditor")) { - const auto& jSel = j["SelectionEditor"]; + EditorSettings::loadSettings(j); + + if (j.contains("Selection Editor")) { + const auto& jSel = j["Selection Editor"]; onlyVisible = jSel.value("onlyVisible", onlyVisible); selectionBoxColor = jSel.value("selectionBoxColor", selectionBoxColor); @@ -65,11 +86,14 @@ struct SelectionEditorSettings : public EditorSettings * @brief Saves the settings to a JSON object. * @param[out] j: the JSON object to write to. */ - void saveSettings(nlohmann::json& j) const + void saveSettings(nlohmann::json& j) const override { - j["SelectionEditor"]["onlyVisible"] = onlyVisible; - j["SelectionEditor"]["selectionBoxColor"] = selectionBoxColor; - j["SelectionEditor"]["editMode"] = static_cast(editMode); + EditorSettings::saveSettings(j); + + auto& jSel = j["Selection Editor"]; + jSel["onlyVisible"] = onlyVisible; + jSel["selectionBoxColor"] = selectionBoxColor; + jSel["editMode"] = static_cast(editMode); } private: @@ -77,12 +101,17 @@ struct SelectionEditorSettings : public EditorSettings { using enum Key::Enum; using enum KeyModifier::Enum; - - return KeyMap { - {{A, {CONTROL}}, SelectionAtomicAction::ALL }, - {{D, {CONTROL}}, SelectionAtomicAction::NONE }, - {{I, {CONTROL}}, SelectionAtomicAction::INVERT} - }; + using Input = std::pair; + + KeyMap map("Selection Atomic Actions"); + map.registerActions({ + {SelectionAtomicAction::ALL, "Select All", {Input {A, {CONTROL}}}}, + {SelectionAtomicAction::NONE, + "Deselect All", {Input {D, {CONTROL}}}}, + {SelectionAtomicAction::INVERT, + "Invert Selection", {Input {I, {CONTROL}}}} + }); + return map; } static MouseMap defaultMouseMap() @@ -90,11 +119,16 @@ struct SelectionEditorSettings : public EditorSettings using enum MouseButton::Enum; using enum KeyModifier::Enum; - return MouseMap { - {{LEFT, {NO_MODIFIER}}, SelectionDragAction::REGULAR }, - {{LEFT, {CONTROL}}, SelectionDragAction::ADD }, - {{LEFT, {CONTROL, SHIFT}}, SelectionDragAction::SUBTRACT} - }; + MouseMap map("Selection Drag Actions"); + map.registerActions({ + {SelectionDragAction::REGULAR, + "Regular Selection", {MouseInput {LEFT, {NO_MODIFIER}, false}} }, + {SelectionDragAction::ADD, + "Add to Selection", {MouseInput {LEFT, {CONTROL}, false}} }, + {SelectionDragAction::SUBTRACT, + "Subtract from Selection", {MouseInput {LEFT, {CONTROL, SHIFT}, false}}} + }); + return map; } }; diff --git a/vclib/render/include/vclib/render/settings/trackball_settings.h b/vclib/render/include/vclib/render/settings/trackball_settings.h index ccd1f8d007..9b6ff3c06b 100644 --- a/vclib/render/include/vclib/render/settings/trackball_settings.h +++ b/vclib/render/include/vclib/render/settings/trackball_settings.h @@ -11,6 +11,8 @@ #include #include +#include + namespace vcl { /** @@ -18,90 +20,161 @@ namespace vcl { */ struct TrackballSettings { - using DragMotionMap = BindingMap< - std::pair, - TrackballMotionType>; - using ScrollAtomicMap = BindingMap< + using DragMotionMap = InputActionMap; + using ScrollAtomicMap = InputActionMap< std::pair, TrackballMotionType>; using KeyAtomicMap = - BindingMap, std::string>; + InputActionMap, std::string>; + using MouseAtomicMap = InputActionMap; DragMotionMap dragMotionMap = defaultDragMotionMap(); ScrollAtomicMap scrollAtomicMap = defaultScrollMotionMap(); KeyAtomicMap keyAtomicMap = defaultKeyAtomicMap(); + MouseAtomicMap mouseAtomicMap = defaultMouseAtomicMap(); + + /** + * @brief Resets the settings and bindings to their default values. + */ + void resetDefaults() + { + dragMotionMap.resetToDefaults(); + scrollAtomicMap.resetToDefaults(); + keyAtomicMap.resetToDefaults(); + mouseAtomicMap.resetToDefaults(); + } + + /** + * @brief Retrieves the action maps associated with the trackball. + */ + std::vector> actionMaps() + { + return {dragMotionMap, scrollAtomicMap, keyAtomicMap, mouseAtomicMap}; + } + + /** + * @brief Retrieves the action maps associated with the trackball. + */ + std::vector> + actionMaps() const + { + return {dragMotionMap, scrollAtomicMap, keyAtomicMap, mouseAtomicMap}; + } + + /** + * @brief Loads the bindings from a JSON object. + * @param[in] j: the JSON object to read from. + */ + void loadSettings(const nlohmann::json& j) + { + for (auto& map : actionMaps()) { + map.get().loadSettings(j); + } + } + + /** + * @brief Saves the bindings to a JSON object. + * @param[out] j: the JSON object to write to. + */ + void saveSettings(nlohmann::json& j) const + { + for (const auto& map : actionMaps()) { + map.get().saveSettings(j); + } + } private: static DragMotionMap defaultDragMotionMap() { using enum MouseButton::Enum; using enum KeyModifier::Enum; + using enum TrackballMotionType; - return DragMotionMap { - {{LEFT, {NO_MODIFIER}}, ARC }, - {{LEFT, {CONTROL}}, PAN }, - {{LEFT, {ALT}}, ZMOVE }, - {{LEFT, {SHIFT}}, SCALE }, - {{MIDDLE, {NO_MODIFIER}}, PAN }, - {{MIDDLE, {CONTROL}}, ROLL }, - {{LEFT, {SHIFT, CONTROL}}, DIR_LIGHT_ARC}, - }; + DragMotionMap map("Trackball Drag Motions"); + map.registerActions({ + {ARC, + "Arcball Rotation", {MouseInput {LEFT, {NO_MODIFIER}, false}} }, + {PAN, "Pan", {MouseInput {LEFT, {CONTROL}, false}} }, + {ZMOVE, "Zoom (Translation)", {MouseInput {LEFT, {ALT}, false}} }, + {SCALE, "Scale", {MouseInput {LEFT, {SHIFT}, false}} }, + {ROLL, "Roll", {MouseInput {MIDDLE, {CONTROL}, false}} }, + {DIR_LIGHT_ARC, + "Light Rotation", {MouseInput {LEFT, {SHIFT, CONTROL}, false}}} + }); + return map; } static ScrollAtomicMap defaultScrollMotionMap() { using enum KeyModifier::Enum; using enum TrackballMotionType; + using Input = std::pair; + + ScrollAtomicMap map("Trackball Scroll Motions"); + map.registerActions({ + {SCALE, "Scale", {Input {ScrollAxis::VERTICAL, {NO_MODIFIER}}} }, + {ROLL, "Roll", {Input {ScrollAxis::VERTICAL, {CONTROL}}} }, + {FOV, + "Field of View", {Input {ScrollAxis::VERTICAL, {SHIFT}}, + Input {ScrollAxis::HORIZONTAL, {SHIFT}}}} + }); + return map; + } + + static MouseAtomicMap defaultMouseAtomicMap() + { + using enum MouseButton::Enum; + using enum KeyModifier::Enum; + using enum TrackballMotionType; - return ScrollAtomicMap { - {{ScrollAxis::VERTICAL, {NO_MODIFIER}}, SCALE}, - {{ScrollAxis::VERTICAL, {CONTROL}}, ROLL }, - {{ScrollAxis::VERTICAL, {SHIFT}}, FOV }, -#ifdef __APPLE__ - {{ScrollAxis::HORIZONTAL, {SHIFT}}, FOV }, -#endif - }; + MouseAtomicMap map("Trackball Mouse Atomic Motions"); + map.registerActions({ + {FOCUS, + "Focus on Object", {MouseInput {LEFT, {NO_MODIFIER}, true}}} + }); + return map; } static KeyAtomicMap defaultKeyAtomicMap() { using enum Key::Enum; using enum KeyModifier::Enum; - - return KeyAtomicMap { - {{R, {NO_MODIFIER}}, "Reset Trackball" }, - {{R, {CONTROL, SHIFT}}, "Reset Directional Light"}, - - // rotate - {{NP_2, {NO_MODIFIER}}, "Rotate X+" }, - {{NP_4, {NO_MODIFIER}}, "Rotate Y-" }, - {{NP_6, {NO_MODIFIER}}, "Rotate Y+" }, - {{NP_8, {NO_MODIFIER}}, "Rotate X-" }, - - // translate - {{UP, {NO_MODIFIER}}, "Translate Y+" }, - {{DOWN, {NO_MODIFIER}}, "Translate Y-" }, - {{LEFT, {NO_MODIFIER}}, "Translate X-" }, - {{RIGHT, {NO_MODIFIER}}, "Translate X+" }, - - // set view - {{NP_1, {NO_MODIFIER}}, "View Front" }, - {{NP_7, {NO_MODIFIER}}, "View Top" }, - {{NP_3, {NO_MODIFIER}}, "View Right" }, - {{NP_1, {CONTROL}}, "View Back" }, - {{NP_7, {CONTROL}}, "View Bottom" }, - {{NP_3, {CONTROL}}, "View Left" }, - - // projection mode - {{NP_5, {NO_MODIFIER}}, "Toggle Projection" }, - - // rotate light - {{NP_2, {CONTROL, SHIFT}}, "Rotate Light X+" }, - {{NP_4, {CONTROL, SHIFT}}, "Rotate Light Y-" }, - {{NP_6, {CONTROL, SHIFT}}, "Rotate Light Y+" }, - {{NP_8, {CONTROL, SHIFT}}, "Rotate Light X-" }, - }; + using Input = std::pair; + + KeyAtomicMap map("Trackball Key Motions"); + map.registerActions({ + // bound to Ctrl+R (not plain R) to avoid clashing with the + // viewer's global "Fit Scene" action, also bound to R + {"Reset Trackball", "Reset Trackball", {Input {R, {CONTROL}}} }, + {"Reset Directional Light", + "Reset Directional Light", {Input {R, {CONTROL, SHIFT}}} }, + {"Rotate X+", "Rotate X+", {Input {NP_2, {NO_MODIFIER}}} }, + {"Rotate Y-", "Rotate Y-", {Input {NP_4, {NO_MODIFIER}}} }, + {"Rotate Y+", "Rotate Y+", {Input {NP_6, {NO_MODIFIER}}} }, + {"Rotate X-", "Rotate X-", {Input {NP_8, {NO_MODIFIER}}} }, + {"Translate Y+", "Translate Y+", {Input {UP, {NO_MODIFIER}}} }, + {"Translate Y-", "Translate Y-", {Input {DOWN, {NO_MODIFIER}}} }, + {"Translate X-", "Translate X-", {Input {LEFT, {NO_MODIFIER}}} }, + {"Translate X+", "Translate X+", {Input {RIGHT, {NO_MODIFIER}}} }, + {"View Front", "View Front", {Input {NP_1, {NO_MODIFIER}}} }, + {"View Top", "View Top", {Input {NP_7, {NO_MODIFIER}}} }, + {"View Right", "View Right", {Input {NP_3, {NO_MODIFIER}}} }, + {"View Back", "View Back", {Input {NP_1, {CONTROL}}} }, + {"View Bottom", "View Bottom", {Input {NP_7, {CONTROL}}} }, + {"View Left", "View Left", {Input {NP_3, {CONTROL}}} }, + {"Toggle Projection", + "Toggle Projection", {Input {NP_5, {NO_MODIFIER}}} }, + {"Rotate Light X+", + "Rotate Light X+", {Input {NP_2, {CONTROL, SHIFT}}}}, + {"Rotate Light Y-", + "Rotate Light Y-", {Input {NP_4, {CONTROL, SHIFT}}}}, + {"Rotate Light Y+", + "Rotate Light Y+", {Input {NP_6, {CONTROL, SHIFT}}}}, + {"Rotate Light X-", + "Rotate Light X-", {Input {NP_8, {CONTROL, SHIFT}}}} + }); + return map; } }; diff --git a/vclib/render/include/vclib/render/settings/viewer_settings.h b/vclib/render/include/vclib/render/settings/viewer_settings.h index a6c1177cf1..5804ff5cd2 100644 --- a/vclib/render/include/vclib/render/settings/viewer_settings.h +++ b/vclib/render/include/vclib/render/settings/viewer_settings.h @@ -25,10 +25,11 @@ namespace vcl { struct ViewerSettings : public TrackballSettings { /** - * @brief Global actions registered by the viewer or editors. + * @brief Global actions type, used to register actions by the viewer or + * editors. */ using ViewerGlobalActionMap = - BindingMap, std::string>; + InputActionMap, std::string>; /** * @brief The tone mapping operators available when rendering. @@ -98,11 +99,17 @@ struct ViewerSettings : public TrackballSettings */ std::string panoramaPath = ""; + /** + * @brief Global actions registered by the viewer or editors. + */ + ViewerGlobalActionMap globalActionMap{"Viewer Global Actions"}; + /** * @brief Resets the settings to their default values. */ void resetDefaults() { + TrackballSettings::resetDefaults(); renderMode = RenderMode::CLASSIC; imageBasedLighting = false; renderBackgroundPanorama = false; @@ -110,6 +117,7 @@ struct ViewerSettings : public TrackballSettings toneMapping = ToneMapping::ACES_HILL; backgroundColor = vcl::Color::DarkGray; panoramaPath = ""; + globalActionMap.resetToDefaults(); } /** @@ -118,8 +126,10 @@ struct ViewerSettings : public TrackballSettings */ void loadSettings(const nlohmann::json& j) { - if (j.contains("ViewerSettings")) { - const auto& js = j["ViewerSettings"]; + TrackballSettings::loadSettings(j); + globalActionMap.loadSettings(j); + if (j.contains("Viewer Settings")) { + const auto& js = j["Viewer Settings"]; renderMode = static_cast( js.value("renderMode", static_cast(renderMode))); imageBasedLighting = @@ -140,17 +150,38 @@ struct ViewerSettings : public TrackballSettings */ void saveSettings(nlohmann::json& j) const { - j["ViewerSettings"]["renderMode"] = static_cast(renderMode); - j["ViewerSettings"]["imageBasedLighting"] = imageBasedLighting; - j["ViewerSettings"]["renderBackgroundPanorama"] = - renderBackgroundPanorama; - j["ViewerSettings"]["exposure"] = exposure; - j["ViewerSettings"]["toneMapping"] = static_cast(toneMapping); - j["ViewerSettings"]["backgroundColor"] = backgroundColor; - j["ViewerSettings"]["panoramaPath"] = panoramaPath; + TrackballSettings::saveSettings(j); + globalActionMap.saveSettings(j); + + auto& js = j["Viewer Settings"]; + js["renderMode"] = static_cast(renderMode); + js["imageBasedLighting"] = imageBasedLighting; + js["renderBackgroundPanorama"] = renderBackgroundPanorama; + js["exposure"] = exposure; + js["toneMapping"] = static_cast(toneMapping); + js["backgroundColor"] = backgroundColor; + js["panoramaPath"] = panoramaPath; + } + + /** + * @brief Retrieves the action maps associated with the viewer. + */ + std::vector> actionMaps() + { + auto res = TrackballSettings::actionMaps(); + res.push_back(globalActionMap); + return res; } - ViewerGlobalActionMap globalActionMap; + /** + * @brief Retrieves the action maps associated with the viewer. + */ + std::vector> actionMaps() const + { + auto res = TrackballSettings::actionMaps(); + res.push_back(globalActionMap); + return res; + } }; } // namespace vcl diff --git a/vclib/render/include/vclib/render/viewer/trackball.h b/vclib/render/include/vclib/render/viewer/trackball.h index 278940fe00..ed55faa009 100644 --- a/vclib/render/include/vclib/render/viewer/trackball.h +++ b/vclib/render/include/vclib/render/viewer/trackball.h @@ -32,6 +32,44 @@ enum class TrackballMotionType { MOTION_COUNT ///< Number of motion types. }; +inline std::string toString(TrackballMotionType type) +{ + switch (type) { + case TrackballMotionType::ARC: return "Arcball Rotation"; + case TrackballMotionType::PAN: return "Pan"; + case TrackballMotionType::ZMOVE: return "Zoom (Translation)"; + case TrackballMotionType::ROLL: return "Roll"; + case TrackballMotionType::SCALE: return "Scale"; + case TrackballMotionType::FOV: return "Field of View"; + case TrackballMotionType::FOCUS: return "Focus"; + case TrackballMotionType::DIR_LIGHT_ARC: return "Light Rotation"; + default: return "Unknown"; + } +} + +inline void fromString(const std::string& str, TrackballMotionType& out) +{ + if (str == "Arcball Rotation") + out = TrackballMotionType::ARC; + else if (str == "Pan") + out = TrackballMotionType::PAN; + else if (str == "Zoom (Translation)") + out = TrackballMotionType::ZMOVE; + else if (str == "Roll") + out = TrackballMotionType::ROLL; + else if (str == "Scale") + out = TrackballMotionType::SCALE; + else if (str == "Field of View") + out = TrackballMotionType::FOV; + else if (str == "Focus") + out = TrackballMotionType::FOCUS; + else if (str == "Light Rotation") + out = TrackballMotionType::DIR_LIGHT_ARC; + else + throw std::invalid_argument( + "Invalid TrackballMotionType string: " + str); +} + /** * @brief The TrackBall class implements a trackball (a camera combined with * model transformation). diff --git a/vclib/render/src/vclib/qt/gui/settings_dialog.cpp b/vclib/render/src/vclib/qt/gui/settings_dialog.cpp index cbf7262194..6dd21473e1 100644 --- a/vclib/render/src/vclib/qt/gui/settings_dialog.cpp +++ b/vclib/render/src/vclib/qt/gui/settings_dialog.cpp @@ -51,8 +51,19 @@ SettingsDialog::SettingsDialog( QWidget* page = new QWidget(); QVBoxLayout* layout = new QVBoxLayout(page); - layout->addWidget(tab->createWidget(page)); - layout->addStretch(); + + QWidget* tabWidgetInstance = tab->createWidget(page); + + if (tabWidgetInstance->sizePolicy().verticalPolicy() == + QSizePolicy::Expanding || + tabWidgetInstance->sizePolicy().verticalPolicy() == + QSizePolicy::MinimumExpanding) { + layout->addWidget(tabWidgetInstance, 1); + } + else { + layout->addWidget(tabWidgetInstance, 0); + layout->addStretch(); + } categoryTabs[cat]->addTab(page, tab->name()); } @@ -77,7 +88,7 @@ SettingsDialog::SettingsDialog( // Reset All Defaults button connect(mUI->resetAllDefaultsButton, &QPushButton::clicked, this, [this]() { - QList buttons = + const QList buttons = this->findChildren("resetDefaultButton"); for (QPushButton* btn : buttons) { btn->click(); @@ -92,6 +103,8 @@ SettingsDialog::~SettingsDialog() void SettingsDialog::onApplyClicked() { + emit applied(); + if (mUI->saveAsDefaultCheckBox->isChecked()) { std::string filePath = mSettingsFilePath; if (filePath.empty()) { @@ -119,6 +132,9 @@ void SettingsDialog::onApplyClicked() if (tab->category() == "Editors") { tab->saveSettings(j["Editors"]); } + else if (tab->category() == "Viewer") { + tab->saveSettings(j["Viewer"]); + } else { tab->saveSettings(j); } @@ -142,8 +158,6 @@ void SettingsDialog::onApplyClicked() "Failed to save default settings to file."); } } - - emit applied(); } } // namespace vcl::qt diff --git a/vclib/render/src/vclib/qt/gui/settings_dialog/input_bindings_widget.cpp b/vclib/render/src/vclib/qt/gui/settings_dialog/input_bindings_widget.cpp new file mode 100644 index 0000000000..07decbe359 --- /dev/null +++ b/vclib/render/src/vclib/qt/gui/settings_dialog/input_bindings_widget.cpp @@ -0,0 +1,327 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#include + +#include "ui_input_bindings_widget.h" +#include + +#include +#include + +#include +#include +#include +#include + +namespace vcl::qt { + +InputBindingsWidget::InputBindingsWidget( + std::reference_wrapper map, + QWidget* parent) : + QWidget(parent), mUI(new Ui::InputBindingsWidget), mMap(map) +{ + mUI->setupUi(this); + + // Setup table headers + mUI->bindingsTable->setColumnCount(2); + mUI->bindingsTable->setHorizontalHeaderLabels({"Action", "Binding"}); + mUI->bindingsTable->horizontalHeader()->setStretchLastSection(true); + mUI->bindingsTable->verticalHeader()->setVisible(false); + + // Auto-resize columns: 50/50 split for clean alignment + mUI->bindingsTable->horizontalHeader()->setSectionResizeMode( + 0, QHeaderView::Stretch); + mUI->bindingsTable->horizontalHeader()->setSectionResizeMode( + 1, QHeaderView::Stretch); + + // Add alternating row colors to help the eye read across + mUI->bindingsTable->setAlternatingRowColors(true); + + populateTable(); + + QPushButton* resetBtn = new QPushButton("Reset Map Defaults", this); + resetBtn->setObjectName("resetDefaultButton"); + resetBtn->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed); + + // The layout is automatically created by setupUi + if (QVBoxLayout* l = qobject_cast(this->layout())) { + l->addWidget(resetBtn, 0, Qt::AlignRight); + } + + connect(resetBtn, &QPushButton::clicked, this, [this]() { + const auto actions = mMap.get().actions(); + for (const auto& action : actions) { + mPendingBindings[action.id] = action.defaultInputs; + } + QTimer::singleShot(0, this, [this]() { + populateTable(); + }); + emit bindingsChanged(); + }); +} + +InputBindingsWidget::~InputBindingsWidget() = default; + +void InputBindingsWidget::applySettings() +{ + for (const auto& [id, inputStrs] : mPendingBindings) { + mMap.get().setBindings(id, inputStrs); + } +} + +void InputBindingsWidget::populateTable() +{ + const auto actions = mMap.get().actions(); + + mUI->bindingsTable->setRowCount(0); + mUI->bindingsTable->setRowCount(actions.size()); + + for (int i = 0; i < (int) actions.size(); ++i) { + const auto& action = actions[i]; + std::string actionId = action.id; + + // Column 0: Action Name + QTableWidgetItem* nameItem = + new QTableWidgetItem(QString::fromStdString(action.name)); + nameItem->setFlags( + nameItem->flags() & ~Qt::ItemIsEditable); // Read-only + + // Store the action ID in the UserRole of the first column + nameItem->setData(Qt::UserRole, QString::fromStdString(actionId)); + mUI->bindingsTable->setItem(i, 0, nameItem); + + // Column 1: Action Binding (Container) + QWidget* bindingWidget = new QWidget(mUI->bindingsTable); + QHBoxLayout* bindingLayout = new QHBoxLayout(bindingWidget); + bindingLayout->setContentsMargins(0, 0, 0, 0); + bindingLayout->setSpacing(5); + + QLabel* warningLabel = new QLabel("⚠️", bindingWidget); + warningLabel->setObjectName("warningLabel"); + warningLabel->hide(); + bindingLayout->addWidget(warningLabel); + + // Container for dynamic shortcut buttons + QWidget* shortcutsContainer = new QWidget(bindingWidget); + QHBoxLayout* shortcutsLayout = new QHBoxLayout(shortcutsContainer); + shortcutsLayout->setContentsMargins(0, 0, 0, 0); + shortcutsLayout->setSpacing(5); + bindingLayout->addWidget(shortcutsContainer); + + QToolButton* addBtn = new QToolButton(bindingWidget); + addBtn->setText("➕"); + addBtn->setToolTip("Add shortcut"); + addBtn->setStyleSheet( + "QToolButton { color: green; border: none; font-weight: bold; }"); + bindingLayout->addWidget(addBtn); + + bindingLayout->addStretch(); // push everything left + + // Lambda to redraw the shortcuts container for this action + auto redrawShortcuts = [this, + shortcutsContainer, + shortcutsLayout, + actionId]() { + // clear layout + QLayoutItem* item; + while ((item = shortcutsLayout->takeAt(0)) != nullptr) { + if (QWidget* widget = item->widget()) { + widget->deleteLater(); + } + delete item; + } + + const auto& inputs = currentInputs(actionId); + + for (size_t j = 0; j < inputs.size(); ++j) { + const auto& inputStr = inputs[j]; + + QWidget* pairWidget = new QWidget(shortcutsContainer); + QHBoxLayout* pairLayout = new QHBoxLayout(pairWidget); + pairLayout->setContentsMargins(0, 0, 0, 0); + pairLayout->setSpacing(2); + + ShortcutButton* bindingBtn = new ShortcutButton( + mMap.get().inputType(), + QString::fromStdString( + inputStr.empty() ? "Listening..." : inputStr), + pairWidget); + bindingBtn->setObjectName("shortcutButton"); + bindingBtn->setSizePolicy( + QSizePolicy::Minimum, QSizePolicy::Fixed); + + QToolButton* unbindBtn = new QToolButton(pairWidget); + unbindBtn->setText("❌"); + unbindBtn->setToolTip("Unbind this shortcut"); + unbindBtn->setStyleSheet( + "QToolButton { color: red; border: none; font-weight: " + "bold; }"); + + pairLayout->addWidget(bindingBtn); + pairLayout->addWidget(unbindBtn); + + shortcutsLayout->addWidget(pairWidget); + + connect( + bindingBtn, &QPushButton::clicked, this, [bindingBtn]() { + bindingBtn->startListening(); + }); + + bindingBtn->onInputCaptured = [this, actionId, j]( + const std::string& newStr) { + if (mPendingBindings.find(actionId) == + mPendingBindings.end()) { + mPendingBindings[actionId] = currentInputs(actionId); + } + mPendingBindings[actionId][j] = newStr; + emit bindingsChanged(); + }; + + connect( + unbindBtn, + &QToolButton::clicked, + this, + [this, actionId, j]() { + if (mPendingBindings.find(actionId) == + mPendingBindings.end()) { + mPendingBindings[actionId] = + currentInputs(actionId); + } + auto& vec = mPendingBindings[actionId]; + if (j < vec.size()) { + vec.erase(vec.begin() + j); + } + + // Queue a safe table redraw to reflect the deletion + QTimer::singleShot(0, this, [this]() { + populateTable(); + }); + emit bindingsChanged(); + }); + } + + // Auto-start listening on the newly added empty shortcut + if (!inputs.empty() && inputs.back().empty()) { + if (shortcutsLayout->count() > 0) { + QWidget* lastPair = + shortcutsLayout->itemAt(shortcutsLayout->count() - 1) + ->widget(); + if (QPushButton* pBtn = lastPair->findChild( + "shortcutButton")) { + if (ShortcutButton* btn = + static_cast(pBtn)) { + btn->startListening(); + } + } + } + } + }; + + connect( + addBtn, + &QToolButton::clicked, + this, + [this, actionId, redrawShortcuts]() { + if (mPendingBindings.find(actionId) == mPendingBindings.end()) { + mPendingBindings[actionId] = currentInputs(actionId); + } + mPendingBindings[actionId].push_back(""); // Add empty slot + redrawShortcuts(); + }); + + // Initial draw + redrawShortcuts(); + + mUI->bindingsTable->setCellWidget(i, 1, bindingWidget); + } + + // Disable internal scrolling and fix height to content + mUI->bindingsTable->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + int totalHeight = 2; // For borders + if (mUI->bindingsTable->horizontalHeader()->isVisible()) { + totalHeight += mUI->bindingsTable->horizontalHeader()->height(); + } + + for (int i = 0; i < mUI->bindingsTable->rowCount(); ++i) { + totalHeight += mUI->bindingsTable->rowHeight(i); + } + mUI->bindingsTable->setMinimumHeight(totalHeight); + mUI->bindingsTable->setMaximumHeight(totalHeight); +} + +int InputBindingsWidget::inputType() const +{ + return static_cast(mMap.get().inputType()); +} + +std::string InputBindingsWidget::mapName() const +{ + return mMap.get().mapName(); +} + +std::vector InputBindingsWidget::getActions() + const +{ + std::vector actions; + for (const auto& a : mMap.get().actions()) { + actions.push_back(ActionInfo {a.id, a.name}); + } + return actions; +} + +std::vector InputBindingsWidget::currentInputs( + const std::string& actionId) const +{ + if (mPendingBindings.count(actionId)) + return mPendingBindings.at(actionId); + + for (const auto& a : mMap.get().actions()) { + if (a.id == actionId) { + return a.inputs; + } + } + return {}; +} + +void InputBindingsWidget::setConflict( + const std::string& actionId, + bool hasConflict, + const QString& tooltip) +{ + for (int i = 0; i < mUI->bindingsTable->rowCount(); ++i) { + QTableWidgetItem* nameItem = mUI->bindingsTable->item(i, 0); + if (nameItem && + nameItem->data(Qt::UserRole).toString().toStdString() == actionId) { + QWidget* w = mUI->bindingsTable->cellWidget(i, 1); + if (w) { + if (QLabel* warningLabel = + w->findChild("warningLabel")) { + warningLabel->setVisible(hasConflict); + warningLabel->setToolTip(tooltip); + } + } + break; + } + } +} + +void InputBindingsWidget::clearAllConflicts() +{ + for (int i = 0; i < mUI->bindingsTable->rowCount(); ++i) { + QWidget* w = mUI->bindingsTable->cellWidget(i, 1); + if (w) { + if (QLabel* warningLabel = w->findChild("warningLabel")) { + warningLabel->setVisible(false); + warningLabel->setToolTip(""); + } + } + } +} + +} // namespace vcl::qt diff --git a/vclib/render/src/vclib/qt/gui/settings_dialog/input_bindings_widget.ui b/vclib/render/src/vclib/qt/gui/settings_dialog/input_bindings_widget.ui new file mode 100644 index 0000000000..38c2d8450a --- /dev/null +++ b/vclib/render/src/vclib/qt/gui/settings_dialog/input_bindings_widget.ui @@ -0,0 +1,66 @@ + + + vcl::qt::InputBindingsWidget + + + + 0 + 0 + 400 + 200 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + false + + + 2 + + + false + + + true + + + false + + + + + + + + + + diff --git a/vclib/render/src/vclib/qt/gui/settings_dialog/shortcuts_settings_tab.cpp b/vclib/render/src/vclib/qt/gui/settings_dialog/shortcuts_settings_tab.cpp new file mode 100644 index 0000000000..eae1f65f8f --- /dev/null +++ b/vclib/render/src/vclib/qt/gui/settings_dialog/shortcuts_settings_tab.cpp @@ -0,0 +1,209 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +namespace vcl::qt { + +QString ShortcutsSettingsTab::category() const +{ + return "Shortcuts"; +} + +QString ShortcutsSettingsTab::name() const +{ + return "Key Bindings"; +} + +QWidget* ShortcutsSettingsTab::createWidget(QWidget* parent) +{ + mWidgets.clear(); + + QScrollArea* scrollArea = new QScrollArea(parent); + scrollArea->setWidgetResizable(true); + scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + + QWidget* scrollContent = new QWidget(scrollArea); + QVBoxLayout* layout = new QVBoxLayout(scrollContent); + layout->setContentsMargins(10, 10, 10, 10); + layout->setSpacing(5); + + QLabel* infoLabel = new QLabel( + "Note: Editor shortcuts are only active when the editor is enabled. " + "If a conflict occurs, active editors override the base Viewer " + "shortcuts.", + scrollContent); + infoLabel->setWordWrap(true); + infoLabel->setStyleSheet("color: gray; font-style: italic;"); + layout->addWidget(infoLabel); + layout->addSpacing(10); + + auto groups = mProvider(); + for (const auto& group : groups) { + QLabel* titleLabel = + new QLabel(QString::fromStdString(group.name), scrollContent); + QFont font = titleLabel->font(); + font.setBold(true); + font.setPointSize(font.pointSize() + 2); + titleLabel->setFont(font); + layout->addWidget(titleLabel); + + for (const auto& mapRef : group.maps) { + QToolButton* toggleButton = new QToolButton(scrollContent); + toggleButton->setStyleSheet( + "QToolButton { border: none; font-weight: bold; text-align: " + "left; }"); + toggleButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + toggleButton->setArrowType(Qt::DownArrow); + toggleButton->setText( + QString::fromStdString(mapRef.get().mapName())); + toggleButton->setCheckable(true); + toggleButton->setChecked(false); + + InputBindingsWidget* bindingsWidget = + new InputBindingsWidget(mapRef, scrollContent); + + QObject::connect( + bindingsWidget, + &InputBindingsWidget::bindingsChanged, + bindingsWidget, + [this]() { + checkConflicts(); + }); + + QObject::connect( + toggleButton, + &QToolButton::toggled, + bindingsWidget, + [toggleButton, bindingsWidget](bool checked) { + toggleButton->setArrowType( + checked ? Qt::RightArrow : Qt::DownArrow); + bindingsWidget->setVisible(!checked); + }); + + layout->addWidget(toggleButton); + layout->addWidget(bindingsWidget); + mWidgets.push_back({bindingsWidget, group.name}); + } + + layout->addSpacing(15); + } + + if (groups.empty()) { + QLabel* placeholderLabel = + new QLabel("No shortcut available.", scrollContent); + placeholderLabel->setAlignment(Qt::AlignCenter); + layout->addWidget(placeholderLabel); + } + + layout->addStretch(); + scrollContent->setLayout(layout); + scrollArea->setWidget(scrollContent); + + // Initial check for default conflicts + checkConflicts(); + + return scrollArea; +} + +void ShortcutsSettingsTab::applySettings() +{ + for (auto& pair : mWidgets) { + pair.first->applySettings(); + } +} + +void ShortcutsSettingsTab::saveSettings(nlohmann::json& j) const +{ + auto groups = mProvider(); + for (const auto& group : groups) { + if (group.name == "Viewer") { + for (const auto& mapRef : group.maps) { + mapRef.get().saveSettings(j["Viewer"]); + } + } + else { + for (const auto& mapRef : group.maps) { + mapRef.get().saveSettings(j["Editors"]); + } + } + } +} + +void ShortcutsSettingsTab::checkConflicts() +{ + struct BindingInfo + { + InputBindingsWidget* widget; + std::string actionId; + std::string actionName; + std::string mapName; + }; + + // Map from (groupName) -> (inputType) -> (inputStr -> vector of bindings) + std::map< + std::string, + std::map>>> + allBindings; + + // First pass: clear all warnings + for (auto& pair : mWidgets) { + pair.first->clearAllConflicts(); + } + + // Second pass: gather all current inputs scoped by their ActionMapGroup + for (auto& pair : mWidgets) { + InputBindingsWidget* widget = pair.first; + std::string groupName = pair.second; + + int inputType = widget->inputType(); + std::string mapName = widget->mapName(); + for (const auto& action : widget->getActions()) { + for (const std::string& inputStr : widget->currentInputs(action.id)) { + if (!inputStr.empty() && inputStr != "None") { + allBindings[groupName][inputType][inputStr].push_back( + {widget, action.id, action.name, mapName}); + } + } + } + } + + // Third pass: identify conflicts and set warnings + for (const auto& [groupName, groupMap] : allBindings) { + for (const auto& [type, inputMap] : groupMap) { + for (const auto& [inputStr, bindings] : inputMap) { + if (bindings.size() > 1) { + // Build tooltip + QStringList conflictNames; + for (const auto& b : bindings) { + conflictNames << QString::fromStdString( + "• " + b.mapName + " -> " + b.actionName); + } + QString tooltip = + "Conflict detected with:\n" + conflictNames.join("\n"); + + // Set warning on all conflicting widgets + for (const auto& b : bindings) { + b.widget->setConflict(b.actionId, true, tooltip); + } + } + } + } + } +} + +} // namespace vcl::qt diff --git a/vclib/render/src/vclib/qt/gui/shortcut_button.cpp b/vclib/render/src/vclib/qt/gui/shortcut_button.cpp new file mode 100644 index 0000000000..e7e3c18bed --- /dev/null +++ b/vclib/render/src/vclib/qt/gui/shortcut_button.cpp @@ -0,0 +1,167 @@ +// VCLib - Visual Computing Library +// Copyright (C) 2021-2026 Visual Computing Lab, ISTI - CNR. +// +// This Source Code Form is subject to the terms of the Mozilla Public License, +// v. 2.0. If a copy of the MPL was not distributed with this file, You can +// obtain one at https://mozilla.org/MPL/2.0/. + +#include + +#include + +#include +#include +#include +#include +#include + +namespace vcl::qt { + +ShortcutButton::ShortcutButton( + AbstractInputActionMap::InputType expectedType, + const QString& text, + QWidget* parent) : QPushButton(text, parent), mExpectedType(expectedType) +{ + setFocusPolicy(Qt::StrongFocus); + setStyleSheet("text-align: left; padding: 2px 5px;"); + setCursor(Qt::PointingHandCursor); +} + +void ShortcutButton::startListening() +{ + if (mListening) + return; + mListening = true; + mOriginalText = text(); + setText("Press any key..."); + setFocus(); +} + +void ShortcutButton::keyPressEvent(QKeyEvent* event) +{ + if (!mListening) { + QPushButton::keyPressEvent(event); + return; + } + + if (mExpectedType != AbstractInputActionMap::InputType::KEY) { + return; + } + + if (event->key() == Qt::Key_Shift || event->key() == Qt::Key_Control || + event->key() == Qt::Key_Alt || event->key() == Qt::Key_Meta || + event->key() == Qt::Key_unknown) { + return; + } + + vcl::Key::Enum vclKey = vcl::qt::fromQt((Qt::Key) event->key()); + vcl::KeyModifiers vclMods = vcl::qt::fromQt(event->modifiers()); + std::string inputStr = vcl::toString(std::make_pair(vclKey, vclMods)); + + mListening = false; + setText(QString::fromStdString(inputStr)); + if (onInputCaptured) + onInputCaptured(inputStr); + clearFocus(); +} + +void ShortcutButton::mousePressEvent(QMouseEvent* event) +{ + if (!mListening) { + QPushButton::mousePressEvent(event); + return; + } + + if (mExpectedType != AbstractInputActionMap::InputType::MOUSE_BUTTON) { + return; + } + + mPendingButton = event->button(); + mPendingModifiers = event->modifiers(); + + if (!mDoubleClickTimer) { + mDoubleClickTimer = new QTimer(this); + mDoubleClickTimer->setSingleShot(true); + connect(mDoubleClickTimer, &QTimer::timeout, this, [this]() { + if (!mListening) + return; + + vcl::MouseButton::Enum vclBtn = vcl::qt::fromQt(mPendingButton); + vcl::KeyModifiers vclMods = vcl::qt::fromQt(mPendingModifiers); + std::string inputStr = + vcl::toString(vcl::MouseInput {vclBtn, vclMods, false}); + + mListening = false; + setText(QString::fromStdString(inputStr)); + if (onInputCaptured) + onInputCaptured(inputStr); + clearFocus(); + }); + } + + mDoubleClickTimer->start(QApplication::doubleClickInterval()); +} + +void ShortcutButton::mouseDoubleClickEvent(QMouseEvent* event) +{ + if (mListening && mDoubleClickTimer && mDoubleClickTimer->isActive()) { + mDoubleClickTimer->stop(); + + if (mExpectedType != AbstractInputActionMap::InputType::MOUSE_BUTTON) { + return; + } + + vcl::MouseButton::Enum vclBtn = vcl::qt::fromQt(event->button()); + vcl::KeyModifiers vclMods = vcl::qt::fromQt(event->modifiers()); + std::string inputStr = + vcl::toString(vcl::MouseInput {vclBtn, vclMods, true}); + + mListening = false; + setText(QString::fromStdString(inputStr)); + if (onInputCaptured) + onInputCaptured(inputStr); + clearFocus(); + } + else { + QPushButton::mouseDoubleClickEvent(event); + } +} + +void ShortcutButton::wheelEvent(QWheelEvent* event) +{ + if (!mListening) { + QPushButton::wheelEvent(event); + return; + } + + if (mExpectedType != AbstractInputActionMap::InputType::SCROLL_AXIS) { + return; + } + + vcl::ScrollAxis::Enum axis = (std::abs(event->angleDelta().x()) > + std::abs(event->angleDelta().y())) ? + vcl::ScrollAxis::HORIZONTAL : + vcl::ScrollAxis::VERTICAL; + vcl::KeyModifiers vclMods = vcl::qt::fromQt(event->modifiers()); + std::string inputStr = vcl::toString(std::make_pair(axis, vclMods)); + + mListening = false; + setText(QString::fromStdString(inputStr)); + if (onInputCaptured) + onInputCaptured(inputStr); + clearFocus(); +} + +void ShortcutButton::focusOutEvent(QFocusEvent* event) +{ + if (mListening) { + mListening = false; + setText(mOriginalText); + if (mDoubleClickTimer && mDoubleClickTimer->isActive()) { + mDoubleClickTimer->stop(); + } + } + QPushButton::focusOutEvent(event); +} + +} // namespace vcl::qt diff --git a/vclib/render/src/vclib/qt/mesh_viewer.cpp b/vclib/render/src/vclib/qt/mesh_viewer.cpp index 6fb99019d5..683d4665de 100644 --- a/vclib/render/src/vclib/qt/mesh_viewer.cpp +++ b/vclib/render/src/vclib/qt/mesh_viewer.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -26,24 +27,6 @@ namespace vcl::qt { -bool KeyFilter::eventFilter(QObject* watched, QEvent* event) -{ - if (event->type() == QEvent::KeyPress) { - QKeyEvent* keyEvent = static_cast(event); - // Ignore only the Ctrl + S shortcut override, you can customize check - // for your needs - if (keyEvent->modifiers().testFlag(Qt::ControlModifier) && - keyEvent->key() == 'S') { - qDebug() << "Ignoring " << keyEvent->modifiers() << " + " - << (char) keyEvent->key() << " for " << watched; - event->ignore(); - return true; - } - } - - return QObject::eventFilter(watched, event); -} - /** * @brief MeshViewer constructor. * @@ -61,6 +44,19 @@ MeshViewer::MeshViewer(QWidget* parent, const std::string& settingsFilePath) : // give keyboard focus to the viewer widget immediately mUI->viewer->setFocus(); + // Register Qt-specific screenshot action with dialog + viewer().registerGlobalAction( + "Take Screenshot", + {Key::S, {KeyModifier::CONTROL}}, + [this]() { + vcl::qt::ScreenShotDialog dialog(this); + if (dialog.exec() && dialog.selectedFiles().size() > 0) { + auto sf = dialog.selectedFiles(); + mUI->viewer->screenshot( + sf[0].toStdString(), dialog.screenMultiplierValue()); + } + }); + // prevent any widget in the right area from stealing keyboard focus mUI->rightArea->setFocusPolicy(Qt::NoFocus); std::function disableFocus = [&disableFocus](QWidget* w) { @@ -120,9 +116,6 @@ MeshViewer::MeshViewer(QWidget* parent, const std::string& settingsFilePath) : /** Events **/ - // install the key filter - mUI->viewer->installEventFilter(new KeyFilter(this)); - // each time that the RenderSettingsFrame updates its settings, we call the // meshRenderSettingsUpdated() member function connect( @@ -217,6 +210,9 @@ MeshViewer::MeshViewer(QWidget* parent, const std::string& settingsFilePath) : } mSettingsData.addTab(std::make_shared(this)); + mSettingsData.addTab(std::make_shared([this]() { + return viewer().actionMapGroups(); + })); setupMeshRenderSettingsTabs(mSettingsData, mDefaultMeshRenderSettings); } @@ -305,7 +301,7 @@ void MeshViewer::setViewerSettings(const ViewerSettings& settings) const ViewerSettings& MeshViewer::viewerSettings() const { - return mViewerSettingsFrame->viewerSettings(); + return viewer().viewerSettings(); } void MeshViewer::fitScene() @@ -380,23 +376,6 @@ void MeshViewer::addEditorFrame(QWidget* frame) } } -void MeshViewer::keyPressEvent(QKeyEvent* event) -{ - // show screenshot dialog on CTRL + S - if (event->key() == Qt::Key_S && event->modifiers() & Qt::ControlModifier) { - vcl::qt::ScreenShotDialog dialog(this); - if (dialog.exec() && dialog.selectedFiles().size() > 0) { - auto sf = dialog.selectedFiles(); - mUI->viewer->screenshot( - sf[0].toStdString(), dialog.screenMultiplierValue()); - } - } - else { - event->ignore(); - QWidget::keyPressEvent(event); - } -} - /** * @brief Setup and add the settings button to the UI toolbar. */ @@ -569,8 +548,18 @@ void MeshViewer::openSettings() SettingsDialog dialog(mSettingsData, this); connect(&dialog, &SettingsDialog::applied, this, [&]() { + // Apply non-shortcuts settings first to avoid overwriting shortcuts with temp copies + for (auto& tab : mSettingsData.tabs()) { + if (tab->category() != "Shortcuts") + tab->applySettings(); + } + // Apply shortcuts last so they take precedence + for (auto& tab : mSettingsData.tabs()) { + if (tab->category() == "Shortcuts") + tab->applySettings(); + } + for (auto& tab : mSettingsData.tabs()) { - tab->applySettings(); tab->updateToolbarFrames(mUI->toolBar); } viewer().update();