Merge pull request #18961 from kadet1090/tool-hints

This commit is contained in:
Benjamin Nauck
2025-05-21 18:46:10 +02:00
committed by GitHub
28 changed files with 1253 additions and 5 deletions

View File

@@ -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");

View File

@@ -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();

View File

@@ -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

View File

@@ -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

View File

@@ -305,6 +305,13 @@
<file>overlay_error.svg</file>
<file>feature_suppressed.svg</file>
<file>forbidden.svg</file>
<file>user-input/mouse-left.svg</file>
<file>user-input/mouse-right.svg</file>
<file>user-input/mouse-move.svg</file>
<file>user-input/mouse-middle.svg</file>
<file>user-input/mouse-scroll.svg</file>
<file>user-input/mouse-scroll-up.svg</file>
<file>user-input/mouse-scroll-down.svg</file>
</qresource>
<qresource prefix="/icons/FreeCAD-default">
<file>index.theme</file>

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v7m-6 0h12M6 7a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V7Z"/>
<path fill="#ffffff" d="M12 3v7H6V6l3-3h3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
sodipodi:docname="mouse-middle.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="25.104167"
inkscape:cx="11.213278"
inkscape:cy="12.487967"
inkscape:window-width="2560"
inkscape:window-height="1414"
inkscape:window-x="0"
inkscape:window-y="26"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
stroke="#ffffff"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M 12,7.0481328 V 11.155187 M 6,7 C 6,4.790861 7.790861,3 10,3 h 4 c 2.209139,0 4,1.790861 4,4 v 10 c 0,2.209139 -1.790861,4 -4,4 H 10 C 7.790861,21 6,19.209139 6,17 Z"
id="path1"
sodipodi:nodetypes="ccsssssssss" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v7m-6 0h12M6 7a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V7Z"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v7m-6 0h12M6 7a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V7Z"/>
<path fill="#ffffff" d="M12 3v7h6V6l-3-3h-3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 335 B

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
sodipodi:docname="mouse-scroll-down.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="50.208333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="2560"
inkscape:window-height="1414"
inkscape:window-x="0"
inkscape:window-y="26"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
stroke="#ffffff"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M 12,5.873029 V 10 M 13.5,12 12,13.5 10.5,12 M 6,7 C 6,4.790861 7.790861,3 10,3 h 4 c 2.209139,0 4,1.790861 4,4 v 10 c 0,2.209139 -1.790861,4 -4,4 H 10 C 7.790861,21 6,19.209139 6,17 Z"
id="path1"
sodipodi:nodetypes="cccccsssssssss" />
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
sodipodi:docname="mouse-scroll-up.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="50.208333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="2560"
inkscape:window-height="1414"
inkscape:window-x="0"
inkscape:window-y="26"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
stroke="#ffffff"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m 12,9 v 3 M 10.5,7 12,5.5 13.5,7 M 6,7 C 6,4.790861 7.790861,3 10,3 h 4 c 2.209139,0 4,1.790861 4,4 v 10 c 0,2.209139 -1.790861,4 -4,4 H 10 C 7.790861,21 6,19.209139 6,17 Z"
id="path1"
sodipodi:nodetypes="cccccsssssssss" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
<path stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v1m1.5 2L12 13.5 10.5 12m0-5L12 5.5 13.5 7M6 7a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v10a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V7Z"/>
</svg>

After

Width:  |  Height:  |  Size: 317 B

222
src/Gui/InputHint.h Normal file
View File

@@ -0,0 +1,222 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2024 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef GUI_INPUTHINT_H
#define GUI_INPUTHINT_H
#include "FCGlobal.h"
#include <Qt>
#include <QString>
#include <list>
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<UserInput> 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<UserInput> keys)
: keys(std::move(keys))
{}
InputSequence(const std::initializer_list<UserInput> keys)
: keys(keys)
{}
};
QString message;
std::list<InputSequence> sequences;
};
} // namespace Gui
#endif // GUI_INPUTHINT_H

218
src/Gui/InputHintPy.cpp Normal file
View File

@@ -0,0 +1,218 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#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<const char*, InputHint::UserInput> 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<int>(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);
}
}

34
src/Gui/InputHintPy.h Normal file
View File

@@ -0,0 +1,34 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef INPUTHINTPY_H
#define INPUTHINTPY_H
#include <Base/PyObjectBase.h>
namespace Gui
{
void registerUserInputEnumInPython(PyObject* module);
} // Namespace Gui
#endif //INPUTHINTPY_H

319
src/Gui/InputHintWidget.cpp Normal file
View File

