Draft: close task panels on doc close

Related: #17952.

This PR introduces a document observer to close task panels on doc close.

For now it is for the Draft Workbench only. The BIM Workbench will be dealt with in a future PR.

The basic code is simple, but to make things works some additional things were addressed:
* gui_base.py: the GuiCommandBase class was enhanced to handle App.activeDraftCommand, self.doc, self.view and self.planetracker. Strictly speaking only the first 2 are required for this PR.
* gui_base.py: self.command_name was changed to self.featureName for compatibility with gui_base_original.py. Not required for this PR.
* gui_arcs.py, gui_circulararray.py, gui_polararray.py and gui_orthoarray.py: updated in relation to the GuiCommandBase class.
* gui_arcs.py Arc_3Points: The command now has a ui property and shows a plane tracker. Only the first is required for this PR.
* gui_shapestrings.py: This command had two ui attributes: self.ui and self.task. This was problematic. To fix this the base class of the command was changed from gui_base_original.Creator to gui_base.GuiCommandBase. As a result the getStrings method is no longer available meaning that the useSupport parameter is ignored when creating a ShapeString. But since that mechanism does not work properly anyway, I feel that this is acceptable. Should many user complain the functionality can of course be reintroduced.
This commit is contained in:
Roy-043
2025-04-09 12:04:02 +02:00
committed by Chris Hennes
parent 0009ddbd0b
commit ca340da86c
10 changed files with 146 additions and 115 deletions

View File

