Draft: Implement hints for creation tools (#23244)

* Draft: Implement hints for creation tools

Fixes: #22886
Related: #22298

This PR implements hints for the tools in the Draft Creation toolbar.

* Fix confusing indentation

* Fix typo in comment
This commit is contained in:
Roy-043
2025-08-25 19:42:59 +02:00
committed by GitHub
parent a346c266e7
commit 416a2fe714
13 changed files with 243 additions and 51 deletions

View File

@@ -292,36 +292,7 @@ class Arc(gui_base_original.Creator):
else: # choose second angle
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 starting 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 []
self.update_hints()
def drawArc(self):
"""Actually draw the arc object."""
@@ -436,6 +407,7 @@ class Arc(gui_base_original.Creator):
self.step = 1
self.ui.setNextFocus()
_toolmsg(translate("draft", "Pick radius"))
self.update_hints()
def numericRadius(self, rad):
"""Validate the entry radius in the user interface.
@@ -494,6 +466,29 @@ class Arc(gui_base_original.Creator):
self.angle = math.radians(rad)
self.step = 4
self.drawArc()
self.update_hints()
def get_hints(self):
if self.step == 0:
hints = [
Gui.InputHint(translate("draft", "%1 pick center"), Gui.UserInput.MouseLeft)
]
elif self.step == 1:
hints = [
Gui.InputHint(translate("draft", "%1 pick radius"), Gui.UserInput.MouseLeft)
]
elif self.step == 2:
hints = [
Gui.InputHint(translate("draft", "%1 pick start angle"), Gui.UserInput.MouseLeft)
]
else:
hints = [
Gui.InputHint(translate("draft", "%1 pick aperture"), Gui.UserInput.MouseLeft)
]
return hints \
+ gui_tool_utils._get_hint_xyz_constrain() \
+ gui_tool_utils._get_hint_mod_constrain() \
+ gui_tool_utils._get_hint_mod_snap()
Gui.addCommand('Draft_Arc', Arc())
@@ -534,6 +529,7 @@ class Arc_3Points(gui_base.GuiCommandBase):
Gui.Snapper.ui.setTitle(title=translate("draft", "Arc From 3 Points"),
icon="Draft_Arc_3Points")
Gui.Snapper.ui.continueCmd.show()
self.update_hints()
def getPoint(self, point, info):
"""Get the point by clicking on the 3D view.
@@ -578,6 +574,7 @@ class Arc_3Points(gui_base.GuiCommandBase):
Gui.Snapper.ui.setTitle(title=translate("draft", "Arc From 3 Points"),
icon="Draft_Arc_3Points")
Gui.Snapper.ui.continueCmd.show()
self.update_hints()
else:
# If three points were already picked in the 3D view
@@ -631,6 +628,24 @@ class Arc_3Points(gui_base.GuiCommandBase):
if cont or (cont is None and Gui.Snapper.ui and Gui.Snapper.ui.continueMode):
self.Activated()
def get_hints(self):
if len(self.points) == 0:
hints = [
Gui.InputHint(translate("draft", "%1 pick first point"), Gui.UserInput.MouseLeft)
]
elif len(self.points) == 1:
hints = [
Gui.InputHint(translate("draft", "%1 pick second point"), Gui.UserInput.MouseLeft)
]
else:
hints = [
Gui.InputHint(translate("draft", "%1 pick third point"), Gui.UserInput.MouseLeft)
]
return hints \
+ gui_tool_utils._get_hint_xyz_constrain() \
+ gui_tool_utils._get_hint_mod_constrain() \
+ gui_tool_utils._get_hint_mod_snap()
Draft_Arc_3Points = Arc_3Points
Gui.addCommand('Draft_Arc_3Points', Arc_3Points())

View File

@@ -29,6 +29,8 @@
## \addtogroup draftguitools
# @{
from PySide import QtCore
import FreeCAD as App
import FreeCADGui as Gui
from draftguitools import gui_trackers as trackers
@@ -171,6 +173,12 @@ class GuiCommandBase:
_toolmsg("{}".format(16*"-"))
_toolmsg("GuiCommand: {}".format(self.featureName))
def update_hints(self):
Gui.HintManager.show(*self.get_hints())
def get_hints(self):
return []
def finish(self):
"""Terminate the active command by committing the list of commands.
@@ -194,6 +202,8 @@ class GuiCommandBase:
todo.ToDo.delayCommit(self.commit_list)
self.commit_list = []
QtCore.QTimer.singleShot(0, Gui.HintManager.hide)
def commit(self, name, func):
"""Store actions to be committed to the document.

View File

@@ -33,7 +33,6 @@ of the DraftToolBar, the Snapper, and the working plane.
## \addtogroup draftguitools
# @{
from PySide import QtCore
import FreeCAD as App
@@ -137,15 +136,13 @@ class DraftTool:
_toolmsg("GuiCommand: {}".format(self.featureName))
# update hints after the tool is fully initialized
QtCore.QTimer.singleShot(0, self.updateHints)
QtCore.QTimer.singleShot(0, self.update_hints)
def updateHints(self):
Gui.HintManager.show(*self.getHints())
def update_hints(self):
Gui.HintManager.show(*self.get_hints())
def getHints(self):
return [
Gui.InputHint("%1 constrain", Gui.UserInput.KeyShift)
]
def get_hints(self):
return []
def end_callbacks(self, call):
try:
@@ -195,7 +192,7 @@ class DraftTool:
todo.ToDo.delayCommit(self.commitList)
self.commitList = []
Gui.HintManager.hide()
QtCore.QTimer.singleShot(0, Gui.HintManager.hide)
def commit(self, name, func):
"""Store actions in the commit list to be run later.

View File

@@ -47,7 +47,7 @@ from draftutils import gui_utils
from draftutils import params
from draftutils import todo
from draftutils import utils
from draftutils.messages import _err, _msg, _toolmsg
from draftutils.messages import _err, _toolmsg
from draftutils.translate import translate
@@ -134,8 +134,6 @@ class BezCurve(gui_lines.Line):
if (self.point-self.node[0]).Length < utils.tolerance():
self.undolast()
self.finish(cont=None, closed=True)
_msg(translate("draft",
"Bézier curve has been closed"))
def undolast(self):
"""Undo last line segment."""
@@ -143,7 +141,7 @@ class BezCurve(gui_lines.Line):
self.node.pop()
self.bezcurvetrack.update(self.node, degree=self.degree)
self.obj.Shape = self.updateShape(self.node)
_msg(translate("draft", "Last point has been removed"))
self.update_hints()
def drawUpdate(self, point):
"""Draw and update to the curve."""
@@ -155,6 +153,7 @@ class BezCurve(gui_lines.Line):
else:
self.obj.Shape = self.updateShape(self.node)
_toolmsg(translate("draft", "Pick next point"))
self.update_hints()
def updateShape(self, pts):
"""Create shape for display during creation process."""
@@ -343,7 +342,6 @@ class CubicBezCurve(gui_lines.Line):
_sym = 2 * self.node[0] - self.node[1]
self.node.append(_sym)
self.finish(cont=None, closed=True)
_msg(translate("draft", "Bézier curve has been closed"))
# Release the held button
if arg["State"] == "UP" and arg["Button"] == "BUTTON1":
if arg["Position"] == self.pos:
@@ -378,7 +376,7 @@ class CubicBezCurve(gui_lines.Line):
self.node.pop()
self.bezcurvetrack.update(self.node, degree=self.degree)
self.obj.Shape = self.updateShape(self.node)
_msg(translate("draft", "Last point has been removed"))
self.update_hints()
def drawUpdate(self, point):
"""Create shape for display during creation process."""
@@ -391,6 +389,7 @@ class CubicBezCurve(gui_lines.Line):
# is a knot
self.obj.Shape = self.updateShape(self.node[:-1])
_toolmsg(translate("draft", "Click and drag to define next knot"))
self.update_hints()
def updateShape(self, pts):
"""Create shape for display during creation process."""
@@ -477,6 +476,17 @@ class CubicBezCurve(gui_lines.Line):
if cont or (cont is None and self.ui and self.ui.continueMode):
self.Activated()
def get_hints(self):
if len(self.node) < 2:
return [Gui.InputHint(
translate("draft", "%1 click and drag to define first point and knot"),
Gui.UserInput.MouseLeft
)]
return [Gui.InputHint(
translate("draft", "%1 click and drag to define next point and knot"),
Gui.UserInput.MouseLeft
)]
Gui.addCommand('Draft_CubicBezCurve', CubicBezCurve())

View File

@@ -206,6 +206,21 @@ class Ellipse(gui_base_original.Creator):
self.rect.on()
if self.planetrack:
self.planetrack.set(point)
self.update_hints()
def get_hints(self):
if len(self.node) == 0:
hints = [
Gui.InputHint(translate("draft", "%1 pick first point"), Gui.UserInput.MouseLeft)
]
else:
hints = [
Gui.InputHint(translate("draft", "%1 pick opposite point"), Gui.UserInput.MouseLeft)
]
return hints \
+ gui_tool_utils._get_hint_xyz_constrain() \
+ gui_tool_utils._get_hint_mod_constrain() \
+ gui_tool_utils._get_hint_mod_snap()
Gui.addCommand('Draft_Ellipse', Ellipse())

View File

@@ -123,7 +123,7 @@ class Line(gui_base_original.Creator):
self.ui.redraw()
self.pos = arg["Position"]
self.node.append(self.point)
self.drawSegment(self.point)
self.drawUpdate(self.point)
if self.mode == "line" and len(self.node) == 2:
self.finish(cont=None, closed=False)
if len(self.node) > 2:
@@ -227,8 +227,9 @@ class Line(gui_base_original.Creator):
# DNC: report on removal
# _toolmsg(translate("draft", "Removing last point"))
_toolmsg(translate("draft", "Pick next point"))
self.update_hints()
def drawSegment(self, point):
def drawUpdate(self, point):
"""Draws new line segment."""
import Part
if self.planetrack and self.node:
@@ -250,6 +251,7 @@ class Line(gui_base_original.Creator):
newshape = currentshape.fuse(newseg)
self.obj.Shape = newshape
_toolmsg(translate("draft", "Pick next point"))
self.update_hints()
def wipe(self):
"""Remove all previous segments and starts from last point."""
@@ -260,6 +262,7 @@ class Line(gui_base_original.Creator):
if self.planetrack:
self.planetrack.set(self.node[0])
_toolmsg(translate("draft", "Pick next point"))
self.update_hints()
def orientWP(self):
"""Orient the working plane."""
@@ -282,11 +285,36 @@ class Line(gui_base_original.Creator):
"""
self.point = App.Vector(numx, numy, numz)
self.node.append(self.point)
self.drawSegment(self.point)
self.drawUpdate(self.point)
if self.mode == "line" and len(self.node) == 2:
self.finish(cont=None, closed=False)
self.ui.setNextFocus()
def get_hints(self):
if len(self.node) == 0:
hints = [
Gui.InputHint(translate("draft", "%1 pick first point"), Gui.UserInput.MouseLeft)
]
elif self.mode == "line":
hints = [
Gui.InputHint(translate("draft", "%1 pick second point"), Gui.UserInput.MouseLeft)
]
elif len(self.node) > 2:
hints = [
Gui.InputHint(
translate("draft", "%1 pick next point, snap to first point to close"),
Gui.UserInput.MouseLeft
)
]
else:
hints = [
Gui.InputHint(translate("draft", "%1 pick next point"), Gui.UserInput.MouseLeft)
]
return hints \
+ gui_tool_utils._get_hint_xyz_constrain() \
+ gui_tool_utils._get_hint_mod_constrain() \
+ gui_tool_utils._get_hint_mod_snap()
Gui.addCommand('Draft_Line', Line())

View File

@@ -42,6 +42,7 @@ import FreeCAD as App
import FreeCADGui as Gui
import Draft_rc
from draftguitools import gui_base_original
from draftguitools import gui_tool_utils
from draftutils import gui_utils
from draftutils import params
from draftutils import todo
@@ -164,6 +165,12 @@ class Point(gui_base_original.Creator):
if cont or (cont is None and self.ui and self.ui.continueMode):
self.Activated()
def get_hints(self):
return [Gui.InputHint(translate("draft", "%1 pick point"), Gui.UserInput.MouseLeft)] \
+ gui_tool_utils._get_hint_xyz_constrain() \
+ gui_tool_utils._get_hint_mod_constrain() \
+ gui_tool_utils._get_hint_mod_snap()
Gui.addCommand('Draft_Point', Point())

View File

@@ -210,6 +210,7 @@ class Polygon(gui_base_original.Creator):
_toolmsg(translate("draft", "Pick radius"))
if self.planetrack:
self.planetrack.set(self.point)
self.update_hints()
elif self.step == 1: # choose radius
self.drawPolygon()
@@ -268,6 +269,7 @@ class Polygon(gui_base_original.Creator):
self.step = 1
self.ui.radiusValue.setFocus()
_toolmsg(translate("draft", "Pick radius"))
self.update_hints()
def numericRadius(self, rad):
"""Validate the entry radius in the user interface.
@@ -298,6 +300,20 @@ class Polygon(gui_base_original.Creator):
self.center = cir[-1].Center
self.drawPolygon()
def get_hints(self):
if self.step == 0:
hints = [
Gui.InputHint(translate("draft", "%1 pick center"), Gui.UserInput.MouseLeft)
]
else:
hints = [
Gui.InputHint(translate("draft", "%1 pick radius"), Gui.UserInput.MouseLeft)
]
return hints \
+ gui_tool_utils._get_hint_xyz_constrain() \
+ gui_tool_utils._get_hint_mod_constrain() \
+ gui_tool_utils._get_hint_mod_snap()
Gui.addCommand('Draft_Polygon', Polygon())

View File

@@ -202,6 +202,21 @@ class Rectangle(gui_base_original.Creator):
self.rect.on()
if self.planetrack:
self.planetrack.set(point)
self.update_hints()
def get_hints(self):
if len(self.node) == 0:
hints = [
Gui.InputHint(translate("draft", "%1 pick first point"), Gui.UserInput.MouseLeft)
]
else:
hints = [
Gui.InputHint(translate("draft", "%1 pick opposite point"), Gui.UserInput.MouseLeft)
]
return hints \
+ gui_tool_utils._get_hint_xyz_constrain() \
+ gui_tool_utils._get_hint_mod_constrain() \
+ gui_tool_utils._get_hint_mod_snap()
Gui.addCommand('Draft_Rectangle', Rectangle())

View File

@@ -69,6 +69,7 @@ class ShapeString(gui_base.GuiCommandBase):
task = Gui.Control.showDialog(self.ui)
task.setDocumentName(Gui.ActiveDocument.Document.Name)
task.setAutoCloseOnDeletedDocument(True)
self.ui.update_hints()
def finish(self):
try:

View File

@@ -42,7 +42,7 @@ import draftguitools.gui_tool_utils as gui_tool_utils
import draftguitools.gui_lines as gui_lines
import draftguitools.gui_trackers as trackers
from draftutils.messages import _msg, _err, _toolmsg
from draftutils.messages import _err, _toolmsg
from draftutils.translate import translate
@@ -121,7 +121,6 @@ class BSpline(gui_lines.Line):
if (self.point - self.node[0]).Length < utils.tolerance():
self.undolast()
self.finish(cont=None, closed=True)
_msg(translate("draft", "Spline has been closed"))
def undolast(self):
"""Undo last line segment."""
@@ -132,7 +131,7 @@ class BSpline(gui_lines.Line):
spline = Part.BSplineCurve()
spline.interpolate(self.node, False)
self.obj.Shape = spline.toShape()
_msg(translate("draft", "Last point has been removed"))
self.update_hints()
def drawUpdate(self, point):
"""Draw and update to the spline."""
@@ -147,6 +146,7 @@ class BSpline(gui_lines.Line):
spline.interpolate(self.node, False)
self.obj.Shape = spline.toShape()
_toolmsg(translate("draft", "Pick next point"))
self.update_hints()
def finish(self, cont=False, closed=False):
"""Terminate the operation and close the spline if asked.

View File

@@ -34,6 +34,8 @@ as they operate on selections and graphical properties.
## \addtogroup draftguitools
# @{
import re
import FreeCAD as App
import FreeCADGui as Gui
import WorkingPlane
@@ -41,6 +43,8 @@ from draftutils import gui_utils
from draftutils import params
from draftutils import utils
from draftutils.messages import _wrn
from draftutils.translate import translate
# Set modifier keys from the parameter database
MODS = ["shift", "ctrl", "alt"]
@@ -58,6 +62,66 @@ def get_mod_alt_key():
return MODS[params.get_param("modalt")]
_HINT_MOD_KEYS = [Gui.UserInput.KeyShift, Gui.UserInput.KeyControl, Gui.UserInput.KeyAlt]
# To allows for easy concatenation the _get_hint_* functions
# always return a list (with a single item or an empty list).
def _get_hint_mod_constrain():
key = _HINT_MOD_KEYS[params.get_param("modconstrain")]
return [Gui.InputHint(translate("draft", "%1 constrain"), key)]
def _get_hint_mod_snap():
if params.get_param("alwaysSnap"):
return []
key = _HINT_MOD_KEYS[params.get_param("modsnap")]
return [Gui.InputHint(translate("draft", "%1 snap"), key)]
def _get_hint_xyz_constrain():
pattern = re.compile("[A-Z]")
shortcut_x = params.get_param("inCommandShortcutRestrictX").upper()
shortcut_y = params.get_param("inCommandShortcutRestrictY").upper()
shortcut_z = params.get_param("inCommandShortcutRestrictZ").upper()
if pattern.fullmatch(shortcut_x) \
and pattern.fullmatch(shortcut_y) \
and pattern.fullmatch(shortcut_z):
key_x = getattr(Gui.UserInput, "Key" + shortcut_x)
key_y = getattr(Gui.UserInput, "Key" + shortcut_y)
key_z = getattr(Gui.UserInput, "Key" + shortcut_z)
return [Gui.InputHint(translate("draft", "%1/%2/%3 switch constraint"), key_x, key_y, key_z)]
return []
def _get_hint_relative():
pattern = re.compile("[A-Z]")
shortcut = params.get_param("inCommandShortcutRelative").upper()
if pattern.fullmatch(shortcut):
key = getattr(Gui.UserInput, "Key" + shortcut)
return [Gui.InputHint(translate("draft", "%1 toggle relative"), key)]
return []
def _get_hint_global():
pattern = re.compile("[A-Z]")
shortcut = params.get_param("inCommandShortcutGlobal").upper()
if pattern.fullmatch(shortcut):
key = getattr(Gui.UserInput, "Key" + shortcut)
return [Gui.InputHint(translate("draft", "%1 toggle global"), key)]
return []
def _get_hint_continue():
pattern = re.compile("[A-Z]")
shortcut = params.get_param("inCommandShortcutContinue").upper()
if pattern.fullmatch(shortcut):
key = getattr(Gui.UserInput, "Key" + shortcut)
return [Gui.InputHint(translate("draft", "%1 toggle continue"), key)]
return []
def format_unit(exp, unit="mm"):
"""Return a formatting string to set a number to the correct unit."""
return App.Units.Quantity(exp, App.Units.Length).UserString

View File

@@ -104,10 +104,12 @@ class ShapeStringTaskPanel:
if not self.pointPicked:
self.point, ctrlPoint, info = gui_tool_utils.getPoint(self, arg, noTracker=True)
self.display_point(self.point)
self.update_hints()
elif arg["Type"] == "SoMouseButtonEvent":
if (arg["State"] == "DOWN") and (arg["Button"] == "BUTTON1"):
self.display_point(self.point)
self.pointPicked = True
self.update_hints()
def change_coord_labels(self):
if self.global_mode:
@@ -153,6 +155,7 @@ class ShapeStringTaskPanel:
self.point = self.wp.position
self.pointPicked = False
self.display_point(self.point)
self.update_hints()
def set_file(self, val):
"""Assign the selected font file."""
@@ -199,6 +202,17 @@ class ShapeStringTaskPanel:
self.text = val
params.set_param("ShapeStringText", self.text)
def update_hints(self):
if self.pointPicked:
Gui.HintManager.hide()
else:
Gui.HintManager.show(
Gui.InputHint(
translate("draft", "%1 pick point"),
Gui.UserInput.MouseLeft
)
)
class ShapeStringTaskPanelCmd(ShapeStringTaskPanel):
"""Task panel for Draft_ShapeString."""