@@ -0,0 +1,319 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#include "PreCompiled.h"
#ifndef _PreComp_
# include <QBuffer>
# include <QPainter>
#endif
#include <BitmapFactory.h>
#include "InputHint.h"
#include "InputHintWidget.h"
Gui::InputHintWidget::InputHintWidget(QWidget* parent) : QLabel(parent)
{}
void Gui::InputHintWidget::showHints(const std::list<InputHint>& 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("<img src=\"data:image/png;base64,%1\" width=%2 height=24 />")
.arg(QLatin1String(buffer.data().toBase64()))
.arg(image.width());
};
const auto getHintHTML = [&](const InputHint& hint) {
QString message = QStringLiteral("<td valign=bottom>%1</td>").arg(hint.message);
for (const auto& sequence : hint.sequences) {
QList<QString> 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("<table style=\"line-height: 28px\" height=28>"
"<tr>%1</tr>"
"</table>");
setText(html.arg(messages.join(QStringLiteral("<td width=10></td>"))));
}
void Gui::InputHintWidget::clearHints()
{
setText({});
}
std::optional<const char*> 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
}

53
src/Gui/InputHintWidget.h Normal file
View File

@@ -0,0 +1,53 @@
// SPDX-License-Identifier: LGPL-2.1-or-later
/****************************************************************************
* *
* Copyright (c) 2025 Kacper Donat <kacper@kadet.net> *
* *
* 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 *
* <https://www.gnu.org/licenses/>. *
* *
***************************************************************************/
#ifndef INPUTHINTWIDGET_H
#define INPUTHINTWIDGET_H
#include <QLabel>
#include <optional>
#include <FCGlobal.h>
#include "InputHint.h"
namespace Gui
{
class GuiExport InputHintWidget : public QLabel
{
Q_OBJECT
public:
explicit InputHintWidget(QWidget *parent);
void showHints(const std::list<InputHint>& hints);
void clearHints();
private:
static std::optional<const char*> getCustomIconPath(InputHint::UserInput key);
static QPixmap generateKeyIcon(InputHint::UserInput key, QColor color);
static QString inputRepresentation(InputHint::UserInput key);
};
} // Namespace Gui
#endif //INPUTHINTWIDGET_H

View File

@@ -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 <Base/Color.h>
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<InputHint>& hints)
{
d->hintLabel->showHints(hints);
}
void MainWindow::hideHints()
{
d->hintLabel->clearHints();
}
// set text to the pane
void MainWindow::setPaneText(int i, QString text)

View File

@@ -29,6 +29,7 @@
#include <QMdiArea>
#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<InputHint>& hints = {});
void hideHints();
void initDockWindows(bool show);

View File

@@ -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<std::string> attr = {"getWindows", "getWindowsOfType", "setActiveWindow", "getActiveWindow", "addWindow", "removeWindow"};
static constexpr std::initializer_list<const char*> attr = {
"getWindows",
"getWindowsOfType",
"setActiveWindow",
"getActiveWindow",
"addWindow",
"removeWindow",
"showHint",
"hideHint",
};
Py::Object py = wrap.fromQWidget(mw, "QMainWindow");
Py::ExtensionObject<MainWindowPy> 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<InputHint::UserInput>(value.as_long());
};
static auto inputSequenceFromPyObject = [](const Py::Object& sequence) -> InputHint::InputSequence {
if (sequence.isTuple()) {
Py::Tuple pyInputs(sequence);
std::list<InputHint::UserInput> 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<InputHint::InputSequence> sequences;
for (auto pySequence : pySequences) {
sequences.push_back(inputSequenceFromPyObject(pySequence));
}
return InputHint {
.message = QString::fromStdString(object.getAttr("message").as_string()),
.sequences = sequences
};
};
std::list<InputHint> 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();
}

View File

@@ -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<MainWindow> _mw;
};

View File

@@ -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<InputHint> ToolHandler::getToolHints() const
{
return {};
}
void ToolHandler::updateHint() const
{
Gui::getMainWindow()->showHints(getToolHints());
}
void ToolHandler::applyCursor()
{
applyCursor(actCursor);

View File

@@ -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<InputHint> getToolHints() const;
void updateHint() const;
private: // NVI
virtual void preActivated()
{}

View File

@@ -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()

View File

@@ -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.

View File

@@ -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();

View File

@@ -96,6 +96,17 @@ public:
SNAP_MODE_45Degree
};
std::list<Gui::InputHint> 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

View File

@@ -74,6 +74,29 @@ public:
{}
~DrawSketchHandlerPolygon() override = default;
std::list<Gui::InputHint> 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
{