@@ -48,7 +48,6 @@ SET (Draft_geoutils
SET(Draft_tests
drafttests/__init__.py
drafttests/README.md
drafttests/auxiliary.py
drafttests/draft_test_objects.py
drafttests/test_airfoildat.py
@@ -65,22 +64,24 @@ SET(Draft_tests
drafttests/test_oca.py
drafttests/test_pivy.py
drafttests/test_svg.py
drafttests/README.md
)
SET(Draft_utilities
draftutils/__init__.py
draftutils/doc_observer.py
draftutils/grid_observer.py
draftutils/groups.py
draftutils/init_tools.py
draftutils/init_draft_statusbar.py
draftutils/params.py
draftutils/units.py
draftutils/utils.py
draftutils/gui_utils.py
draftutils/init_draft_statusbar.py
draftutils/init_tools.py
draftutils/messages.py
draftutils/params.py
draftutils/todo.py
draftutils/translate.py
draftutils/messages.py
draftutils/units.py
draftutils/utils.py
draftutils/README.md
draftutils/grid_observer.py
)
SET(Draft_functions

View File

@@ -157,6 +157,8 @@ class DraftWorkbench(FreeCADGui.Workbench):
WorkingPlane._view_observer_start() # Updates the draftToolBar when switching views.
from draftutils import grid_observer
grid_observer._view_observer_setup()
from draftutils import doc_observer
doc_observer._doc_observer_start()
FreeCAD.Console.PrintLog("Draft workbench activated.\n")
def Deactivated(self):
@@ -171,6 +173,8 @@ class DraftWorkbench(FreeCADGui.Workbench):
WorkingPlane._view_observer_stop()
from draftutils import grid_observer
grid_observer._view_observer_setup()
from draftutils import doc_observer
doc_observer._doc_observer_stop()
FreeCAD.Console.PrintLog("Draft workbench deactivated.\n")
def ContextMenu(self, recipient):

View File

@@ -470,19 +470,19 @@ Gui.addCommand('Draft_Arc', Arc())
class Arc_3Points(gui_base.GuiCommandBase):
"""GuiCommand for the Draft_Arc_3Points tool."""
def __init__(self):
super().__init__(name="Arc_3Points")
def GetResources(self):
"""Set icon, menu and tooltip."""
return {"Pixmap": "Draft_Arc_3Points",
"Accel": "A,T",
"Accel": "A, T",
"MenuText": QT_TRANSLATE_NOOP("Draft_Arc_3Points", "Arc by 3 points"),
"ToolTip": QT_TRANSLATE_NOOP("Draft_Arc_3Points", "Creates a circular arc by picking 3 points.\nCTRL to snap, SHIFT to constrain.")}
def Activated(self):
"""Execute when the command is called."""
if App.activeDraftCommand:
App.activeDraftCommand.finish()
App.activeDraftCommand = self
self.featureName = "Arc_3Points"
super().Activated()
# Reset the values
self.points = []
@@ -498,10 +498,11 @@ class Arc_3Points(gui_base.GuiCommandBase):
Gui.Snapper.getPoint(callback=self.getPoint,
movecallback=self.drawArc)
Gui.Snapper.ui.sourceCmd = self
Gui.Snapper.ui.setTitle(title=translate("draft", "Arc by 3 points"),
icon="Draft_Arc_3Points")
Gui.Snapper.ui.continueCmd.show()
self.ui = Gui.Snapper.ui ## self must have a ui for _finish_command_on_doc_close in doc_observer.py.
self.ui.sourceCmd = self
self.ui.setTitle(title=translate("draft", "Arc by 3 points"),
icon="Draft_Arc_3Points")
self.ui.continueCmd.show()
def getPoint(self, point, info):
"""Get the point by clicking on the 3D view.
@@ -527,6 +528,8 @@ class Arc_3Points(gui_base.GuiCommandBase):
# Avoid adding the same point twice
if point not in self.points:
self.points.append(point)
if self.planetrack and len(self.points) == 1:
self.planetrack.set(point)
if len(self.points) < 3:
# If one or two points were picked, set up again the Snapper
@@ -540,10 +543,11 @@ class Arc_3Points(gui_base.GuiCommandBase):
Gui.Snapper.getPoint(last=self.points[-1],
callback=self.getPoint,
movecallback=self.drawArc)
Gui.Snapper.ui.sourceCmd = self
Gui.Snapper.ui.setTitle(title=translate("draft", "Arc by 3 points"),
icon="Draft_Arc_3Points")
Gui.Snapper.ui.continueCmd.show()
self.ui = Gui.Snapper.ui
self.ui.sourceCmd = self
self.ui.setTitle(title=translate("draft", "Arc by 3 points"),
icon="Draft_Arc_3Points")
self.ui.continueCmd.show()
else:
# If three points were already picked in the 3D view
@@ -592,7 +596,6 @@ class Arc_3Points(gui_base.GuiCommandBase):
Restart (continue) the command if `True`, or if `None` and
`ui.continueMode` is `True`.
"""
App.activeDraftCommand = None
self.tracker.finalize()
super().finish()
if cont or (cont is None and Gui.Snapper.ui and Gui.Snapper.ui.continueMode):

View File

@@ -31,7 +31,9 @@
# @{
import FreeCAD as App
import FreeCADGui as Gui
from draftguitools import gui_trackers as trackers
from draftutils import gui_utils
from draftutils import params
from draftutils import todo
from draftutils.messages import _toolmsg, _log
@@ -60,30 +62,20 @@ class GuiCommandSimplest:
for example, `'Heal'`, `'Flip dimensions'`,
`'Line'`, `'Circle'`, etc.
doc: App::Document, optional
It defaults to the value of `App.activeDocument()`.
The document object itself, which indicates where the actions
of the command will be executed.
Attributes
----------
command_name: str
featureName: str
This is the command name, which is assigned by `name`.
doc: App::Document
This is the document object itself, which is assigned by `doc`.
This attribute should be used by functions to make sure
that the operations are performed in the correct document
and not in other documents.
To set the active document we can use
>>> App.setActiveDocument(self.doc.Name)
"""
def __init__(self, name="None", doc=App.activeDocument()):
self.command_name = name
self.doc = doc
def __init__(self, name="None"):
self.doc = None
self.featureName = name
def IsActive(self):
"""Return True when this command should be available."""
@@ -96,10 +88,8 @@ class GuiCommandSimplest:
Also update the `doc` attribute.
"""
self.doc = App.activeDocument()
_log("Document: {}".format(self.doc.Label))
_log("GuiCommand: {}".format(self.command_name))
_toolmsg("{}".format(16*"-"))
_toolmsg("GuiCommand: {}".format(self.command_name))
_toolmsg("GuiCommand: {}".format(self.featureName))
class GuiCommandNeedsSelection(GuiCommandSimplest):
@@ -153,18 +143,34 @@ class GuiCommandBase:
>>> Draft.autogroup(obj)
"""
def __init__(self):
def __init__(self, name="None"):
App.activeDraftCommand = None
self.call = None
self.commit_list = []
self.doc = None
App.activeDraftCommand = None
self.view = None
self.featureName = name
self.planetrack = None
self.view = None
def IsActive(self):
"""Return True when this command should be available."""
return bool(gui_utils.get_3d_view())
def Activated(self):
self.doc = App.ActiveDocument
if not self.doc:
self.finish()
return
App.activeDraftCommand = self
self.view = gui_utils.get_3d_view()
if params.get_param("showPlaneTracker"):
self.planetrack = trackers.PlaneTracker()
_toolmsg("{}".format(16*"-"))
_toolmsg("GuiCommand: {}".format(self.featureName))
def finish(self):
"""Terminate the active command by committing the list of commands.
@@ -174,6 +180,7 @@ class GuiCommandBase:
App.activeDraftCommand = None
if self.planetrack:
self.planetrack.finalize()
self.planetrack = None
if hasattr(Gui, "Snapper"):
Gui.Snapper.off()
if self.call:
@@ -182,7 +189,7 @@ class GuiCommandBase:
except RuntimeError:
# the view has been deleted already
pass
self.call = None
self.call = None
if self.commit_list:
todo.ToDo.delayCommit(self.commit_list)
self.commit_list = []

View File

@@ -32,8 +32,6 @@ from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD as App
import FreeCADGui as Gui
import Draft
import Draft_rc # include resources, icons, ui files
from draftguitools import gui_base
from draftutils import gui_utils
from draftutils import todo
@@ -41,19 +39,14 @@ from draftutils.messages import _log
from draftutils.translate import translate
from drafttaskpanels import task_circulararray
# The module is used to prevent complaints from code checkers (flake8)
bool(Draft_rc.__name__)
class CircularArray(gui_base.GuiCommandBase):
"""Gui command for the CircularArray tool."""
def __init__(self):
super().__init__()
self.command_name = "Circular array"
super().__init__(name="CircularArray")
self.location = None
self.mouse_event = None
self.view = None
self.callback_move = None
self.callback_click = None
self.ui = None
@@ -61,9 +54,9 @@ class CircularArray(gui_base.GuiCommandBase):
def GetResources(self):
"""Set icon, menu and tooltip."""
return {'Pixmap': 'Draft_CircularArray',
'MenuText': QT_TRANSLATE_NOOP("Draft_CircularArray", "Circular array"),
'ToolTip': QT_TRANSLATE_NOOP("Draft_CircularArray", "Creates copies of the selected object, and places the copies in a radial pattern\ncreating various circular layers.\n\nThe array can be turned into an orthogonal or a polar array by changing its type.")}
return {"Pixmap": "Draft_CircularArray",
"MenuText": QT_TRANSLATE_NOOP("Draft_CircularArray", "Circular array"),
"ToolTip": QT_TRANSLATE_NOOP("Draft_CircularArray", "Creates copies of the selected object, and places the copies in a radial pattern\ncreating various circular layers.\n\nThe array can be turned into an orthogonal or a polar array by changing its type.")}
def Activated(self):
"""Execute when the command is called.
@@ -71,11 +64,10 @@ class CircularArray(gui_base.GuiCommandBase):
We add callbacks that connect the 3D view with
the widgets of the task panel.
"""
_log("GuiCommand: {}".format(self.command_name))
super().Activated()
self.location = coin.SoLocation2Event.getClassTypeId()
self.mouse_event = coin.SoMouseButtonEvent.getClassTypeId()
self.view = Draft.get3DView()
self.callback_move = \
self.view.addEventCallbackPivy(self.location, self.move)
self.callback_click = \
@@ -135,7 +127,7 @@ class CircularArray(gui_base.GuiCommandBase):
self.callback_click = None
if Gui.Control.activeDialog():
Gui.Control.closeDialog()
self.finish()
self.finish()
Gui.addCommand('Draft_CircularArray', CircularArray())

View File

@@ -32,8 +32,6 @@ from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD as App
import FreeCADGui as Gui
import Draft
import Draft_rc # include resources, icons, ui files
from draftguitools import gui_base
from draftutils import gui_utils
from draftutils import todo
@@ -41,19 +39,14 @@ from draftutils.messages import _log
from draftutils.translate import translate
from drafttaskpanels import task_orthoarray
# The module is used to prevent complaints from code checkers (flake8)
bool(Draft_rc.__name__)
class OrthoArray(gui_base.GuiCommandBase):
"""Gui command for the OrthoArray tool."""
def __init__(self):
super().__init__()
self.command_name = "Orthogonal array"
super().__init__(name="OrthoArray")
# self.location = None
self.mouse_event = None
self.view = None
# self.callback_move = None
self.callback_click = None
self.ui = None
@@ -71,11 +64,10 @@ class OrthoArray(gui_base.GuiCommandBase):
We add callbacks that connect the 3D view with
the widgets of the task panel.
"""
_log("GuiCommand: {}".format(self.command_name))
super().Activated()
# self.location = coin.SoLocation2Event.getClassTypeId()
self.mouse_event = coin.SoMouseButtonEvent.getClassTypeId()
self.view = Draft.get3DView()
# self.callback_move = \
# self.view.addEventCallbackPivy(self.location, self.move)
self.callback_click = \
@@ -120,7 +112,7 @@ class OrthoArray(gui_base.GuiCommandBase):
self.callback_click = None
if Gui.Control.activeDialog():
Gui.Control.closeDialog()
self.finish()
self.finish()
Gui.addCommand('Draft_OrthoArray', OrthoArray())

View File

@@ -32,8 +32,6 @@ from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD as App
import FreeCADGui as Gui
import Draft
import Draft_rc # include resources, icons, ui files
from draftguitools import gui_base
from draftutils import gui_utils
from draftutils import todo
@@ -41,19 +39,14 @@ from draftutils.messages import _log
from draftutils.translate import translate
from drafttaskpanels import task_polararray
# The module is used to prevent complaints from code checkers (flake8)
bool(Draft_rc.__name__)
class PolarArray(gui_base.GuiCommandBase):
"""Gui command for the PolarArray tool."""
def __init__(self):
super().__init__()
self.command_name = "Polar array"
super().__init__(name="PolarArray")
self.location = None
self.mouse_event = None
self.view = None
self.callback_move = None
self.callback_click = None
self.ui = None
@@ -61,9 +54,9 @@ class PolarArray(gui_base.GuiCommandBase):
def GetResources(self):
"""Set icon, menu and tooltip."""
return {'Pixmap': 'Draft_PolarArray',
'MenuText': QT_TRANSLATE_NOOP("Draft_PolarArray", "Polar array"),
'ToolTip': QT_TRANSLATE_NOOP("Draft_PolarArray", "Creates copies of the selected object, and places the copies in a polar pattern\ndefined by a center of rotation and its angle.\n\nThe array can be turned into an orthogonal or a circular array by changing its type.")}
return {"Pixmap": "Draft_PolarArray",
"MenuText": QT_TRANSLATE_NOOP("Draft_PolarArray", "Polar array"),
"ToolTip": QT_TRANSLATE_NOOP("Draft_PolarArray", "Creates copies of the selected object, and places the copies in a polar pattern\ndefined by a center of rotation and its angle.\n\nThe array can be turned into an orthogonal or a circular array by changing its type.")}
def Activated(self):
"""Execute when the command is called.
@@ -71,11 +64,10 @@ class PolarArray(gui_base.GuiCommandBase):
We add callbacks that connect the 3D view with
the widgets of the task panel.
"""
_log("GuiCommand: {}".format(self.command_name))
super().Activated()
self.location = coin.SoLocation2Event.getClassTypeId()
self.mouse_event = coin.SoMouseButtonEvent.getClassTypeId()
self.view = Draft.get3DView()
self.callback_move = \
self.view.addEventCallbackPivy(self.location, self.move)
self.callback_click = \
@@ -135,7 +127,7 @@ class PolarArray(gui_base.GuiCommandBase):
self.callback_click = None
if Gui.Control.activeDialog():
Gui.Control.closeDialog()
self.finish()
self.finish()
Gui.addCommand('Draft_PolarArray', PolarArray())

View File

@@ -39,46 +39,46 @@ They are more complex that simple text annotations.
# @{
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD as App
import FreeCADGui as Gui
import Draft_rc
import DraftVecUtils
import draftutils.utils as utils
import draftguitools.gui_base_original as gui_base_original
import draftguitools.gui_tool_utils as gui_tool_utils
import draftutils.todo as todo
from drafttaskpanels.task_shapestring import ShapeStringTaskPanelCmd
from draftguitools import gui_base
from draftutils import gui_utils
from draftutils import todo
from draftutils.messages import _toolmsg
from draftutils.translate import translate
from draftutils.messages import _toolmsg, _err
# The module is used to prevent complaints from code checkers (flake8)
True if Draft_rc.__name__ else False
from drafttaskpanels import task_shapestring
class ShapeString(gui_base_original.Creator):
class ShapeString(gui_base.GuiCommandBase):
"""Gui command for the ShapeString tool."""
def __init__(self):
super().__init__(name="ShapeString")
def GetResources(self):
"""Set icon, menu and tooltip."""
d = {'Pixmap': 'Draft_ShapeString',
'MenuText': QT_TRANSLATE_NOOP("Draft_ShapeString", "Shape from text"),
'ToolTip': QT_TRANSLATE_NOOP("Draft_ShapeString", "Creates a shape from a text string by choosing a specific font and a placement.\nThe closed shapes can be used for extrusions and boolean operations.")}
return d
return {"Pixmap": "Draft_ShapeString",
"MenuText": QT_TRANSLATE_NOOP("Draft_ShapeString", "Shape from text"),
"ToolTip": QT_TRANSLATE_NOOP("Draft_ShapeString", "Creates a shape from a text string by choosing a specific font and a placement.\nThe closed shapes can be used for extrusions and boolean operations.")}
def Activated(self):
"""Execute when the command is called."""
super().Activated(name="ShapeString")
if self.ui:
self.ui.sourceCmd = self
self.task = ShapeStringTaskPanelCmd(self)
self.call = self.view.addEventCallback("SoEvent", self.task.action)
_toolmsg(translate("draft", "Pick ShapeString location point"))
todo.ToDo.delay(Gui.Control.showDialog, self.task)
super().Activated()
self.ui = task_shapestring.ShapeStringTaskPanelCmd(self)
self.call = self.view.addEventCallback("SoEvent", self.ui.action)
_toolmsg(translate("draft", "Pick ShapeString location point"))
todo.ToDo.delay(Gui.Control.showDialog, self.ui)
def finish(self):
self.end_callbacks(self.call)
try:
self.view.removeEventCallback("SoEvent", self.call)
gui_utils.end_all_events()
except RuntimeError:
# the view has been deleted already
pass
self.call = None
if Gui.Control.activeDialog():
Gui.Control.closeDialog()
super().finish()

View File

@@ -142,7 +142,6 @@ class ShapeStringTaskPanelCmd(ShapeStringTaskPanel):
def reject(self):
"""Run when clicking the Cancel button."""
Gui.ActiveDocument.resetEdit()
self.sourceCmd.finish()
self.platWinDialog("Restore")
return True
@@ -160,15 +159,14 @@ class ShapeStringTaskPanelCmd(ShapeStringTaskPanel):
z = App.Units.Quantity(self.form.sbZ.text()).Value
ssBase = App.Vector(x, y, z)
try:
qr, sup, points, fil = self.sourceCmd.getStrings()
Gui.addModule("Draft")
Gui.addModule("WorkingPlane")
self.sourceCmd.commit(translate('draft', 'Create ShapeString'),
['ss = Draft.make_shapestring(String=' + String + ', FontFile=' + FFile + ', Size=' + Size + ', Tracking=' + Tracking + ')',
'plm = FreeCAD.Placement()',
'plm.Base = ' + toString(ssBase),
'plm.Rotation.Q = ' + qr,
'ss.Placement = plm',
'ss.AttachmentSupport = ' + sup,
'pl = FreeCAD.Placement()',
'pl.Base = ' + toString(ssBase),
'pl.Rotation = WorkingPlane.get_working_plane().get_placement().Rotation',
'ss.Placement = pl',
'Draft.autogroup(ss)',
'FreeCAD.ActiveDocument.recompute()'])
except Exception:

View File

@@ -0,0 +1,42 @@
import FreeCAD as App
if App.GuiUp:
import FreeCADGui as Gui
from draftutils.todo import ToDo
class _Draft_DocObserver():
# See: /src/Gui/DocumentObserverPython.h
def slotDeletedDocument(self, gui_doc):
_finish_command_on_doc_close(gui_doc)
_doc_observer = None
def _doc_observer_start():
global _doc_observer
if _doc_observer is None:
_doc_observer = _Draft_DocObserver()
Gui.addDocumentObserver(_doc_observer)
def _doc_observer_stop():
global _doc_observer
try:
if _doc_observer is not None:
Gui.removeDocumentObserver(_doc_observer)
except:
pass
_doc_observer = None
def _finish_command_on_doc_close(gui_doc):
"""Finish the active Draft or BIM command if the related document has been
closed. Only works for commands that have set `App.activeDraftCommand.doc`
and use a task panel.
"""
if getattr(App, "activeDraftCommand", None) \
and getattr(App.activeDraftCommand, "doc", None) == gui_doc.Document \
and getattr(App.activeDraftCommand, "ui", None):
if hasattr(App.activeDraftCommand.ui, "reject"):
ToDo.delay(App.activeDraftCommand.ui.reject, None)
elif hasattr(App.activeDraftCommand.ui, "escape"):
ToDo.delay(App.activeDraftCommand.ui.escape, None)