From 9baeb6e9e6711102421561fc658edc3a51488feb Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 23 Mar 2025 15:34:14 +0100 Subject: [PATCH 1/2] Gui: Add support for hints in status bar --- src/Gui/Application.cpp | 3 + src/Gui/BitmapFactory.cpp | 4 +- src/Gui/CMakeLists.txt | 5 + src/Gui/FreeCADGuiInit.py | 58 ++++ src/Gui/Icons/resource.qrc | 7 + src/Gui/Icons/user-input/mouse-left.svg | 4 + src/Gui/Icons/user-input/mouse-middle.svg | 43 +++ src/Gui/Icons/user-input/mouse-move.svg | 3 + src/Gui/Icons/user-input/mouse-right.svg | 4 + .../Icons/user-input/mouse-scroll-down.svg | 43 +++ src/Gui/Icons/user-input/mouse-scroll-up.svg | 43 +++ src/Gui/Icons/user-input/mouse-scroll.svg | 3 + src/Gui/InputHint.h | 222 ++++++++++++ src/Gui/InputHintPy.cpp | 218 ++++++++++++ src/Gui/InputHintPy.h | 34 ++ src/Gui/InputHintWidget.cpp | 319 ++++++++++++++++++ src/Gui/InputHintWidget.h | 53 +++ src/Gui/MainWindow.cpp | 17 + src/Gui/MainWindow.h | 6 +- src/Gui/MainWindowPy.cpp | 67 +++- src/Gui/MainWindowPy.h | 3 + src/Gui/ToolHandler.cpp | 14 + src/Gui/ToolHandler.h | 5 +- .../Gui/DrawSketchControllableHandler.h | 3 + .../Sketcher/Gui/DrawSketchHandlerLineSet.h | 11 + .../Sketcher/Gui/DrawSketchHandlerPolygon.h | 23 ++ 26 files changed, 1210 insertions(+), 5 deletions(-) create mode 100644 src/Gui/Icons/user-input/mouse-left.svg create mode 100644 src/Gui/Icons/user-input/mouse-middle.svg create mode 100644 src/Gui/Icons/user-input/mouse-move.svg create mode 100644 src/Gui/Icons/user-input/mouse-right.svg create mode 100644 src/Gui/Icons/user-input/mouse-scroll-down.svg create mode 100644 src/Gui/Icons/user-input/mouse-scroll-up.svg create mode 100644 src/Gui/Icons/user-input/mouse-scroll.svg create mode 100644 src/Gui/InputHint.h create mode 100644 src/Gui/InputHintPy.cpp create mode 100644 src/Gui/InputHintPy.h create mode 100644 src/Gui/InputHintWidget.cpp create mode 100644 src/Gui/InputHintWidget.h diff --git a/src/Gui/Application.cpp b/src/Gui/Application.cpp index 1fbef607ed..35a37e9e64 100644 --- a/src/Gui/Application.cpp +++ b/src/Gui/Application.cpp @@ -77,6 +77,7 @@ #include "FileDialog.h" #include "GuiApplication.h" #include "GuiInitScript.h" +#include "InputHintPy.h" #include "LinkViewPy.h" #include "MainWindow.h" #include "Macro.h" @@ -502,6 +503,8 @@ Application::Application(bool GUIenabled) Py::Object(Gui::TaskView::ControlPy::getInstance(), true)); Gui::TaskView::TaskDialogPy::init_type(); + registerUserInputEnumInPython(module); + CommandActionPy::init_type(); Base::Interpreter().addType(CommandActionPy::type_object(), module, "CommandAction"); diff --git a/src/Gui/BitmapFactory.cpp b/src/Gui/BitmapFactory.cpp index dfbe670102..d212c5f6ef 100644 --- a/src/Gui/BitmapFactory.cpp +++ b/src/Gui/BitmapFactory.cpp @@ -325,8 +325,8 @@ QPixmap BitmapFactoryInst::pixmapFromSvg(const QByteArray& originalContents, con for ( const auto &colorToColor : colorMapping ) { ulong fromColor = colorToColor.first; ulong toColor = colorToColor.second; - QString fromColorString = QStringLiteral(":#%1;").arg(fromColor, 6, 16, QChar::fromLatin1('0')); - QString toColorString = QStringLiteral(":#%1;").arg(toColor, 6, 16, QChar::fromLatin1('0')); + QString fromColorString = QStringLiteral("#%1").arg(fromColor, 6, 16, QChar::fromLatin1('0')); + QString toColorString = QStringLiteral("#%1").arg(toColor, 6, 16, QChar::fromLatin1('0')); stringContents = stringContents.replace(fromColorString, toColorString); } QByteArray contents = stringContents.toUtf8(); diff --git a/src/Gui/CMakeLists.txt b/src/Gui/CMakeLists.txt index 17d0f38a69..b3fff2e403 100644 --- a/src/Gui/CMakeLists.txt +++ b/src/Gui/CMakeLists.txt @@ -1305,6 +1305,8 @@ SET(FreeCADGui_CPP_SRCS GuiApplication.cpp GuiApplicationNativeEventAware.cpp GuiConsole.cpp + InputHintWidget.cpp + InputHintPy.cpp Macro.cpp MergeDocuments.cpp ModuleIO.cpp @@ -1340,6 +1342,9 @@ SET(FreeCADGui_SRCS 3Dconnexion/GuiAbstractNativeEvent.h GuiConsole.h InventorAll.h + InputHint.h + InputHintPy.h + InputHintWidget.h Macro.h MergeDocuments.h MetaTypes.h diff --git a/src/Gui/FreeCADGuiInit.py b/src/Gui/FreeCADGuiInit.py index 5c027d838e..505cdcfcc5 100644 --- a/src/Gui/FreeCADGuiInit.py +++ b/src/Gui/FreeCADGuiInit.py @@ -20,6 +20,7 @@ #* USA * #* * #***************************************************************************/ +from dataclasses import dataclass # FreeCAD gui init module # @@ -129,6 +130,63 @@ class NoneWorkbench ( Workbench ): """Return the name of the associated C++ class.""" return "Gui::NoneWorkbench" + +@dataclass +class InputHint: + """ + Represents a single input hint (shortcut suggestion). + + The message is a Qt formatting string with placeholders like %1, %2, ... + The placeholders are replaced with input representations - be it keys, mouse buttons etc. + Each placeholder corresponds to one input sequence. Sequence can either be: + - one input from Gui.UserInput enum + - tuple of mentioned enum values representing the input sequence + + >>> InputHint("%1 change mode", Gui.UserInput.KeyM) + will result in a hint displaying `[M] change mode` + + >>> InputHint("%1 new line", (Gui.UserInput.KeyControl, Gui.UserInput.KeyEnter)) + will result in a hint displaying `[ctrl][enter] new line` + + >>> InputHint("%1/%2 increase/decrease ...", Gui.UserInput.KeyU, Gui.UserInput.KeyJ) + will result in a hint displaying `[U]/[J] increase / decrease ...` + """ + + InputSequence = Gui.UserInput | tuple[Gui.UserInput, ...] + + message: str + sequences: list[InputSequence] + + def __init__(self, message: str, *sequences: InputSequence): + self.message = message + self.sequences = list(sequences) + + +class HintManager: + """ + A convenience class for managing input hints (shortcut suggestions) displayed to the user. + It is here mostly to provide well-defined and easy to reach API from python without developers needing + to call low-level functions on the main window directly. + """ + + def show(self, *hints: InputHint): + """ + Displays the specified input hints to the user. + + :param hints: List of hints to show. + """ + Gui.getMainWindow().showHint(*hints) + + def hide(self): + """ + Hides all currently displayed input hints. + """ + Gui.getMainWindow().hideHint() + + +Gui.InputHint = InputHint +Gui.HintManager = HintManager() + def InitApplications(): import sys,os,traceback import io as cStringIO diff --git a/src/Gui/Icons/resource.qrc b/src/Gui/Icons/resource.qrc index 694dfbd191..67895c44cf 100644 --- a/src/Gui/Icons/resource.qrc +++ b/src/Gui/Icons/resource.qrc @@ -305,6 +305,13 @@ overlay_error.svg feature_suppressed.svg forbidden.svg + user-input/mouse-left.svg + user-input/mouse-right.svg + user-input/mouse-move.svg + user-input/mouse-middle.svg + user-input/mouse-scroll.svg + user-input/mouse-scroll-up.svg + user-input/mouse-scroll-down.svg index.theme diff --git a/src/Gui/Icons/user-input/mouse-left.svg b/src/Gui/Icons/user-input/mouse-left.svg new file mode 100644 index 0000000000..d151576f52 --- /dev/null +++ b/src/Gui/Icons/user-input/mouse-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Gui/Icons/user-input/mouse-middle.svg b/src/Gui/Icons/user-input/mouse-middle.svg new file mode 100644 index 0000000000..8b9a9d9aa3 --- /dev/null +++ b/src/Gui/Icons/user-input/mouse-middle.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/src/Gui/Icons/user-input/mouse-move.svg b/src/Gui/Icons/user-input/mouse-move.svg new file mode 100644 index 0000000000..d9a4ad9996 --- /dev/null +++ b/src/Gui/Icons/user-input/mouse-move.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Gui/Icons/user-input/mouse-right.svg b/src/Gui/Icons/user-input/mouse-right.svg new file mode 100644 index 0000000000..22665af461 --- /dev/null +++ b/src/Gui/Icons/user-input/mouse-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/Gui/Icons/user-input/mouse-scroll-down.svg b/src/Gui/Icons/user-input/mouse-scroll-down.svg new file mode 100644 index 0000000000..42eec1d1a4 --- /dev/null +++ b/src/Gui/Icons/user-input/mouse-scroll-down.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/src/Gui/Icons/user-input/mouse-scroll-up.svg b/src/Gui/Icons/user-input/mouse-scroll-up.svg new file mode 100644 index 0000000000..b042cf6375 --- /dev/null +++ b/src/Gui/Icons/user-input/mouse-scroll-up.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/src/Gui/Icons/user-input/mouse-scroll.svg b/src/Gui/Icons/user-input/mouse-scroll.svg new file mode 100644 index 0000000000..a98de3a924 --- /dev/null +++ b/src/Gui/Icons/user-input/mouse-scroll.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Gui/InputHint.h b/src/Gui/InputHint.h new file mode 100644 index 0000000000..43e763454e --- /dev/null +++ b/src/Gui/InputHint.h @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2024 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef GUI_INPUTHINT_H +#define GUI_INPUTHINT_H + +#include "FCGlobal.h" + +#include +#include + +#include + +namespace Gui +{ +struct InputHint +{ + enum class UserInput + { + // Modifier + ModifierShift = Qt::KeyboardModifier::ShiftModifier, + ModifierCtrl = Qt::KeyboardModifier::ControlModifier, + ModifierAlt = Qt::KeyboardModifier::AltModifier, + ModifierMeta = Qt::KeyboardModifier::MetaModifier, + + // Keyboard Keys + KeySpace = Qt::Key_Space, + KeyExclam = Qt::Key_Exclam, + KeyQuoteDbl = Qt::Key_QuoteDbl, + KeyNumberSign = Qt::Key_NumberSign, + KeyDollar = Qt::Key_Dollar, + KeyPercent = Qt::Key_Percent, + KeyAmpersand = Qt::Key_Ampersand, + KeyApostrophe = Qt::Key_Apostrophe, + KeyParenLeft = Qt::Key_ParenLeft, + KeyParenRight = Qt::Key_ParenRight, + KeyAsterisk = Qt::Key_Asterisk, + KeyPlus = Qt::Key_Plus, + KeyComma = Qt::Key_Comma, + KeyMinus = Qt::Key_Minus, + KeyPeriod = Qt::Key_Period, + KeySlash = Qt::Key_Slash, + Key0 = Qt::Key_0, + Key1 = Qt::Key_1, + Key2 = Qt::Key_2, + Key3 = Qt::Key_3, + Key4 = Qt::Key_4, + Key5 = Qt::Key_5, + Key6 = Qt::Key_6, + Key7 = Qt::Key_7, + Key8 = Qt::Key_8, + Key9 = Qt::Key_9, + KeyColon = Qt::Key_Colon, + KeySemicolon = Qt::Key_Semicolon, + KeyLess = Qt::Key_Less, + KeyEqual = Qt::Key_Equal, + KeyGreater = Qt::Key_Greater, + KeyQuestion = Qt::Key_Question, + KeyAt = Qt::Key_At, + KeyA = Qt::Key_A, + KeyB = Qt::Key_B, + KeyC = Qt::Key_C, + KeyD = Qt::Key_D, + KeyE = Qt::Key_E, + KeyF = Qt::Key_F, + KeyG = Qt::Key_G, + KeyH = Qt::Key_H, + KeyI = Qt::Key_I, + KeyJ = Qt::Key_J, + KeyK = Qt::Key_K, + KeyL = Qt::Key_L, + KeyM = Qt::Key_M, + KeyN = Qt::Key_N, + KeyO = Qt::Key_O, + KeyP = Qt::Key_P, + KeyQ = Qt::Key_Q, + KeyR = Qt::Key_R, + KeyS = Qt::Key_S, + KeyT = Qt::Key_T, + KeyU = Qt::Key_U, + KeyV = Qt::Key_V, + KeyW = Qt::Key_W, + KeyX = Qt::Key_X, + KeyY = Qt::Key_Y, + KeyZ = Qt::Key_Z, + KeyBracketLeft = Qt::Key_BracketLeft, + KeyBackslash = Qt::Key_Backslash, + KeyBracketRight = Qt::Key_BracketRight, + KeyAsciiCircum = Qt::Key_AsciiCircum, + KeyUnderscore = Qt::Key_Underscore, + KeyQuoteLeft = Qt::Key_QuoteLeft, + KeyBraceLeft = Qt::Key_BraceLeft, + KeyBar = Qt::Key_Bar, + KeyBraceRight = Qt::Key_BraceRight, + KeyAsciiTilde = Qt::Key_AsciiTilde, + + // misc keys + KeyEscape = Qt::Key_Escape, + KeyTab = Qt::Key_Tab, + KeyBacktab = Qt::Key_Backtab, + KeyBackspace = Qt::Key_Backspace, + KeyReturn = Qt::Key_Return, + KeyEnter = Qt::Key_Enter, + KeyInsert = Qt::Key_Insert, + KeyDelete = Qt::Key_Delete, + KeyPause = Qt::Key_Pause, + KeyPrintScr = Qt::Key_Print, + KeySysReq = Qt::Key_SysReq, + KeyClear = Qt::Key_Clear, + + // cursor movement + KeyHome = Qt::Key_Home, + KeyEnd = Qt::Key_End, + KeyLeft = Qt::Key_Left, + KeyUp = Qt::Key_Up, + KeyRight = Qt::Key_Right, + KeyDown = Qt::Key_Down, + KeyPageUp = Qt::Key_PageUp, + KeyPageDown = Qt::Key_PageDown, + + // modifiers + KeyShift = Qt::Key_Shift, + KeyControl = Qt::Key_Control, + KeyMeta = Qt::Key_Meta, + KeyAlt = Qt::Key_Alt, + KeyCapsLock = Qt::Key_CapsLock, + KeyNumLock = Qt::Key_NumLock, + KeyScrollLock = Qt::Key_ScrollLock, + + // function keys + KeyF1 = Qt::Key_F1, + KeyF2 = Qt::Key_F2, + KeyF3 = Qt::Key_F3, + KeyF4 = Qt::Key_F4, + KeyF5 = Qt::Key_F5, + KeyF6 = Qt::Key_F6, + KeyF7 = Qt::Key_F7, + KeyF8 = Qt::Key_F8, + KeyF9 = Qt::Key_F9, + KeyF10 = Qt::Key_F10, + KeyF11 = Qt::Key_F11, + KeyF12 = Qt::Key_F12, + KeyF13 = Qt::Key_F13, + KeyF14 = Qt::Key_F14, + KeyF15 = Qt::Key_F15, + KeyF16 = Qt::Key_F16, + KeyF17 = Qt::Key_F17, + KeyF18 = Qt::Key_F18, + KeyF19 = Qt::Key_F19, + KeyF20 = Qt::Key_F20, + KeyF21 = Qt::Key_F21, + KeyF22 = Qt::Key_F22, + KeyF23 = Qt::Key_F23, + KeyF24 = Qt::Key_F24, + KeyF25 = Qt::Key_F25, + KeyF26 = Qt::Key_F26, + KeyF27 = Qt::Key_F27, + KeyF28 = Qt::Key_F28, + KeyF29 = Qt::Key_F29, + KeyF30 = Qt::Key_F30, + KeyF31 = Qt::Key_F31, + KeyF32 = Qt::Key_F32, + KeyF33 = Qt::Key_F33, + KeyF34 = Qt::Key_F34, + KeyF35 = Qt::Key_F35, + + // Mouse Keys + MouseMove = 1 << 16, + MouseLeft = 2 << 16, + MouseRight = 3 << 16, + MouseMiddle = 4 << 16, + MouseScroll = 5 << 16, + MouseScrollUp = 6 << 16, + MouseScrollDown = 7 << 16, + }; + + struct InputSequence + { + std::list keys {}; + + // This is intentionally left as an implicit conversion. For the intended use one UserInput + // is de facto InputSequence of length one. Therefore, there is no need to make that explicit. + explicit(false) InputSequence(UserInput key) + : InputSequence({key}) + {} + + explicit InputSequence(std::list keys) + : keys(std::move(keys)) + {} + + InputSequence(const std::initializer_list keys) + : keys(keys) + {} + }; + + QString message; + std::list sequences; +}; + +} // namespace Gui + +#endif // GUI_INPUTHINT_H diff --git a/src/Gui/InputHintPy.cpp b/src/Gui/InputHintPy.cpp new file mode 100644 index 0000000000..069a734281 --- /dev/null +++ b/src/Gui/InputHintPy.cpp @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include "InputHint.h" +#include "InputHintPy.h" + +void Gui::registerUserInputEnumInPython(PyObject* module) +{ + using enum Gui::InputHint::UserInput; + + constexpr const char* name = "UserInput"; + + PyObject* py_enum_module = PyImport_ImportModule("enum"); + if (!py_enum_module) { + return; + } + + PyObject* py_constants_dict = PyDict_New(); + + // clang-format off + // this structure is repetition of each and every UserInput enum case + // it can be regenerated by copying and pasting all entries and performing one substitution: + // regex search: `(\w+) = .+?,` substitution: `{"$1", $1},` + const std::map userInputEntries { + // Modifier + {"ModifierShift", ModifierShift}, + {"ModifierCtrl", ModifierCtrl}, + {"ModifierAlt", ModifierAlt}, + {"ModifierMeta", ModifierMeta}, + + // Keyboard Keys + {"KeySpace", KeySpace}, + {"KeyExclam", KeyExclam}, + {"KeyQuoteDbl", KeyQuoteDbl}, + {"KeyNumberSign", KeyNumberSign}, + {"KeyDollar", KeyDollar}, + {"KeyPercent", KeyPercent}, + {"KeyAmpersand", KeyAmpersand}, + {"KeyApostrophe", KeyApostrophe}, + {"KeyParenLeft", KeyParenLeft}, + {"KeyParenRight", KeyParenRight}, + {"KeyAsterisk", KeyAsterisk}, + {"KeyPlus", KeyPlus}, + {"KeyComma", KeyComma}, + {"KeyMinus", KeyMinus}, + {"KeyPeriod", KeyPeriod}, + {"KeySlash", KeySlash}, + {"Key0", Key0}, + {"Key1", Key1}, + {"Key2", Key2}, + {"Key3", Key3}, + {"Key4", Key4}, + {"Key5", Key5}, + {"Key6", Key6}, + {"Key7", Key7}, + {"Key8", Key8}, + {"Key9", Key9}, + {"KeyColon", KeyColon}, + {"KeySemicolon", KeySemicolon}, + {"KeyLess", KeyLess}, + {"KeyEqual", KeyEqual}, + {"KeyGreater", KeyGreater}, + {"KeyQuestion", KeyQuestion}, + {"KeyAt", KeyAt}, + {"KeyA", KeyA}, + {"KeyB", KeyB}, + {"KeyC", KeyC}, + {"KeyD", KeyD}, + {"KeyE", KeyE}, + {"KeyF", KeyF}, + {"KeyG", KeyG}, + {"KeyH", KeyH}, + {"KeyI", KeyI}, + {"KeyJ", KeyJ}, + {"KeyK", KeyK}, + {"KeyL", KeyL}, + {"KeyM", KeyM}, + {"KeyN", KeyN}, + {"KeyO", KeyO}, + {"KeyP", KeyP}, + {"KeyQ", KeyQ}, + {"KeyR", KeyR}, + {"KeyS", KeyS}, + {"KeyT", KeyT}, + {"KeyU", KeyU}, + {"KeyV", KeyV}, + {"KeyW", KeyW}, + {"KeyX", KeyX}, + {"KeyY", KeyY}, + {"KeyZ", KeyZ}, + {"KeyBracketLeft", KeyBracketLeft}, + {"KeyBackslash", KeyBackslash}, + {"KeyBracketRight", KeyBracketRight}, + {"KeyAsciiCircum", KeyAsciiCircum}, + {"KeyUnderscore", KeyUnderscore}, + {"KeyQuoteLeft", KeyQuoteLeft}, + {"KeyBraceLeft", KeyBraceLeft}, + {"KeyBar", KeyBar}, + {"KeyBraceRight", KeyBraceRight}, + {"KeyAsciiTilde", KeyAsciiTilde}, + + // misc keys + {"KeyEscape", KeyEscape}, + {"KeyTab", KeyTab}, + {"KeyBacktab", KeyBacktab}, + {"KeyBackspace", KeyBackspace}, + {"KeyReturn", KeyReturn}, + {"KeyEnter", KeyEnter}, + {"KeyInsert", KeyInsert}, + {"KeyDelete", KeyDelete}, + {"KeyPause", KeyPause}, + {"KeyPrintScr", KeyPrintScr}, + {"KeySysReq", KeySysReq}, + {"KeyClear", KeyClear}, + + // cursor movement + {"KeyHome", KeyHome}, + {"KeyEnd", KeyEnd}, + {"KeyLeft", KeyLeft}, + {"KeyUp", KeyUp}, + {"KeyRight", KeyRight}, + {"KeyDown", KeyDown}, + {"KeyPageUp", KeyPageUp}, + {"KeyPageDown", KeyPageDown}, + + // modifiers + {"KeyShift", KeyShift}, + {"KeyControl", KeyControl}, + {"KeyMeta", KeyMeta}, + {"KeyAlt", KeyAlt}, + {"KeyCapsLock", KeyCapsLock}, + {"KeyNumLock", KeyNumLock}, + {"KeyScrollLock", KeyScrollLock}, + + // function keys + {"KeyF1", KeyF1}, + {"KeyF2", KeyF2}, + {"KeyF3", KeyF3}, + {"KeyF4", KeyF4}, + {"KeyF5", KeyF5}, + {"KeyF6", KeyF6}, + {"KeyF7", KeyF7}, + {"KeyF8", KeyF8}, + {"KeyF9", KeyF9}, + {"KeyF10", KeyF10}, + {"KeyF11", KeyF11}, + {"KeyF12", KeyF12}, + {"KeyF13", KeyF13}, + {"KeyF14", KeyF14}, + {"KeyF15", KeyF15}, + {"KeyF16", KeyF16}, + {"KeyF17", KeyF17}, + {"KeyF18", KeyF18}, + {"KeyF19", KeyF19}, + {"KeyF20", KeyF20}, + {"KeyF21", KeyF21}, + {"KeyF22", KeyF22}, + {"KeyF23", KeyF23}, + {"KeyF24", KeyF24}, + {"KeyF25", KeyF25}, + {"KeyF26", KeyF26}, + {"KeyF27", KeyF27}, + {"KeyF28", KeyF28}, + {"KeyF29", KeyF29}, + {"KeyF30", KeyF30}, + {"KeyF31", KeyF31}, + {"KeyF32", KeyF32}, + {"KeyF33", KeyF33}, + {"KeyF34", KeyF34}, + {"KeyF35", KeyF35}, + + // Mouse Keys + {"MouseMove", MouseMove}, + {"MouseLeft", MouseLeft}, + {"MouseRight", MouseRight}, + {"MouseMiddle", MouseMiddle}, + {"MouseScroll", MouseScroll}, + {"MouseScrollUp", MouseScrollUp}, + {"MouseScrollDown", MouseScrollDown}, + }; + // clang-format on + + // Populate dictionary + for (const auto& [key, value] : userInputEntries) { + PyDict_SetItemString(py_constants_dict, key, PyLong_FromLong(static_cast(value))); + } + + PyObject* py_enum_class = PyObject_CallMethod(py_enum_module, "IntEnum", "sO", name, py_constants_dict); + + Py_CLEAR(py_constants_dict); + Py_CLEAR(py_enum_module); + + if (py_enum_class && PyModule_AddObject(module, name, py_enum_class) < 0) { + Py_CLEAR(py_enum_class); + } +} diff --git a/src/Gui/InputHintPy.h b/src/Gui/InputHintPy.h new file mode 100644 index 0000000000..7c00722dcc --- /dev/null +++ b/src/Gui/InputHintPy.h @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef INPUTHINTPY_H +#define INPUTHINTPY_H + +#include + +namespace Gui +{ + void registerUserInputEnumInPython(PyObject* module); +} // Namespace Gui + +#endif //INPUTHINTPY_H diff --git a/src/Gui/InputHintWidget.cpp b/src/Gui/InputHintWidget.cpp new file mode 100644 index 0000000000..146cc9f1bc --- /dev/null +++ b/src/Gui/InputHintWidget.cpp @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#include "PreCompiled.h" +#ifndef _PreComp_ +# include +# include +#endif + +#include + +#include "InputHint.h" +#include "InputHintWidget.h" + +Gui::InputHintWidget::InputHintWidget(QWidget* parent) : QLabel(parent) +{} + +void Gui::InputHintWidget::showHints(const std::list& hints) +{ + if (hints.empty()) { + clearHints(); + return; + } + + const auto getKeyImage = [this](InputHint::UserInput key) { + const auto& factory = BitmapFactory(); + + QPixmap image = [&] { + QColor color = palette().text().color(); + + if (auto iconPath = getCustomIconPath(key)) { + return factory.pixmapFromSvg(*iconPath, + QSize(24, 24), + {{0xFFFFFF, color.rgb() & RGB_MASK}}); + } + + return generateKeyIcon(key, color); + }(); + + + QBuffer buffer; + image.save(&buffer, "png"); + + return QStringLiteral("") + .arg(QLatin1String(buffer.data().toBase64())) + .arg(image.width()); + }; + + const auto getHintHTML = [&](const InputHint& hint) { + QString message = QStringLiteral("%1").arg(hint.message); + + for (const auto& sequence : hint.sequences) { + QList keyImages; + + for (const auto key : sequence.keys) { + keyImages.append(getKeyImage(key)); + } + + message = message.arg(keyImages.join(QString {})); + } + + return message; + }; + + QStringList messages; + for (const auto& hint : hints) { + messages.append(getHintHTML(hint)); + } + + QString html = QStringLiteral("" + "%1" + "
"); + + setText(html.arg(messages.join(QStringLiteral("")))); +} + +void Gui::InputHintWidget::clearHints() +{ + setText({}); +} + +std::optional Gui::InputHintWidget::getCustomIconPath(const InputHint::UserInput key) +{ + switch (key) { + case InputHint::UserInput::MouseLeft: + return ":/icons/user-input/mouse-left.svg"; + case InputHint::UserInput::MouseRight: + return ":/icons/user-input/mouse-right.svg"; + case InputHint::UserInput::MouseMove: + return ":/icons/user-input/mouse-move.svg"; + case InputHint::UserInput::MouseMiddle: + return ":/icons/user-input/mouse-middle.svg"; + case InputHint::UserInput::MouseScroll: + return ":/icons/user-input/mouse-scroll.svg"; + case InputHint::UserInput::MouseScrollDown: + return ":/icons/user-input/mouse-scroll-down.svg"; + case InputHint::UserInput::MouseScrollUp: + return ":/icons/user-input/mouse-scroll-up.svg"; + default: + return std::nullopt; + } +} + +QPixmap Gui::InputHintWidget::generateKeyIcon(const InputHint::UserInput key, const QColor color) +{ + constexpr int margin = 3; + constexpr int padding = 4; + constexpr int radius = 2; + constexpr int iconTotalHeight = 24; + constexpr int iconSymbolHeight = iconTotalHeight - 2 * margin; + + const QFont font(QStringLiteral("sans"), 10, QFont::Bold); + const QFontMetrics fm(font); + const QString text = inputRepresentation(key); + const QRect textBoundingRect = fm.tightBoundingRect(text); + + const int symbolWidth = std::max(textBoundingRect.width() + padding * 2, iconSymbolHeight); + + const QRect keyRect(margin, margin, symbolWidth, 18); + + QPixmap pixmap(symbolWidth + margin * 2, iconTotalHeight); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing); + painter.setPen(QPen(color, 2)); + painter.setFont(font); + painter.drawRoundedRect(keyRect, radius, radius); + painter.drawText( + // adjust the rectangle so it is visually centered + // this is important for characters that are below baseline + keyRect.translated(0, -(textBoundingRect.y() + textBoundingRect.height()) / 2), + Qt::AlignHCenter, + text); + + return pixmap; +} + +QString Gui::InputHintWidget::inputRepresentation(const InputHint::UserInput key) +{ + using enum InputHint::UserInput; + + // clang-format off + switch (key) { + // Keyboard Keys + case KeySpace: return tr("Space"); + case KeyExclam: return QStringLiteral("!"); + case KeyQuoteDbl: return QStringLiteral("\""); + case KeyNumberSign: return QStringLiteral("-/+"); + case KeyDollar: return QStringLiteral("$"); + case KeyPercent: return QStringLiteral("%"); + case KeyAmpersand: return QStringLiteral("&"); + case KeyApostrophe: return QStringLiteral("\'"); + case KeyParenLeft: return QStringLiteral("("); + case KeyParenRight: return QStringLiteral(")"); + case KeyAsterisk: return QStringLiteral("*"); + case KeyPlus: return QStringLiteral("+"); + case KeyComma: return QStringLiteral(","); + case KeyMinus: return QStringLiteral("-"); + case KeyPeriod: return QStringLiteral("."); + case KeySlash: return QStringLiteral("/"); + case Key0: return QStringLiteral("0"); + case Key1: return QStringLiteral("1"); + case Key2: return QStringLiteral("2"); + case Key3: return QStringLiteral("3"); + case Key4: return QStringLiteral("4"); + case Key5: return QStringLiteral("5"); + case Key6: return QStringLiteral("6"); + case Key7: return QStringLiteral("7"); + case Key8: return QStringLiteral("8"); + case Key9: return QStringLiteral("9"); + case KeyColon: return QStringLiteral(":"); + case KeySemicolon: return QStringLiteral(";"); + case KeyLess: return QStringLiteral("<"); + case KeyEqual: return QStringLiteral("="); + case KeyGreater: return QStringLiteral(">"); + case KeyQuestion: return QStringLiteral("?"); + case KeyAt: return QStringLiteral("@"); + case KeyA: return QStringLiteral("A"); + case KeyB: return QStringLiteral("B"); + case KeyC: return QStringLiteral("C"); + case KeyD: return QStringLiteral("D"); + case KeyE: return QStringLiteral("E"); + case KeyF: return QStringLiteral("F"); + case KeyG: return QStringLiteral("G"); + case KeyH: return QStringLiteral("H"); + case KeyI: return QStringLiteral("I"); + case KeyJ: return QStringLiteral("J"); + case KeyK: return QStringLiteral("K"); + case KeyL: return QStringLiteral("L"); + case KeyM: return QStringLiteral("M"); + case KeyN: return QStringLiteral("N"); + case KeyO: return QStringLiteral("O"); + case KeyP: return QStringLiteral("P"); + case KeyQ: return QStringLiteral("Q"); + case KeyR: return QStringLiteral("R"); + case KeyS: return QStringLiteral("S"); + case KeyT: return QStringLiteral("T"); + case KeyU: return QStringLiteral("U"); + case KeyV: return QStringLiteral("V"); + case KeyW: return QStringLiteral("W"); + case KeyX: return QStringLiteral("X"); + case KeyY: return QStringLiteral("Y"); + case KeyZ: return QStringLiteral("Z"); + case KeyBracketLeft: return QStringLiteral("["); + case KeyBackslash: return QStringLiteral("\\"); + case KeyBracketRight: return QStringLiteral("]"); + case KeyUnderscore: return QStringLiteral("_"); + case KeyQuoteLeft: return QStringLiteral("\""); + case KeyBraceLeft: return QStringLiteral("{"); + case KeyBar: return QStringLiteral("|"); + case KeyBraceRight: return QStringLiteral("}"); + case KeyAsciiTilde: return QStringLiteral("~"); + + // misc keys + case KeyEscape: return tr("Escape"); + case KeyTab: return tr("tab ⭾"); + case KeyBacktab: return tr("Backtab"); + case KeyBackspace: return tr("⌫"); + case KeyReturn: return tr("↵ Enter"); + case KeyEnter: return tr("Enter"); + case KeyInsert: return tr("Insert"); + case KeyDelete: return tr("Delete"); + case KeyPause: return tr("Pause"); + case KeyPrintScr: return tr("Print"); + case KeySysReq: return tr("SysReq"); + case KeyClear: return tr("Clear"); + + // cursor movement + case KeyHome: return tr("Home"); + case KeyEnd: return tr("End"); + case KeyLeft: return QStringLiteral("←"); + case KeyUp: return QStringLiteral("↑"); + case KeyRight: return QStringLiteral("→"); + case KeyDown: return QStringLiteral("↓"); + case KeyPageUp: return tr("PgDown"); + case KeyPageDown: return tr("PgUp"); + + // modifiers +#ifdef FC_OS_MACOSX + case KeyShift: return QStringLiteral("⇧"); + case KeyControl: return QStringLiteral("⌘"); + case KeyMeta: return QStringLiteral("⌃"); + case KeyAlt: return QStringLiteral("⌥"); +#else + case KeyShift: return tr("Shift"); + case KeyControl: return tr("Ctrl"); +#ifdef FC_OS_WIN32 + case KeyMeta: return tr("⊞ Win"); +#else + case KeyMeta: return tr("❖ Meta"); +#endif + case KeyAlt: return tr("Alt"); +#endif + case KeyCapsLock: return tr("Caps Lock"); + case KeyNumLock: return tr("Num Lock"); + case KeyScrollLock: return tr("Scroll Lock"); + + // function + case KeyF1: return QStringLiteral("F1"); + case KeyF2: return QStringLiteral("F2"); + case KeyF3: return QStringLiteral("F3"); + case KeyF4: return QStringLiteral("F4"); + case KeyF5: return QStringLiteral("F5"); + case KeyF6: return QStringLiteral("F6"); + case KeyF7: return QStringLiteral("F7"); + case KeyF8: return QStringLiteral("F8"); + case KeyF9: return QStringLiteral("F9"); + case KeyF10: return QStringLiteral("F10"); + case KeyF11: return QStringLiteral("F11"); + case KeyF12: return QStringLiteral("F12"); + case KeyF13: return QStringLiteral("F13"); + case KeyF14: return QStringLiteral("F14"); + case KeyF15: return QStringLiteral("F15"); + case KeyF16: return QStringLiteral("F16"); + case KeyF17: return QStringLiteral("F17"); + case KeyF18: return QStringLiteral("F18"); + case KeyF19: return QStringLiteral("F19"); + case KeyF20: return QStringLiteral("F20"); + case KeyF21: return QStringLiteral("F21"); + case KeyF22: return QStringLiteral("F22"); + case KeyF23: return QStringLiteral("F23"); + case KeyF24: return QStringLiteral("F24"); + case KeyF25: return QStringLiteral("F25"); + case KeyF26: return QStringLiteral("F26"); + case KeyF27: return QStringLiteral("F27"); + case KeyF28: return QStringLiteral("F28"); + case KeyF29: return QStringLiteral("F29"); + case KeyF30: return QStringLiteral("F30"); + case KeyF31: return QStringLiteral("F31"); + case KeyF32: return QStringLiteral("F32"); + case KeyF33: return QStringLiteral("F33"); + case KeyF34: return QStringLiteral("F34"); + case KeyF35: return QStringLiteral("F35"); + + default: return tr("???"); + } + // clang-format on +} diff --git a/src/Gui/InputHintWidget.h b/src/Gui/InputHintWidget.h new file mode 100644 index 0000000000..c9e86c1d44 --- /dev/null +++ b/src/Gui/InputHintWidget.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: LGPL-2.1-or-later +/**************************************************************************** + * * + * Copyright (c) 2025 Kacper Donat * + * * + * This file is part of FreeCAD. * + * * + * FreeCAD is free software: you can redistribute it and/or modify it * + * under the terms of the GNU Lesser General Public License as * + * published by the Free Software Foundation, either version 2.1 of the * + * License, or (at your option) any later version. * + * * + * FreeCAD is distributed in the hope that it will be useful, but * + * WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * + * Lesser General Public License for more details. * + * * + * You should have received a copy of the GNU Lesser General Public * + * License along with FreeCAD. If not, see * + * . * + * * + ***************************************************************************/ + +#ifndef INPUTHINTWIDGET_H +#define INPUTHINTWIDGET_H + +#include +#include + +#include + +#include "InputHint.h" + +namespace Gui +{ +class GuiExport InputHintWidget : public QLabel +{ + Q_OBJECT + +public: + explicit InputHintWidget(QWidget *parent); + + void showHints(const std::list& hints); + void clearHints(); + +private: + static std::optional getCustomIconPath(InputHint::UserInput key); + static QPixmap generateKeyIcon(InputHint::UserInput key, QColor color); + static QString inputRepresentation(InputHint::UserInput key); +}; + +} // Namespace Gui +#endif //INPUTHINTWIDGET_H diff --git a/src/Gui/MainWindow.cpp b/src/Gui/MainWindow.cpp index c2a664d48f..ab2cf7184c 100644 --- a/src/Gui/MainWindow.cpp +++ b/src/Gui/MainWindow.cpp @@ -93,6 +93,7 @@ #include "DockWindowManager.h" #include "DownloadManager.h" #include "FileDialog.h" +#include "InputHintWidget.h" #include "MenuManager.h" #include "ModuleIO.h" #include "NotificationArea.h" @@ -117,6 +118,7 @@ #include "View3DInventor.h" #include "View3DInventorViewer.h" #include "Dialogs/DlgObjectSelection.h" + #include FC_LOG_LEVEL_INIT("MainWindow",false,true,true) @@ -280,6 +282,7 @@ struct MainWindowP { DimensionWidget* sizeLabel; QLabel* actionLabel; + InputHintWidget* hintLabel; QLabel* rightSideLabel; QTimer* actionTimer; QTimer* statusTimer; @@ -396,6 +399,11 @@ MainWindow::MainWindow(QWidget * parent, Qt::WindowFlags f) statusBar()->addPermanentWidget(progressBar, 0); statusBar()->addPermanentWidget(d->sizeLabel, 0); + // hint label + d->hintLabel = new InputHintWidget(statusBar()); + statusBar()->addWidget(d->hintLabel); + + // right side label d->rightSideLabel = new QLabel(statusBar()); d->rightSideLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); statusBar()->addPermanentWidget(d->rightSideLabel); @@ -2255,6 +2263,15 @@ void MainWindow::showStatus(int type, const QString& message) statusBar()->showMessage(msg.simplified(), timeout); } +void MainWindow::showHints(const std::list& hints) +{ + d->hintLabel->showHints(hints); +} + +void MainWindow::hideHints() +{ + d->hintLabel->clearHints(); +} // set text to the pane void MainWindow::setPaneText(int i, QString text) diff --git a/src/Gui/MainWindow.h b/src/Gui/MainWindow.h index ac49d55a2d..f6ea2791f1 100644 --- a/src/Gui/MainWindow.h +++ b/src/Gui/MainWindow.h @@ -29,6 +29,7 @@ #include #include "Window.h" +#include "InputHint.h" class QMimeData; class QUrl; @@ -201,7 +202,10 @@ public: void updateActions(bool delay = false); enum StatusType {None, Err, Wrn, Pane, Msg, Log, Tmp, Critical}; - void showStatus(int type, const QString & message); + void showStatus(int type, const QString& message); + + void showHints(const std::list& hints = {}); + void hideHints(); void initDockWindows(bool show); diff --git a/src/Gui/MainWindowPy.cpp b/src/Gui/MainWindowPy.cpp index 3a9a12e844..845fe6cb32 100644 --- a/src/Gui/MainWindowPy.cpp +++ b/src/Gui/MainWindowPy.cpp @@ -56,6 +56,8 @@ void MainWindowPy::init_type() add_varargs_method("getActiveWindow", &MainWindowPy::getActiveWindow, "getActiveWindow()"); add_varargs_method("addWindow", &MainWindowPy::addWindow, "addWindow(MDIView)"); add_varargs_method("removeWindow", &MainWindowPy::removeWindow, "removeWindow(MDIView)"); + add_varargs_method("showHint",&MainWindowPy::showHint,"showHint(hint)"); + add_varargs_method("hideHint",&MainWindowPy::hideHint,"hideHint()"); } PyObject *MainWindowPy::extension_object_new(struct _typeobject * /*type*/, PyObject * /*args*/, PyObject * /*kwds*/) @@ -87,7 +89,16 @@ Py::Object MainWindowPy::createWrapper(MainWindow *mw) } // copy attributes - std::list attr = {"getWindows", "getWindowsOfType", "setActiveWindow", "getActiveWindow", "addWindow", "removeWindow"}; + static constexpr std::initializer_list attr = { + "getWindows", + "getWindowsOfType", + "setActiveWindow", + "getActiveWindow", + "addWindow", + "removeWindow", + "showHint", + "hideHint", + }; Py::Object py = wrap.fromQWidget(mw, "QMainWindow"); Py::ExtensionObject inst(create(mw)); @@ -215,3 +226,57 @@ Py::Object MainWindowPy::removeWindow(const Py::Tuple& args) } return Py::None(); } + +Py::Object MainWindowPy::showHint(const Py::Tuple& args) +{ + static auto userInputFromPyObject = [](const Py::Object& object) -> InputHint::UserInput { + Py::Long value(object.getAttr("value")); + + return static_cast(value.as_long()); + }; + + static auto inputSequenceFromPyObject = [](const Py::Object& sequence) -> InputHint::InputSequence { + if (sequence.isTuple()) { + Py::Tuple pyInputs(sequence); + + std::list inputs; + for (auto pyInput : pyInputs) { + inputs.push_back(userInputFromPyObject(pyInput)); + } + + return InputHint::InputSequence(inputs); + } + + return userInputFromPyObject(sequence); + }; + + static auto hintFromPyObject = [](const Py::Object& object) -> InputHint { + Py::List pySequences = object.getAttr("sequences"); + + std::list sequences; + for (auto pySequence : pySequences) { + sequences.push_back(inputSequenceFromPyObject(pySequence)); + } + + return InputHint { + .message = QString::fromStdString(object.getAttr("message").as_string()), + .sequences = sequences + }; + }; + + std::list hints; + for (auto arg : args) { + hints.push_back(hintFromPyObject(arg)); + } + + _mw->showHints(hints); + + return Py::None(); +} + +Py::Object MainWindowPy::hideHint(const Py::Tuple&) +{ + _mw->hideHints(); + + return Py::None(); +} diff --git a/src/Gui/MainWindowPy.h b/src/Gui/MainWindowPy.h index 398dc1a0a3..c28571a8ef 100644 --- a/src/Gui/MainWindowPy.h +++ b/src/Gui/MainWindowPy.h @@ -54,6 +54,9 @@ public: Py::Object addWindow(const Py::Tuple&); Py::Object removeWindow(const Py::Tuple&); + Py::Object showHint(const Py::Tuple&); + Py::Object hideHint(const Py::Tuple&); + private: QPointer _mw; }; diff --git a/src/Gui/ToolHandler.cpp b/src/Gui/ToolHandler.cpp index 182fa8c6d2..23c7302ef8 100644 --- a/src/Gui/ToolHandler.cpp +++ b/src/Gui/ToolHandler.cpp @@ -41,6 +41,7 @@ #include "View3DInventorViewer.h" #include "ToolHandler.h" +#include "InputHint.h" using namespace Gui; @@ -59,6 +60,7 @@ bool ToolHandler::activate() oldCursor = cw->cursor(); updateCursor(); + updateHint(); this->preActivated(); this->activated(); @@ -74,6 +76,8 @@ void ToolHandler::deactivate() this->postDeactivated(); unsetCursor(); + + Gui::MainWindow::getInstance()->hideHints(); } //************************************************************************** @@ -233,6 +237,16 @@ void ToolHandler::updateCursor() } } +std::list ToolHandler::getToolHints() const +{ + return {}; +} + +void ToolHandler::updateHint() const +{ + Gui::getMainWindow()->showHints(getToolHints()); +} + void ToolHandler::applyCursor() { applyCursor(actCursor); diff --git a/src/Gui/ToolHandler.h b/src/Gui/ToolHandler.h index 11803c7561..88189c8b0c 100644 --- a/src/Gui/ToolHandler.h +++ b/src/Gui/ToolHandler.h @@ -35,7 +35,7 @@ namespace Gui { class View3DInventorViewer; - +struct InputHint; class GuiExport ToolHandler { @@ -53,6 +53,9 @@ public: /// enabling to set data member dependent icons (i.e. for different construction methods) void updateCursor(); + virtual std::list getToolHints() const; + void updateHint() const; + private: // NVI virtual void preActivated() {} diff --git a/src/Mod/Sketcher/Gui/DrawSketchControllableHandler.h b/src/Mod/Sketcher/Gui/DrawSketchControllableHandler.h index 6b07bd3baf..c1fa7151a2 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchControllableHandler.h +++ b/src/Mod/Sketcher/Gui/DrawSketchControllableHandler.h @@ -160,7 +160,10 @@ private: bool onModeChanged() override { DrawSketchHandler::resetPositionText(); + DrawSketchHandler::updateHint(); + toolWidgetManager.onHandlerModeChanged(); + if (DSDefaultHandler::onModeChanged()) { // If onModeChanged returns false, then the handler has been purged. toolWidgetManager.afterHandlerModeChanged(); diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerLineSet.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerLineSet.h index 37b0f6834e..f113e59e81 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerLineSet.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerLineSet.h @@ -96,6 +96,17 @@ public: SNAP_MODE_45Degree }; + std::list getToolHints() const override + { + using UserInput = Gui::InputHint::UserInput; + + return { + {QWidget::tr("%1 change mode"), {UserInput::KeyM}}, + {QWidget::tr("%1 start drawing"), {UserInput::MouseLeft}}, + {QWidget::tr("%1 stop drawing"), {UserInput::MouseRight}}, + }; + } + void registerPressedKey(bool pressed, int key) override { if (Mode == STATUS_SEEK_Second && key == SoKeyboardEvent::M && pressed diff --git a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h index 54ce11d364..cf2d23d69e 100644 --- a/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h +++ b/src/Mod/Sketcher/Gui/DrawSketchHandlerPolygon.h @@ -74,6 +74,29 @@ public: {} ~DrawSketchHandlerPolygon() override = default; + std::list getToolHints() const override + { + using UserInput = Gui::InputHint::UserInput; + + switch (state()) { + case SelectMode::SeekFirst: + return { + {QWidget::tr("%1 pick polygon center"), {UserInput::MouseLeft}}, + {QWidget::tr("%1/%2 increase / decrease number of sides"), + {UserInput::KeyU, UserInput::KeyJ}}, + }; + case SelectMode::SeekSecond: + return { + {QWidget::tr("%1 pick rotation and size"), {UserInput::MouseMove}}, + {QWidget::tr("%1 confirm"), {UserInput::MouseLeft}}, + {QWidget::tr("%1/%2 increase / decrease number of sides"), + {UserInput::KeyU, UserInput::KeyJ}}, + }; + default: + return {}; + } + } + private: void updateDataAndDrawToPosition(Base::Vector2d onSketchPos) override { From 239173bcf2a76af1950e5825e2f511a71a95673c Mon Sep 17 00:00:00 2001 From: Kacper Donat Date: Sun, 18 May 2025 15:10:14 +0200 Subject: [PATCH 2/2] Draft: Add example tool hints --- src/Mod/Draft/draftguitools/gui_arcs.py | 30 +++++++++++++++++++ .../Draft/draftguitools/gui_base_original.py | 13 ++++++++ 2 files changed, 43 insertions(+) diff --git a/src/Mod/Draft/draftguitools/gui_arcs.py b/src/Mod/Draft/draftguitools/gui_arcs.py index 038038bb1d..dbc6e92411 100644 --- a/src/Mod/Draft/draftguitools/gui_arcs.py +++ b/src/Mod/Draft/draftguitools/gui_arcs.py @@ -291,6 +291,36 @@ class Arc(gui_base_original.Creator): self.step = 4 self.drawArc() + self.updateHints() + + def getHints(self): + hint_global = Gui.InputHint(translate("draft", "%1 toggle global"), Gui.UserInput.KeyG) + hint_continue = Gui.InputHint(translate("draft", "%1 toggle continue"), Gui.UserInput.KeyN) + + if self.step == 0: + return [ + Gui.InputHint(translate("draft", "%1 pick center"), Gui.UserInput.MouseLeft), + hint_global, + hint_continue, + ] + elif self.step == 1: + return [ + Gui.InputHint(translate("draft", "%1 pick radius"), Gui.UserInput.MouseLeft), + hint_continue, + ] + elif self.step == 2: + return [ + Gui.InputHint(translate("draft", "%1 pick staring angle"), Gui.UserInput.MouseLeft), + hint_continue, + ] + elif self.step == 3: + return [ + Gui.InputHint(translate("draft", "%1 pick aperture"), Gui.UserInput.MouseLeft), + hint_continue, + ] + else: + return [] + def drawArc(self): """Actually draw the arc object.""" rot, sup, pts, fil = self.getStrings() diff --git a/src/Mod/Draft/draftguitools/gui_base_original.py b/src/Mod/Draft/draftguitools/gui_base_original.py index b9f10c8eb2..27789bc5a3 100644 --- a/src/Mod/Draft/draftguitools/gui_base_original.py +++ b/src/Mod/Draft/draftguitools/gui_base_original.py @@ -136,6 +136,17 @@ class DraftTool: _toolmsg("{}".format(16*"-")) _toolmsg("GuiCommand: {}".format(self.featureName)) + # update hints after the tool is fully initialized + QtCore.QTimer.singleShot(0, self.updateHints) + + def updateHints(self): + Gui.HintManager.show(*self.getHints()) + + def getHints(self): + return [ + Gui.InputHint("%1 constrain", Gui.UserInput.KeyShift) + ] + def end_callbacks(self, call): try: self.view.removeEventCallback("SoEvent", call) @@ -184,6 +195,8 @@ class DraftTool: todo.ToDo.delayCommit(self.commitList) self.commitList = [] + Gui.HintManager.hide() + def commit(self, name, func): """Store actions in the commit list to be run later.