Merge pull request #22712 from PaddleStroke/asm_watcher

Assembly: TaskWatcher
This commit is contained in:
Kacper Donat
2025-07-28 00:13:28 +02:00
committed by GitHub
6 changed files with 640 additions and 3 deletions

View File

@@ -40,6 +40,7 @@
#include <Gui/Application.h>
#include <Gui/Document.h>
#include <Gui/MainWindow.h>
#include <Gui/ViewProviderDocumentObject.h>
#include "TaskView.h"
#include "TaskDialog.h"
@@ -317,6 +318,9 @@ TaskView::TaskView(QWidget *parent)
connectApplicationRedoDocument =
App::GetApplication().signalRedoDocument.connect
(std::bind(&Gui::TaskView::TaskView::slotRedoDocument, this, sp::_1));
connectApplicationInEdit =
Gui::Application::Instance->signalInEdit.connect(
std::bind(&Gui::TaskView::TaskView::slotInEdit, this, sp::_1));
//NOLINTEND
updateWatcher();
@@ -329,6 +333,7 @@ TaskView::~TaskView()
connectApplicationClosedView.disconnect();
connectApplicationUndoDocument.disconnect();
connectApplicationRedoDocument.disconnect();
connectApplicationInEdit.disconnect();
Gui::Selection().Detach(this);
for (QWidget* panel : contextualPanels) {
@@ -474,8 +479,20 @@ QSize TaskView::minimumSizeHint() const
void TaskView::slotActiveDocument(const App::Document& doc)
{
Q_UNUSED(doc);
if (!ActiveDialog)
if (!ActiveDialog) {
// at this point, active object of the active view returns None.
// which is a problem if shouldShow of a watcher rely on the presence
// of an active object (example Assembly).
QTimer::singleShot(100, this, &TaskView::updateWatcher);
}
}
void TaskView::slotInEdit(const Gui::ViewProviderDocumentObject& vp)
{
Q_UNUSED(vp);
if (!ActiveDialog) {
updateWatcher();
}
}
void TaskView::slotDeletedDocument(const App::Document& doc)

View File

@@ -39,6 +39,7 @@ class Property;
namespace Gui {
class MDIView;
class ControlSingleton;
class ViewProviderDocumentObject;
namespace DockWnd{
class ComboView;
}
@@ -187,6 +188,7 @@ private:
void saveCurrentWidth();
void tryRestoreWidth();
void slotActiveDocument(const App::Document&);
void slotInEdit(const Gui::ViewProviderDocumentObject&);
void slotDeletedDocument(const App::Document&);
void slotViewClosed(const Gui::MDIView*);
void slotUndoDocument(const App::Document&);
@@ -224,6 +226,7 @@ protected:
Connection connectApplicationClosedView;
Connection connectApplicationUndoDocument;
Connection connectApplicationRedoDocument;
Connection connectApplicationInEdit;
};
} //namespace TaskView

View File

@@ -27,11 +27,12 @@ from PySide.QtCore import QT_TRANSLATE_NOOP
if App.GuiUp:
import FreeCADGui as Gui
from PySide import QtCore, QtGui, QtWidgets
import UtilsAssembly
import Preferences
# translate = App.Qt.translate
translate = App.Qt.translate
__title__ = "Assembly Command Create Assembly"
__author__ = "Ondsel"
@@ -91,5 +92,81 @@ class CommandCreateAssembly:
App.closeActiveTransaction()
class ActivateAssemblyTaskPanel:
"""A basic TaskPanel to select an assembly to activate."""
def __init__(self, assemblies):
self.assemblies = assemblies
self.form = QtWidgets.QWidget()
self.form.setWindowTitle(translate("Assembly_ActivateAssembly", "Activate Assembly"))
layout = QtWidgets.QVBoxLayout(self.form)
label = QtWidgets.QLabel(
translate("Assembly_ActivateAssembly", "Select an assembly to activate:")
)
self.combo = QtWidgets.QComboBox()
for asm in self.assemblies:
# Store the user-friendly Label for display, and the internal Name for activation
self.combo.addItem(asm.Label, asm.Name)
layout.addWidget(label)
layout.addWidget(self.combo)
def accept(self):
"""Called when the user clicks OK."""
selected_name = self.combo.currentData()
if selected_name:
Gui.doCommand(f"Gui.ActiveDocument.setEdit('{selected_name}')")
return True
def reject(self):
"""Called when the user clicks Cancel or closes the panel."""
return True
class CommandActivateAssembly:
def __init__(self):
self.task_panel = None
def GetResources(self):
return {
"Pixmap": "Assembly_ActivateAssembly",
"MenuText": QT_TRANSLATE_NOOP("Assembly_ActivateAssembly", "Activate Assembly"),
"ToolTip": QT_TRANSLATE_NOOP(
"Assembly_ActivateAssembly", "Sets an assembly as the active one for editing."
),
"CmdType": "ForEdit",
}
def IsActive(self):
if Gui.Control.activeDialog() or App.ActiveDocument is None:
return False
# Command is only active if no assembly is currently active
if UtilsAssembly.activeAssembly() is not None:
return False
# And if there is at least one assembly in the document to activate
for obj in App.ActiveDocument.Objects:
if obj.isDerivedFrom("Assembly::AssemblyObject"):
return True
return False
def Activated(self):
doc = App.ActiveDocument
assemblies = [o for o in doc.Objects if o.isDerivedFrom("Assembly::AssemblyObject")]
if len(assemblies) == 1:
# If there's only one, activate it directly without showing a dialog
Gui.doCommand(f"Gui.ActiveDocument.setEdit('{assemblies[0].Name}')")
elif len(assemblies) > 1:
# If there are multiple, show a task panel to let the user choose
self.task_panel = ActivateAssemblyTaskPanel(assemblies)
Gui.Control.showDialog(self.task_panel)
if App.GuiUp:
Gui.addCommand("Assembly_CreateAssembly", CommandCreateAssembly())
Gui.addCommand("Assembly_ActivateAssembly", CommandActivateAssembly())

