from collections import OrderedDict import FreeCAD, FreeCADGui from PySide import QtCore, QtGui from .deps import with_metaclass from .utils import getElementPos,objName,addIconToFCAD,guilogger as logger from .proxy import ProxyType from .FCADLogger import FCADLogger class SelectionObserver: def __init__(self): self._attached = False self.cmds = [] self.elements = dict() self.attach() def setCommands(self,cmds): self.cmds = cmds def _setElementVisible(self,obj,subname,vis): sobj = obj.getSubObject(subname,1) from .assembly import isTypeOf,AsmConstraint,\ AsmElement,AsmElementLink if isTypeOf(sobj,(AsmElement,AsmElementLink)): res = sobj.Proxy.parent.Object.isElementVisible(sobj.Name) if res and vis: return False sobj.Proxy.parent.Object.setElementVisible(sobj.Name,vis) elif isTypeOf(sobj,AsmConstraint): vis = [vis] * len(sobj.Group) sobj.setPropertyStatus('VisibilityList','-Immutable') sobj.VisibilityList = vis sobj.setPropertyStatus('VisibilityList','Immutable') else: return if vis: FreeCADGui.Selection.updateSelection(vis,obj,subname) def setElementVisible(self,docname,objname,subname,vis,presel=False): if not AsmCmdManager.AutoElementVis: self.elements.clear() return doc = FreeCAD.getDocument(docname) if not doc: return obj = doc.getObject(objname) if not obj: return key = (docname,objname,subname) val = None if not vis: val = self.elements.get(key,None) if val is None or (presel and val): return if logger.catchWarn('',self._setElementVisible, obj,subname,vis) is False and presel: return if not vis: self.elements.pop(key,None) elif not presel: self.elements[key] = True else: self.elements.setdefault(key,False) def resetElementVisible(self): elements = list(self.elements) self.elements.clear() for docname,objname,subname in elements: doc = FreeCAD.getDocument(docname) if not doc: continue obj = doc.getObject(objname) if not obj: continue logger.catchWarn('',self._setElementVisible,obj,subname,False) def onChange(self,hasSelection=True): for cmd in self.cmds: cmd.onSelectionChange(hasSelection) def addSelection(self,docname,objname,subname,_pos): self.onChange() self.setElementVisible(docname,objname,subname,True) def removeSelection(self,docname,objname,subname): self.onChange(FreeCADGui.Selection.hasSelection()) self.setElementVisible(docname,objname,subname,False) def setPreselection(self,docname,objname,subname): self.setElementVisible(docname,objname,subname,True,True) def removePreselection(self,docname,objname,subname): self.setElementVisible(docname,objname,subname,False,True) def setSelection(self,*_args): self.onChange() if AsmCmdManager.AutoElementVis: self.resetElementVisible() for sel in FreeCADGui.Selection.getSelectionEx('*',False): for sub in sel.SubElementNames: self.setElementVisible(sel.Object.Document.Name, sel.Object.Name,sub,True) def clearSelection(self,*_args): self.onChange(False) self.resetElementVisible() def attach(self): logger.trace('attach selection aboserver {}'.format(self._attached)) if not self._attached: FreeCADGui.Selection.addObserver(self,False) self._attached = True def detach(self): logger.trace('detach selection aboserver {}'.format(self._attached)) if self._attached: FreeCADGui.Selection.removeObserver(self) self._attached = False self.clearSelection('') class AsmCmdManager(ProxyType): _HiddenToolbars = set() Toolbars = OrderedDict() Menus = OrderedDict() _defaultMenuGroupName = '&Assembly3' @staticmethod def getToolbarParams(): return FreeCAD.ParamGet('User parameter:BaseApp/MainWindow/Toolbars') @classmethod def init(mcs): if not mcs.getParam('Bool','GuiInited',False): hgrp = mcs.getToolbarParams() for toolbar in mcs._HiddenToolbars: hgrp.SetBool(toolbar,False) mcs.setParam('Bool','GuiInited',True) @classmethod def toggleToolbars(mcs): hgrp = mcs.getToolbarParams() show = False for toolbar in mcs._HiddenToolbars: if not hgrp.GetBool(toolbar,True): show = True break mw = FreeCADGui.getMainWindow() for toolbar in mcs._HiddenToolbars: if show != hgrp.GetBool(toolbar,True): hgrp.SetBool(toolbar,show) tb = mw.findChild(QtGui.QToolBar,toolbar) if not tb: logger.error('cannot find toolbar "{}"'.format(toolbar)) tb.setVisible(show) @classmethod def register(mcs,cls): if cls._id < 0: return super(AsmCmdManager,mcs).register(cls) FreeCADGui.addCommand(cls.getName(),cls) if cls._toolbarName: tb = mcs.Toolbars.setdefault(cls._toolbarName,[]) if not tb and not getattr(cls,'_toolbarVisible',True): mcs._HiddenToolbars.add(cls._toolbarName) tb.append(cls) if cls._menuGroupName is not None: name = cls._menuGroupName if not name: name = mcs._defaultMenuGroupName mcs.Menus.setdefault(name,[]).append(cls) @classmethod def getParamGroup(mcs): return FreeCAD.ParamGet( 'User parameter:BaseApp/Preferences/Mod/Assembly3') @classmethod def getParam(mcs,tp,name,default=None): return getattr(mcs.getParamGroup(),'Get'+tp)(name,default) @classmethod def setParam(mcs,tp,name,v): getattr(mcs.getParamGroup(),'Set'+tp)(name,v) def workbenchActivated(cls): pass def workbenchDeactivated(cls): pass def getContextMenuName(cls): if cls.IsActive() and cls._contextMenuName: return cls._contextMenuName def getName(cls): return 'asm3'+cls.__name__[3:] def getMenuText(cls): return cls._menuText def getToolTip(cls): return getattr(cls,'_tooltip',cls.getMenuText()) def IsActive(cls): if cls._id<0 or not FreeCAD.ActiveDocument: return False if cls._active is None: cls.checkActive() return cls._active def onSelectionChange(cls, hasSelection): _ = hasSelection class AsmCmdBase(with_metaclass(AsmCmdManager, object)): _id = -1 _active = None _toolbarName = 'Assembly3' _menuGroupName = '' _contextMenuName = 'Assembly' _accel = None @classmethod def checkActive(cls): cls._active = True @classmethod def GetResources(cls): ret = { 'Pixmap':addIconToFCAD(cls._iconName), 'MenuText':cls.getMenuText(), 'ToolTip':cls.getToolTip() } if cls._accel: ret['Accel'] = cls._accel return ret class AsmCmdNew(AsmCmdBase): _id = 0 _menuText = 'Create assembly' _iconName = 'Assembly_New_Assembly.svg' _accel = 'A, N' @classmethod def Activated(cls): from . import assembly assembly.Assembly.make() class AsmCmdNewElement(AsmCmdBase): _id = 19 _menuText = 'Create element' _iconName = 'Assembly_New_Element.svg' _accel = 'A, E' @classmethod def Activated(cls): from . import assembly logger.report('Failed to add element', assembly.AsmElement.make, undo=True, allowDuplicate= QtGui.QApplication.keyboardModifiers()==QtCore.Qt.ControlModifier) @classmethod def checkActive(cls): from . import assembly cls._active = logger.catchTrace( '',assembly.AsmElement.getSelections) is not None @classmethod def onSelectionChange(cls,hasSelection): cls._active = None if hasSelection else False class AsmCmdSolve(AsmCmdBase): _id = 1 _menuText = 'Solve constraints' _iconName = 'AssemblyWorkbench.svg' _accel = 'A, S' @classmethod def Activated(cls): from . import solver FreeCAD.setActiveTransaction('Assembly solve') logger.report('command "{}" exception'.format(cls.getName()), solver.solve,reportFailed=True) FreeCAD.closeActiveTransaction() class AsmCmdMove(AsmCmdBase): _id = 2 _menuText = 'Move part' _iconName = 'Assembly_Move.svg' _accel = 'A, M' _moveInfo = None @classmethod def Activated(cls): from . import mover mover.movePart(True,cls._moveInfo) @classmethod def canMove(cls): from . import mover cls._moveInfo = None cls._moveInfo = mover.getMovingElementInfo() mover.checkFixedPart(cls._moveInfo.ElementInfo) return True @classmethod def checkActive(cls): cls._active = True if logger.catchTrace('',cls.canMove) else False @classmethod def onSelectionChange(cls,hasSelection): if not hasSelection: cls._active = False else: cls._active = None cls._moveInfo = None class AsmCmdAxialMove(AsmCmdBase): _id = 3 _menuText = 'Axial move part' _iconName = 'Assembly_AxialMove.svg' _useCenterballDragger = False _accel = 'A, A' @classmethod def IsActive(cls): return AsmCmdMove.IsActive() @classmethod def Activated(cls): from . import mover mover.movePart(False,AsmCmdMove._moveInfo) class AsmCmdQuickMove(AsmCmdAxialMove): _id = 13 _menuText = 'Quick move' _tooltip = 'Bring an object contained in an assembly to where the mouse\n'\ 'is located. This is designed to help bringing an object far\n'\ 'away quickly into view.' _iconName = 'Assembly_QuickMove.svg' _accel = 'A, Q' @classmethod def Activated(cls): from . import mover mover.quickMove() class AsmCmdCheckable(AsmCmdBase): _id = -2 _saveParam = False _defaultValue = False @classmethod def getAttributeName(cls): return cls.__name__[6:] @classmethod def getChecked(cls): return getattr(cls.__class__,cls.getAttributeName()) @classmethod def setChecked(cls,v): setattr(cls.__class__,cls.getAttributeName(),v) if cls._saveParam: cls.setParam('Bool',cls.getAttributeName(),v) @classmethod def onRegister(cls): if cls._saveParam: v = cls.getParam('Bool',cls.getAttributeName(),cls._defaultValue) else: v = False cls.setChecked(v) @classmethod def GetResources(cls): ret = super(AsmCmdCheckable,cls).GetResources() ret['Checkable'] = cls.getChecked() return ret @classmethod def Activated(cls,checked): cls.setChecked(True if checked else False) class AsmCmdLockMover(AsmCmdCheckable): _id = 15 _menuText = 'Lock mover' _tooltip = 'Lock mover for fixed part' _iconName = 'Assembly_LockMover.svg' _saveParam = True @classmethod def Activated(cls,checked): super(AsmCmdLockMover,cls).Activated(checked) AsmCmdMove._active = None AsmCmdAxialMove._active = None AsmCmdQuickMove._active = None class AsmCmdToggleVisibility(AsmCmdBase): _id = 17 _menuText = 'Toggle part visibility' _tooltip = 'Toggle the visibility of the selected part' _iconName = 'Assembly_TogglePartVisibility.svg' _accel = 'A, Space' @classmethod def Activated(cls): moveInfo = AsmCmdMove._moveInfo if not moveInfo: return info = moveInfo.ElementInfo if info.Subname: subs = moveInfo.SelSubname[:-len(info.Subname)] else: subs = moveInfo.SelSubname subs = subs.split('.') if isinstance(info.Part,tuple): part = info.Part[0] vis = part.isElementVisible(str(info.Part[1])) part.setElementVisible(str(info.Part[1]),not vis) else: from .assembly import resolveAssembly partGroup = resolveAssembly(info.Parent).getPartGroup() vis = partGroup.isElementVisible(info.Part.Name) partGroup.setElementVisible(info.Part.Name,not vis) FreeCADGui.Selection.clearSelection() FreeCADGui.Selection.addSelection(moveInfo.SelObj,'.'.join(subs)) FreeCADGui.runCommand('Std_TreeSelection') if vis: FreeCADGui.runCommand('Std_TreeCollapse') @classmethod def IsActive(cls): return AsmCmdMove._moveInfo is not None class AsmCmdTrace(AsmCmdCheckable): _id = 4 _menuText = 'Trace part move' _iconName = 'Assembly_Trace.svg' _object = None _subname = None @classmethod def Activated(cls,checked): super(AsmCmdTrace,cls).Activated(checked) if not checked: cls._object = None return sel = FreeCADGui.Selection.getSelectionEx('',False) if len(sel)==1: subs = sel[0].SubElementNames if len(subs)==1: cls._object = sel[0].Object cls._subname = subs[0] logger.info('trace {}.{}'.format( cls._object.Name,cls._subname)) return logger.info('trace moving element') @classmethod def getPosition(cls): if not cls._object: return try: if cls._object.Document != FreeCAD.ActiveDocument: cls._object = None return getElementPos((cls._object,cls._subname)) except Exception: cls._object = None class AsmCmdAutoRecompute(AsmCmdCheckable): _id = 5 _menuText = 'Auto recompute' _iconName = 'Assembly_AutoRecompute.svg' _saveParam = True class AsmCmdAutoElementVis(AsmCmdCheckable): _id = 9 _menuText = 'Auto element visibility' _iconName = 'Assembly_AutoElementVis.svg' _saveParam = True _defaultValue = True @classmethod def Activated(cls,checked): super(AsmCmdAutoElementVis,cls).Activated(checked) from .assembly import isTypeOf,AsmConstraint,\ AsmElement,AsmElementLink,AsmElementGroup for doc in FreeCAD.listDocuments().values(): for obj in doc.Objects: if isTypeOf(obj,(AsmConstraint,AsmElementGroup)): obj.Visibility = False if isTypeOf(obj,AsmConstraint): obj.ViewObject.OnTopWhenSelected = 2 obj.setPropertyStatus('VisibilityList', 'NoModify' if checked else '-NoModify') elif isTypeOf(obj,(AsmElementLink,AsmElement)): if checked: obj.Proxy.parent.Object.setElementVisible( obj.Name,False) obj.Visibility = False obj.ViewObject.OnTopWhenSelected = 2 class AsmCmdAddWorkplane(AsmCmdBase): _id = 8 _menuText = 'Add workplane' _iconName = 'Assembly_Add_Workplane.svg' _toolbarName = None _menuGroupName = None _accel = 'A, P' _makeType = 0 @classmethod def checkActive(cls): from . import assembly if logger.catchTrace('Add workplane selection', assembly.AsmWorkPlane.getSelection): cls._active = True else: cls._active = False @classmethod def onSelectionChange(cls,hasSelection): cls._active = None if hasSelection else False @classmethod def Activated(cls,idx=0): _ = idx from . import assembly assembly.AsmWorkPlane.make(tp=cls._makeType) class AsmCmdAddWorkplaneXZ(AsmCmdAddWorkplane): _id = 10 _menuText = 'Add XZ workplane' _iconName = 'Assembly_Add_WorkplaneXZ.svg' _makeType = 1 class AsmCmdAddWorkplaneZY(AsmCmdAddWorkplane): _id = 11 _menuText = 'Add ZY workplane' _iconName = 'Assembly_Add_WorkplaneZY.svg' _makeType = 2 class AsmCmdAddOrigin(AsmCmdAddWorkplane): _id = 14 _menuText = 'Add Origin' _iconName = 'Assembly_Add_Origin.svg' _makeType = 3 _accel = 'A, O' class AsmCmdAddWorkplaneGroup(AsmCmdAddWorkplane): _id = 12 _menuGroupName = '' _toolbarName = AsmCmdBase._toolbarName _cmds = (AsmCmdAddWorkplane.getName(), AsmCmdAddWorkplaneXZ.getName(), AsmCmdAddWorkplaneZY.getName(), AsmCmdAddOrigin.getName()) @classmethod def GetCommands(cls): return cls._cmds @classmethod def Activated(cls,idx=0): FreeCADGui.runCommand(cls._cmds[idx]) class AsmCmdGotoRelation(AsmCmdBase): _id = 16 _menuText = 'Go to relation' _tooltip = 'Select the corresponding part object in the relation group' _iconName = 'Assembly_GotoRelation.svg' _accel = 'A, R' _toolbarName = '' @classmethod def Activated(cls): from .assembly import AsmRelationGroup if AsmCmdMove._moveInfo: AsmRelationGroup.gotoRelation(AsmCmdMove._moveInfo) return sels = FreeCADGui.Selection.getSelectionEx('',0,True) if sels and len(sels[0].SubElementNames)==1: AsmRelationGroup.gotoRelationOfConstraint( sels[0].Object,sels[0].SubElementNames[0]) @classmethod def IsActive(cls): if AsmCmdMove._moveInfo: return True if cls._active is None: cls.checkActive() return cls._active @classmethod def checkActive(cls): from .assembly import isTypeOf, AsmConstraint, AsmElementLink sels = FreeCADGui.Selection.getSelection('',1,True) if sels and isTypeOf(sels[0],(AsmConstraint,AsmElementLink)): cls._active = True else: cls._active = False @classmethod def onSelectionChange(cls,hasSelection): cls._active = None if hasSelection else False class AsmCmdUp(AsmCmdBase): _id = 6 _menuText = 'Move item up' _iconName = 'Assembly_TreeItemUp.svg' @classmethod def getSelection(cls): from .assembly import isTypeOf, Assembly, AsmGroup sels = FreeCADGui.Selection.getSelectionEx('',False) if len(sels)!=1 or len(sels[0].SubElementNames)!=1: return ret= sels[0].Object.resolve(sels[0].SubElementNames[0]) obj,parent = ret[0],ret[1] if isTypeOf(parent,Assembly) or not isTypeOf(parent,AsmGroup) or \ len(parent.Group) <= 1: return return (obj,parent,sels[0].Object,sels[0].SubElementNames[0]) @classmethod def checkActive(cls): cls._active = True if cls.getSelection() else False @classmethod def move(cls,step): ret = cls.getSelection() if not ret: return obj,parent,topParent,subname = ret children = parent.Group i = children.index(obj) j = i+step if j<0: j = len(children)-1 elif j>=len(children): j = 0 logger.debug('move {}:{} -> {}:{}'.format( i,objName(obj),j,objName(children[j]))) FreeCAD.setActiveTransaction(cls._menuText) readonly = 'Immutable' in parent.getPropertyStatus('Group') if readonly: parent.setPropertyStatus('Group','-Immutable') parent.Group = {i:children[j],j:obj} if readonly: parent.setPropertyStatus('Group','Immutable') FreeCAD.closeActiveTransaction(); # The tree view may deselect the item because of claimChildren changes, # so we restore the selection here FreeCADGui.Selection.addSelection(topParent,subname) @classmethod def onSelectionChange(cls,hasSelection): cls._active = None if hasSelection else False @classmethod def Activated(cls): cls.move(-1) class AsmCmdDown(AsmCmdUp): _id = 7 _menuText = 'Move item down' _iconName = 'Assembly_TreeItemDown.svg' @classmethod def Activated(cls): cls.move(1) class AsmCmdMultiply(AsmCmdBase): _id = 18 _menuText = 'Multiply constraint' _tooltip = 'Mutiply the part owner of the first element to constrain\n'\ 'against the rest of the elements.\n\n'\ 'To activate this function, the FIRST part must be of the\n'\ 'FIRST element of a link array. In will optionally expand\n'\ 'colplanar circular edges with the same radius in the second\n'\ 'element on wards. To disable auto expansion, use NoExpand\n'\ 'property in the element link.' _iconName = 'Assembly_ConstraintMultiply.svg' @classmethod def checkActive(cls): from .assembly import AsmConstraint if logger.catchTrace('',AsmConstraint.makeMultiply,True): cls._active = True else: cls._active = False @classmethod def Activated(cls): from .assembly import AsmConstraint logger.report('',AsmConstraint.makeMultiply) @classmethod def onSelectionChange(cls,hasSelection): cls._active = None if hasSelection else False