diff --git a/src/Gui/TaskView/TaskView.cpp b/src/Gui/TaskView/TaskView.cpp index e5ddeaf469..9e5394ba6a 100644 --- a/src/Gui/TaskView/TaskView.cpp +++ b/src/Gui/TaskView/TaskView.cpp @@ -40,6 +40,7 @@ #include #include #include +#include #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) diff --git a/src/Gui/TaskView/TaskView.h b/src/Gui/TaskView/TaskView.h index 66a7d5c8e2..3cc27a704f 100644 --- a/src/Gui/TaskView/TaskView.h +++ b/src/Gui/TaskView/TaskView.h @@ -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 diff --git a/src/Mod/Assembly/CommandCreateAssembly.py b/src/Mod/Assembly/CommandCreateAssembly.py index cb1f3ed143..6a9af33bc7 100644 --- a/src/Mod/Assembly/CommandCreateAssembly.py +++ b/src/Mod/Assembly/CommandCreateAssembly.py @@ -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()) diff --git a/src/Mod/Assembly/Gui/Resources/Assembly.qrc b/src/Mod/Assembly/Gui/Resources/Assembly.qrc index 3cbbbf896f..8455b59f51 100644 --- a/src/Mod/Assembly/Gui/Resources/Assembly.qrc +++ b/src/Mod/Assembly/Gui/Resources/Assembly.qrc @@ -1,5 +1,6 @@ + icons/Assembly_ActivateAssembly.svg icons/Assembly_AssemblyLink.svg icons/Assembly_AssemblyLinkRigid.svg icons/Assembly_InsertLink.svg diff --git a/src/Mod/Assembly/Gui/Resources/icons/Assembly_ActivateAssembly.svg b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ActivateAssembly.svg new file mode 100644 index 0000000000..ad832f7a3d --- /dev/null +++ b/src/Mod/Assembly/Gui/Resources/icons/Assembly_ActivateAssembly.svg @@ -0,0 +1,388 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path-Stock + 2015-07-04 + https://www.freecad.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path-Stock.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Assembly/InitGui.py b/src/Mod/Assembly/InitGui.py index 7e90be47f8..62394c1c90 100644 --- a/src/Mod/Assembly/InitGui.py +++ b/src/Mod/Assembly/InitGui.py @@ -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())