View File

@@ -1,5 +1,6 @@
<RCC>
<qresource prefix="/">
<file>icons/Assembly_ActivateAssembly.svg</file>
<file>icons/Assembly_AssemblyLink.svg</file>
<file>icons/Assembly_AssemblyLinkRigid.svg</file>
<file>icons/Assembly_InsertLink.svg</file>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -118,11 +118,162 @@ class AssemblyWorkbench(Workbench):
# update the translation engine
FreeCADGui.updateLocale()
# Add task watchers to provide contextual tools in the task panel
self.setWatchers()
def Deactivated(self):
pass
FreeCADGui.Control.clearTaskWatcher()
def ContextMenu(self, recipient):
pass
def setWatchers(self):
import UtilsAssembly
translate = FreeCAD.Qt.translate
class AssemblyCreateWatcher:
"""Shows 'Create Assembly' when no assembly exists in the document."""
def __init__(self):
self.commands = ["Assembly_CreateAssembly"]
self.title = translate("Assembly", "Create")
def shouldShow(self):
doc = FreeCAD.ActiveDocument
if hasattr(doc, "RootObjects"):
for obj in doc.RootObjects:
if obj.isDerivedFrom("Assembly::AssemblyObject"):
return False
return True
class AssemblyActivateWatcher:
"""Shows 'Activate Assembly' when an assembly exists but is not active."""
def __init__(self):
self.commands = ["Assembly_ActivateAssembly"]
self.title = translate("Assembly", "Activate")
def shouldShow(self):
doc = FreeCAD.ActiveDocument
has_assembly = False
if hasattr(doc, "RootObjects"):
for obj in doc.RootObjects:
if obj.isDerivedFrom("Assembly::AssemblyObject"):
has_assembly = True
break
assembly = UtilsAssembly.activeAssembly()
return has_assembly and (assembly is None or assembly.Document != doc)
class AssemblyBaseWatcher:
"""Base class for watchers that require an active assembly."""
def __init__(self):
self.assembly = None
def shouldShow(self):
doc = FreeCAD.ActiveDocument
self.assembly = UtilsAssembly.activeAssembly()
return self.assembly is not None and self.assembly.Document == doc
class AssemblyInsertWatcher(AssemblyBaseWatcher):
"""Shows 'Insert Component' when an assembly is active."""
def __init__(self):
super().__init__()
self.commands = ["Assembly_Insert"]
self.title = translate("Assembly", "Insert")
def shouldShow(self):
return super().shouldShow()
class AssemblyGroundWatcher(AssemblyBaseWatcher):
"""Shows 'Ground' when the active assembly has no grounded parts."""
def __init__(self):
super().__init__()
self.commands = ["Assembly_ToggleGrounded"]
self.title = translate("Assembly", "Grounding")
def shouldShow(self):
if not super().shouldShow():
return False
return (
UtilsAssembly.assembly_has_at_least_n_parts(1)
and not UtilsAssembly.isAssemblyGrounded()
)
class AssemblyJointsWatcher(AssemblyBaseWatcher):
"""Shows Joint, View, and BOM tools when there are enough parts."""
def __init__(self):
super().__init__()
self.commands = [
"Assembly_CreateJointFixed",
"Assembly_CreateJointRevolute",
"Assembly_CreateJointCylindrical",
"Assembly_CreateJointSlider",
"Assembly_CreateJointBall",
"Separator",
"Assembly_CreateJointDistance",
"Assembly_CreateJointParallel",
"Assembly_CreateJointPerpendicular",
"Assembly_CreateJointAngle",
]
self.title = translate("Assembly", "Constraints")
def shouldShow(self):
if not super().shouldShow():
return False
return UtilsAssembly.assembly_has_at_least_n_parts(2)
class AssemblyToolsWatcher(AssemblyBaseWatcher):
"""Shows Joint, View, and BOM tools when there are enough parts."""
def __init__(self):
super().__init__()
self.commands = [
"Assembly_CreateView",
"Assembly_CreateBom",
]
self.title = translate("Assembly", "Tools")
def shouldShow(self):
if not super().shouldShow():
return False
return UtilsAssembly.assembly_has_at_least_n_parts(1)
class AssemblySimulationWatcher(AssemblyBaseWatcher):
"""Shows 'Create Simulation' when specific motional joints exist."""
def __init__(self):
super().__init__()
self.commands = ["Assembly_CreateSimulation"]
self.title = translate("Assembly", "Simulation")
def shouldShow(self):
if not super().shouldShow():
return False
joint_types = ["Revolute", "Slider", "Cylindrical"]
joints = UtilsAssembly.getJointsOfType(self.assembly, joint_types)
return len(joints) > 0
watchers = [
AssemblyCreateWatcher(),
AssemblyActivateWatcher(),
AssemblyInsertWatcher(),
AssemblyGroundWatcher(),
AssemblyJointsWatcher(),
AssemblyToolsWatcher(),
AssemblySimulationWatcher(),
]
FreeCADGui.Control.addTaskWatcher(watchers)
Gui.addWorkbench(AssemblyWorkbench())