diff --git a/src/Gui/Placement.ui b/src/Gui/Placement.ui
index da36fffeca..5c90aad657 100644
--- a/src/Gui/Placement.ui
+++ b/src/Gui/Placement.ui
@@ -414,7 +414,7 @@
-
- Euler angles (XY'Z'')
+ Euler angles (xy'z'')
diff --git a/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py
index 63109115a1..e11864b86e 100644
--- a/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py
+++ b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.py
@@ -1,604 +1,604 @@
-#/***************************************************************************
-# * Copyright (c) 2016 Victor Titov (DeepSOIC) *
-# * *
-# * This file is part of the FreeCAD CAx development system. *
-# * *
-# * This library is free software; you can redistribute it and/or *
-# * modify it under the terms of the GNU Library General Public *
-# * License as published by the Free Software Foundation; either *
-# * version 2 of the License, or (at your option) any later version. *
-# * *
-# * This library is distributed in the hope that it will be useful, *
-# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
-# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
-# * GNU Library General Public License for more details. *
-# * *
-# * You should have received a copy of the GNU Library General Public *
-# * License along with this library; see the file COPYING.LIB. If not, *
-# * write to the Free Software Foundation, Inc., 59 Temple Place, *
-# * Suite 330, Boston, MA 02111-1307, USA *
-# * *
-# ***************************************************************************/
-
-from __future__ import absolute_import
-
-import FreeCAD as App
-import Part
-mm = App.Units.MilliMetre
-deg = App.Units.Degree
-Q = App.Units.Quantity
-
-from AttachmentEditor.FrozenClass import FrozenClass
-try:
- from Show import TempoVis
- from Show.DepGraphTools import getAllDependent
-except ImportError as err:
- def TempoVis(doc):
- return None
- def getAllDependent(feature):
- return []
- App.Console.PrintWarning("AttachmentEditor: Failed to import some code from Show module. Functionality will be limited.\n")
- App.Console.PrintWarning(str(err))
-
-if App.GuiUp:
- import FreeCADGui as Gui
- from PySide import QtCore, QtGui
- from FreeCADGui import PySideUic as uic
-
-#-------------------------- translation-related code ----------------------------------------
-#Thanks, yorik! (see forum thread "A new Part tool is being born... JoinFeatures!"
-#http://forum.freecadweb.org/viewtopic.php?f=22&t=11112&start=30#p90239 )
-try:
- _fromUtf8 = QtCore.QString.fromUtf8
-except AttributeError:
- def _fromUtf8(s):
- return s
-try:
- _encoding = QtGui.QApplication.UnicodeUTF8
- def _translate(context, text, disambig):
- return QtGui.QApplication.translate(context, text, disambig, _encoding)
-except AttributeError:
- def _translate(context, text, disambig):
- return QtGui.QApplication.translate(context, text, disambig)
-#--------------------------/translation-related code ----------------------------------------
-
-def linkSubList_convertToOldStyle(references):
- ("input: [(obj1, (sub1, sub2)), (obj2, (sub1, sub2))]\n"
- "output: [(obj1, sub1), (obj1, sub2), (obj2, sub1), (obj2, sub2)]")
- result = []
- for tup in references:
- if type(tup[1]) is tuple or type(tup[1]) is list:
- for subname in tup[1]:
- result.append((tup[0], subname))
- if len(tup[1]) == 0:
- result.append((tup[0], ''))
- elif isinstance(tup[1],basestring):
- # old style references, no conversion required
- result.append(tup)
- return result
-
-
-def StrFromLink(feature, subname):
- return feature.Name+ ((':'+subname) if subname else '')
-
-def LinkFromStr(strlink, document):
- if len(strlink) == 0:
- return None
- pieces = strlink.split(':')
-
- feature = document.getObject(pieces[0])
-
- subname = ''
- if feature is None:
- raise ValueError(_translate('AttachmentEditor',"No object named {name}",None).format(name= pieces[0]))
- if len(pieces) == 2:
- subname = pieces[1]
- elif len(pieces) > 2:
- raise ValueError(_translate('AttachmentEditor',"Failed to parse link (more than one colon encountered)",None))
-
- return (feature,str(subname)) #wrap in str to remove unicode, which confuses assignment to PropertyLinkSubList.
-
-def StrListFromRefs(references):
- '''input: PropertyLinkSubList. Output: list of strings for UI.'''
- references_oldstyle = linkSubList_convertToOldStyle(references)
- return [StrFromLink(feature,subelement) for (feature, subelement) in references_oldstyle]
-
-def RefsFromStrList(strings, document):
- '''input: strings as from UI. Output: list of tuples that can be assigned to PropertyLinkSubList.'''
- refs = []
- for st in strings:
- lnk = LinkFromStr(st, document)
- if lnk is not None:
- refs.append(lnk)
- return refs
-
-def GetSelectionAsLinkSubList():
- sel = Gui.Selection.getSelectionEx()
- result = []
- for selobj in sel:
- for subname in selobj.SubElementNames:
- result.append((selobj.Object, subname))
- if len(selobj.SubElementNames) == 0:
- result.append((selobj.Object, ''))
- return result
-
-
-def PlacementsFuzzyCompare(plm1, plm2):
- pos_eq = (plm1.Base - plm2.Base).Length < 1e-7 # 1e-7 is OCC's Precision::Confusion
-
- q1 = plm1.Rotation.Q
- q2 = plm2.Rotation.Q
- # rotations are equal if q1 == q2 or q1 == -q2.
- # Invert one of Q's if their scalar product is negative, before comparison.
- if q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2] + q1[3]*q2[3] < 0:
- q2 = [-v for v in q2]
- rot_eq = ( abs(q1[0]-q2[0]) +
- abs(q1[1]-q2[1]) +
- abs(q1[2]-q2[2]) +
- abs(q1[3]-q2[3]) ) < 1e-12 # 1e-12 is OCC's Precision::Angular (in radians)
- return pos_eq and rot_eq
-
-class CancelError(Exception):
- def __init__(self):
- self.message = 'Canceled by user'
- self.isCancelError = True
-
-class AttachmentEditorTaskPanel(FrozenClass):
- '''The editmode TaskPanel for attachment editing'''
- KEYmode = QtCore.Qt.ItemDataRole.UserRole # Key to use in Item.data(key) to obtain a mode associated with list item
- KEYon = QtCore.Qt.ItemDataRole.UserRole + 1 # Key to use in Item.data(key) to obtain if the mode is valid
-
- def __define_attributes(self):
- self.obj = None #feature being attached
- self.attacher = None #AttachEngine that is being actively used by the dialog. Its parameters are constantly and actively kept in sync with the dialog.
- self.obj_is_attachable = True # False when editing non-attachable objects (alignment, not attachment)
-
- self.last_sugr = None #result of last execution of suggestor
-
- self.form = None #Qt widget of dialog interface
- self.block = False #when True, event handlers return without doing anything (instead of doing-undoing blockSignals to everything)
- self.refLines = [] #reference lineEdit widgets, packed into a list for convenience
- self.refButtons = [] #buttons next to reference lineEdits
- self.attachmentOffsetEdits = [] #all edit boxes related to attachmentOffset
- self.i_active_ref = -1 #index of reference being selected (-1 means no reaction to selecting)
- self.auto_next = False #if true, references being selected are appended ('Selecting' state is automatically advanced to next button)
-
- self.tv = None #TempoVis class instance
-
- self.create_transaction = True # if false, dialog doesn't mess with transactions.
- self.callback_OK = None
- self.callback_Cancel = None
- self.callback_Apply = None
-
- self._freeze()
-
- def __init__(self, obj_to_attach,
- take_selection = False,
- create_transaction = True,
- callback_OK = None,
- callback_Cancel = None,
- callback_Apply = None):
-
- self.__define_attributes()
-
- self.create_transaction = create_transaction
- self.callback_OK = callback_OK
- self.callback_Cancel = callback_Cancel
- self.callback_Apply = callback_Apply
-
- self.obj = obj_to_attach
- if hasattr(obj_to_attach,'Attacher'):
- self.attacher = obj_to_attach.Attacher
- elif hasattr(obj_to_attach,'AttacherType'):
- self.attacher = Part.AttachEngine(obj_to_attach.AttacherType)
- else:
- movable = True
- if not hasattr(self.obj, "Placement"):
- movable = False
- if 'Hidden' in self.obj.getEditorMode("Placement") or 'ReadOnly' in self.obj.getEditorMode("Placement"):
- movable = False
- if not movable:
- if self.callback_Cancel:
- self.callback_Cancel()
- raise ValueError(_translate('AttachmentEditor',"Object {name} is neither movable nor attachable, can't edit attachment",None)
- .format(name= self.obj.Label))
-
- self.obj_is_attachable = False
- self.attacher = Part.AttachEngine()
-
- mb = QtGui.QMessageBox()
- mb.setIcon(mb.Icon.Warning)
- mb.setText(_translate('AttachmentEditor',
- "{obj} is not attachable. You can still use attachment editor dialog to align the object, but the attachment won't be parametric."
- ,None)
- .format(obj= obj_to_attach.Label))
- mb.setWindowTitle(_translate('AttachmentEditor',"Attachment",None))
- btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
- btnOK = mb.addButton(_translate('AttachmentEditor',"Continue",None),QtGui.QMessageBox.ButtonRole.ActionRole)
- mb.setDefaultButton(btnOK)
- mb.exec_()
- if mb.clickedButton() is btnAbort:
- if self.callback_Cancel:
- self.callback_Cancel()
- raise CancelError()
-
- import os
- self.form=uic.loadUi(os.path.dirname(__file__) + os.path.sep + 'TaskAttachmentEditor.ui')
- self.form.setWindowIcon(QtGui.QIcon(':/icons/Part_Attachment.svg'))
- self.form.setWindowTitle(_translate('AttachmentEditor',"Attachment",None))
-
- self.refLines = [self.form.lineRef1,
- self.form.lineRef2,
- self.form.lineRef3,
- self.form.lineRef4]
- self.refButtons = [self.form.buttonRef1,
- self.form.buttonRef2,
- self.form.buttonRef3,
- self.form.buttonRef4]
- self.attachmentOffsetEdits = [self.form.attachmentOffsetX,
- self.form.attachmentOffsetY,
- self.form.attachmentOffsetZ,
- self.form.attachmentOffsetYaw,
- self.form.attachmentOffsetPitch,
- self.form.attachmentOffsetRoll]
-
- self.block = False
-
- for i in range(len(self.refLines)):
- QtCore.QObject.connect(self.refLines[i], QtCore.SIGNAL('textEdited(QString)'), lambda txt, i=i: self.lineRefChanged(i,txt))
-
- for i in range(len(self.refLines)):
- QtCore.QObject.connect(self.refButtons[i], QtCore.SIGNAL('clicked()'), lambda i=i: self.refButtonClicked(i))
-
- for i in range(len(self.attachmentOffsetEdits)):
- QtCore.QObject.connect(self.attachmentOffsetEdits[i], QtCore.SIGNAL('valueChanged(double)'), lambda val, i=i: self.attachmentOffsetChanged(i,val))
-
- QtCore.QObject.connect(self.form.checkBoxFlip, QtCore.SIGNAL('clicked()'), self.checkBoxFlipClicked)
-
- QtCore.QObject.connect(self.form.listOfModes, QtCore.SIGNAL('itemSelectionChanged()'), self.modeSelected)
-
- if self.create_transaction:
- self.obj.Document.openTransaction(_translate('AttachmentEditor',"Edit attachment of {feat}",None).format(feat= self.obj.Name))
-
-
- self.readParameters()
-
-
- if len(self.attacher.References) == 0 and take_selection:
- sel = GetSelectionAsLinkSubList()
- for i in range(len(sel))[::-1]:
- if sel[i][0] is obj_to_attach:
- sel.pop(i)
- self.attacher.References = sel
- # need to update textboxes
- self.fillAllRefLines()
-
- if len(self.attacher.References) == 0:
- self.i_active_ref = 0
- self.auto_next = True
- else:
- self.i_active_ref = -1
- self.auto_next = False
-
- Gui.Selection.addObserver(self)
-
- self.updatePreview()
- self.updateRefButtons()
-
- self.tv = TempoVis(self.obj.Document, tag= "PartGui.TaskAttachmentEditor")
- if self.tv: # tv will still be None if Show module is unavailable
- self.tv.hide_all_dependent(self.obj)
- self.tv.show(self.obj)
- self.tv.setUnpickable(self.obj)
- self.tv.modifyVPProperty(self.obj, "Transparency", 70)
- self.tv.show([obj for (obj,subname) in self.attacher.References])
-
- # task dialog handling
- def getStandardButtons(self):
- return int(QtGui.QDialogButtonBox.Ok) | int(QtGui.QDialogButtonBox.Cancel)| int(QtGui.QDialogButtonBox.Apply)
-
- def clicked(self,button):
- if button == QtGui.QDialogButtonBox.Apply:
- if self.obj_is_attachable:
- self.writeParameters()
- self.updatePreview()
- if self.callback_Apply:
- self.callback_Apply()
-
- def accept(self):
- if self.obj_is_attachable:
- self.writeParameters()
- if self.create_transaction:
- self.obj.Document.commitTransaction()
- self.cleanUp()
- Gui.Control.closeDialog()
- if self.callback_OK:
- self.callback_OK()
-
- def reject(self):
- if self.create_transaction:
- self.obj.Document.abortTransaction()
- self.cleanUp()
- Gui.Control.closeDialog()
- if self.callback_Cancel:
- self.callback_Cancel()
-
-
- #selectionObserver stuff
- def addSelection(self,docname,objname,subname,pnt):
- i = self.i_active_ref
- if i < 0:
- #not selecting any reference
- return
- if i > 0 and self.auto_next:
- prevref = LinkFromStr( self.refLines[i-1].text(), self.obj.Document )
- if prevref[0].Name == objname and subname == '':
- # whole object was selected by double-clicking
- # its subelement was already written to line[i-1], so we decrease i to overwrite the lineRefChanged
- i -= 1
- if i > len(self.refLines)-1:
- # all 4 references have been selected, finish
- assert(self.auto_next)
- self.i_active_ref = -1
- self.updateRefButtons()
- return
- if i > -1:
- # assign the selected reference
- if objname == self.obj.Name:
- self.form.message.setText(_translate('AttachmentEditor',"Ignored. Can't attach object to itself!",None))
- return
- if App.getDocument(docname).getObject(objname) in getAllDependent(self.obj):
- self.form.message.setText(_translate('AttachmentEditor',"{obj1} depends on object being attached, can't use it for attachment",None).format(obj1= objname))
- return
-
- self.refLines[i].setText( StrFromLink(App.getDocument(docname).getObject(objname), subname) )
- self.lineRefChanged(i,'')
- if self.auto_next:
- i += 1
- self.i_active_ref = i
- self.updateRefButtons()
-
- # slots
-
- def attachmentOffsetChanged(self, index, value):
- if self.block:
- return
- plm = self.attacher.AttachmentOffset
- pos = plm.Base
- if index==0:
- pos.x = Q(self.form.attachmentOffsetX.text()).getValueAs(mm)
- if index==1:
- pos.y = Q(self.form.attachmentOffsetY.text()).getValueAs(mm)
- if index==2:
- pos.z = Q(self.form.attachmentOffsetZ.text()).getValueAs(mm)
- if index >= 0 and index <= 2:
- plm.Base = pos
-
- rot = plm.Rotation;
- (yaw, pitch, roll) = rot.toEuler()
- if index==3:
- yaw = Q(self.form.attachmentOffsetYaw.text()).getValueAs(deg)
- if index==4:
- pitch = Q(self.form.attachmentOffsetPitch.text()).getValueAs(deg)
- if index==5:
- roll = Q(self.form.attachmentOffsetRoll.text()).getValueAs(deg)
- if index >= 3 and index <= 5:
- rot = App.Rotation(yaw,pitch,roll)
- plm.Rotation = rot
-
- self.attacher.AttachmentOffset = plm
-
- self.updatePreview()
-
- def checkBoxFlipClicked(self):
- if self.block:
- return
- self.attacher.Reverse = self.form.checkBoxFlip.isChecked()
- self.updatePreview()
-
- def lineRefChanged(self, index, value):
- if self.block:
- return
- # not parsing links here, because doing it in updatePreview will display error message
- self.updatePreview()
-
- def refButtonClicked(self, index):
- if self.block:
- return
- if self.i_active_ref == index:
- #stop selecting
- self.i_active_ref = -1
- else:
- #start selecting
- self.i_active_ref = index
- self.auto_next = False
- self.updateRefButtons()
-
- def modeSelected(self):
- if self.block:
- return
- self.attacher.Mode = self.getCurrentMode()
- self.updatePreview()
-
- #internal methods
- def writeParameters(self):
- 'Transfer from the dialog to the object'
- self.attacher.writeParametersToFeature(self.obj)
-
- def readParameters(self):
- 'Transfer from the object to the dialog'
- if self.obj_is_attachable:
- self.attacher.readParametersFromFeature(self.obj)
-
- plm = self.attacher.AttachmentOffset
- try:
- old_selfblock = self.block
- self.block = True
- self.form.attachmentOffsetX.setText ((plm.Base.x * mm).UserString)
- self.form.attachmentOffsetY.setText ((plm.Base.y * mm).UserString)
- self.form.attachmentOffsetZ.setText ((plm.Base.z * mm).UserString)
- self.form.attachmentOffsetYaw.setText ((plm.Rotation.toEuler()[0] * deg).UserString)
- self.form.attachmentOffsetPitch.setText((plm.Rotation.toEuler()[1] * deg).UserString)
- self.form.attachmentOffsetRoll.setText ((plm.Rotation.toEuler()[2] * deg).UserString)
-
- self.form.checkBoxFlip.setChecked(self.attacher.Reverse)
-
- self.fillAllRefLines()
- finally:
- self.block = old_selfblock
-
- def fillAllRefLines(self):
- old_block = self.block
- try:
- self.block = True
- strings = StrListFromRefs(self.attacher.References)
- if len(strings) < len(self.refLines):
- strings.extend(['']*(len(self.refLines) - len(strings)))
- for i in range(len(self.refLines)):
- self.refLines[i].setText(strings[i])
- finally:
- self.block = old_block
-
- def parseAllRefLines(self):
- self.attacher.References = RefsFromStrList([le.text() for le in self.refLines], self.obj.Document)
-
- def updateListOfModes(self):
- '''needs suggestor to have been called, and assigned to self.last_sugr'''
- try:
- old_selfblock = self.block
- self.block = True
- list_widget = self.form.listOfModes
- list_widget.clear()
- sugr = self.last_sugr
- # always have the option to choose Deactivated mode
- valid_modes = ['Deactivated'] + sugr['allApplicableModes']
-
- # add valid modes
- for m in valid_modes:
- item = QtGui.QListWidgetItem()
- txt = self.attacher.getModeInfo(m)['UserFriendlyName']
- item.setText(txt)
- item.setData(self.KEYmode,m)
- item.setData(self.KEYon,True)
- if m == sugr['bestFitMode']:
- f = item.font()
- f.setBold(True)
- item.setFont(f)
- list_widget.addItem(item)
- item.setSelected(self.attacher.Mode == m)
- # add potential modes
- for m in sugr['reachableModes'].keys():
- item = QtGui.QListWidgetItem()
- txt = self.attacher.getModeInfo(m)['UserFriendlyName']
- listlistrefs = sugr['reachableModes'][m]
- if len(listlistrefs) == 1:
- listrefs_userfriendly = [self.attacher.getRefTypeInfo(t)['UserFriendlyName'] for t in listlistrefs[0]]
- txt = _translate('AttachmentEditor',"{mode} (add {morerefs})",None).format(mode= txt,
- morerefs= u"+".join(listrefs_userfriendly))
- else:
- txt = _translate('AttachmentEditor',"{mode} (add more references)",None).format(mode= txt)
- item.setText(txt)
- item.setData(self.KEYmode,m)
- item.setData(self.KEYon,True)
- if m == sugr['bestFitMode']:
- f = item.font()
- f.setBold(True)
- item.setFont(f)
-
- #disable this item
- f = item.flags()
- f = f & ~(QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable)
- item.setFlags(f)
-
- list_widget.addItem(item)
-
- # re-scan the list to fill in tooltips
- for item in list_widget.findItems('', QtCore.Qt.MatchContains):
- m = item.data(self.KEYmode)
- on = item.data(self.KEYon)
-
- mi = self.attacher.getModeInfo(m)
- cmb = []
- for refstr in mi['ReferenceCombinations']:
- refstr_userfriendly = [self.attacher.getRefTypeInfo(t)['UserFriendlyName'] for t in refstr]
- cmb.append(u", ".join(refstr_userfriendly))
-
- tip = mi['BriefDocu']
- if (m != 'Deactivated'):
- tip += u"\n\n"
- tip += _translate('AttachmentEditor', "Reference combinations:", None) + u" \n\n".join(cmb)
-
- item.setToolTip(tip)
-
- finally:
- self.block = old_selfblock
-
-
- def updateRefButtons(self):
- try:
- old_selfblock = self.block
- self.block = True
- for i in range(len(self.refButtons)):
- btn = self.refButtons[i]
- btn.setCheckable(True)
- btn.setChecked(self.i_active_ref == i)
- typ = _translate('AttachmentEditor',"Reference{i}",None).format(i= str(i+1))
- if self.last_sugr is not None:
- typestr = self.last_sugr['references_Types']
- if i < len(typestr):
- typ = self.attacher.getRefTypeInfo(typestr[i])['UserFriendlyName']
- btn.setText(_translate('AttachmentEditor',"Selecting...",None) if self.i_active_ref == i else typ)
- finally:
- self.block = old_selfblock
-
- def getCurrentMode(self):
- list_widget = self.form.listOfModes
- sel = list_widget.selectedItems()
- if len(sel) == 1:
- if sel[0].data(self.KEYon):
- return str(sel[0].data(self.KEYmode)) # data() returns unicode, which confuses attacher
- # nothing selected in list. Return suggested
- if self.last_sugr is not None:
- if self.last_sugr['message'] == 'OK':
- return self.last_sugr['bestFitMode']
- # no suggested mode. Return current, so it doesn't change
- return self.attacher.Mode
-
- def updatePreview(self):
- new_plm = None
-
- try:
- self.parseAllRefLines()
- self.last_sugr = self.attacher.suggestModes()
- if self.last_sugr['message'] == 'LinkBroken':
- raise ValueError(_translate('AttachmentEditor',"Failed to resolve links. {err}",None).format(err= self.last_sugr['error']))
-
- self.updateListOfModes()
-
- self.attacher.Mode = self.getCurrentMode()
-
- new_plm = self.attacher.calculateAttachedPlacement(self.obj.Placement)
- if new_plm is None:
- self.form.message.setText(_translate('AttachmentEditor',"Not attached",None))
- else:
- self.form.message.setText( _translate('AttachmentEditor',"Attached with mode {mode}",None)
- .format( mode= self.attacher.getModeInfo(self.getCurrentMode())['UserFriendlyName'] ) )
- if PlacementsFuzzyCompare(self.obj.Placement, new_plm) == False:
- # assign only if placement changed. this avoids touching the object
- # when entering and extiting dialog without changing anything
- self.obj.Placement = new_plm
- except Exception as err:
- self.form.message.setText(_translate('AttachmentEditor',"Error: {err}",None).format(err= str(err)))
-
- if new_plm is not None:
- self.form.groupBox_AttachmentOffset.setTitle(_translate('AttachmentEditor',"Attachment Offset:",None))
- self.form.groupBox_AttachmentOffset.setEnabled(True)
- else:
- self.form.groupBox_AttachmentOffset.setTitle(_translate('AttachmentEditor',"Attachment Offset (inactive - not attached):",None))
- self.form.groupBox_AttachmentOffset.setEnabled(False)
-
- def cleanUp(self):
- '''stuff that needs to be done when dialog is closed.'''
- Gui.Selection.removeObserver(self)
- if self.tv:
- self.tv.restore()
+#/***************************************************************************
+# * Copyright (c) 2016 Victor Titov (DeepSOIC) *
+# * *
+# * This file is part of the FreeCAD CAx development system. *
+# * *
+# * This library is free software; you can redistribute it and/or *
+# * modify it under the terms of the GNU Library General Public *
+# * License as published by the Free Software Foundation; either *
+# * version 2 of the License, or (at your option) any later version. *
+# * *
+# * This library is distributed in the hope that it will be useful, *
+# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+# * GNU Library General Public License for more details. *
+# * *
+# * You should have received a copy of the GNU Library General Public *
+# * License along with this library; see the file COPYING.LIB. If not, *
+# * write to the Free Software Foundation, Inc., 59 Temple Place, *
+# * Suite 330, Boston, MA 02111-1307, USA *
+# * *
+# ***************************************************************************/
+
+from __future__ import absolute_import
+
+import FreeCAD as App
+import Part
+mm = App.Units.MilliMetre
+deg = App.Units.Degree
+Q = App.Units.Quantity
+
+from AttachmentEditor.FrozenClass import FrozenClass
+try:
+ from Show import TempoVis
+ from Show.DepGraphTools import getAllDependent
+except ImportError as err:
+ def TempoVis(doc):
+ return None
+ def getAllDependent(feature):
+ return []
+ App.Console.PrintWarning("AttachmentEditor: Failed to import some code from Show module. Functionality will be limited.\n")
+ App.Console.PrintWarning(str(err))
+
+if App.GuiUp:
+ import FreeCADGui as Gui
+ from PySide import QtCore, QtGui
+ from FreeCADGui import PySideUic as uic
+
+#-------------------------- translation-related code ----------------------------------------
+#Thanks, yorik! (see forum thread "A new Part tool is being born... JoinFeatures!"
+#http://forum.freecadweb.org/viewtopic.php?f=22&t=11112&start=30#p90239 )
+try:
+ _fromUtf8 = QtCore.QString.fromUtf8
+except AttributeError:
+ def _fromUtf8(s):
+ return s
+try:
+ _encoding = QtGui.QApplication.UnicodeUTF8
+ def _translate(context, text, disambig):
+ return QtGui.QApplication.translate(context, text, disambig, _encoding)
+except AttributeError:
+ def _translate(context, text, disambig):
+ return QtGui.QApplication.translate(context, text, disambig)
+#--------------------------/translation-related code ----------------------------------------
+
+def linkSubList_convertToOldStyle(references):
+ ("input: [(obj1, (sub1, sub2)), (obj2, (sub1, sub2))]\n"
+ "output: [(obj1, sub1), (obj1, sub2), (obj2, sub1), (obj2, sub2)]")
+ result = []
+ for tup in references:
+ if type(tup[1]) is tuple or type(tup[1]) is list:
+ for subname in tup[1]:
+ result.append((tup[0], subname))
+ if len(tup[1]) == 0:
+ result.append((tup[0], ''))
+ elif isinstance(tup[1],basestring):
+ # old style references, no conversion required
+ result.append(tup)
+ return result
+
+
+def StrFromLink(feature, subname):
+ return feature.Name+ ((':'+subname) if subname else '')
+
+def LinkFromStr(strlink, document):
+ if len(strlink) == 0:
+ return None
+ pieces = strlink.split(':')
+
+ feature = document.getObject(pieces[0])
+
+ subname = ''
+ if feature is None:
+ raise ValueError(_translate('AttachmentEditor',"No object named {name}",None).format(name= pieces[0]))
+ if len(pieces) == 2:
+ subname = pieces[1]
+ elif len(pieces) > 2:
+ raise ValueError(_translate('AttachmentEditor',"Failed to parse link (more than one colon encountered)",None))
+
+ return (feature,str(subname)) #wrap in str to remove unicode, which confuses assignment to PropertyLinkSubList.
+
+def StrListFromRefs(references):
+ '''input: PropertyLinkSubList. Output: list of strings for UI.'''
+ references_oldstyle = linkSubList_convertToOldStyle(references)
+ return [StrFromLink(feature,subelement) for (feature, subelement) in references_oldstyle]
+
+def RefsFromStrList(strings, document):
+ '''input: strings as from UI. Output: list of tuples that can be assigned to PropertyLinkSubList.'''
+ refs = []
+ for st in strings:
+ lnk = LinkFromStr(st, document)
+ if lnk is not None:
+ refs.append(lnk)
+ return refs
+
+def GetSelectionAsLinkSubList():
+ sel = Gui.Selection.getSelectionEx()
+ result = []
+ for selobj in sel:
+ for subname in selobj.SubElementNames:
+ result.append((selobj.Object, subname))
+ if len(selobj.SubElementNames) == 0:
+ result.append((selobj.Object, ''))
+ return result
+
+
+def PlacementsFuzzyCompare(plm1, plm2):
+ pos_eq = (plm1.Base - plm2.Base).Length < 1e-7 # 1e-7 is OCC's Precision::Confusion
+
+ q1 = plm1.Rotation.Q
+ q2 = plm2.Rotation.Q
+ # rotations are equal if q1 == q2 or q1 == -q2.
+ # Invert one of Q's if their scalar product is negative, before comparison.
+ if q1[0]*q2[0] + q1[1]*q2[1] + q1[2]*q2[2] + q1[3]*q2[3] < 0:
+ q2 = [-v for v in q2]
+ rot_eq = ( abs(q1[0]-q2[0]) +
+ abs(q1[1]-q2[1]) +
+ abs(q1[2]-q2[2]) +
+ abs(q1[3]-q2[3]) ) < 1e-12 # 1e-12 is OCC's Precision::Angular (in radians)
+ return pos_eq and rot_eq
+
+class CancelError(Exception):
+ def __init__(self):
+ self.message = 'Canceled by user'
+ self.isCancelError = True
+
+class AttachmentEditorTaskPanel(FrozenClass):
+ '''The editmode TaskPanel for attachment editing'''
+ KEYmode = QtCore.Qt.ItemDataRole.UserRole # Key to use in Item.data(key) to obtain a mode associated with list item
+ KEYon = QtCore.Qt.ItemDataRole.UserRole + 1 # Key to use in Item.data(key) to obtain if the mode is valid
+
+ def __define_attributes(self):
+ self.obj = None #feature being attached
+ self.attacher = None #AttachEngine that is being actively used by the dialog. Its parameters are constantly and actively kept in sync with the dialog.
+ self.obj_is_attachable = True # False when editing non-attachable objects (alignment, not attachment)
+
+ self.last_sugr = None #result of last execution of suggestor
+
+ self.form = None #Qt widget of dialog interface
+ self.block = False #when True, event handlers return without doing anything (instead of doing-undoing blockSignals to everything)
+ self.refLines = [] #reference lineEdit widgets, packed into a list for convenience
+ self.refButtons = [] #buttons next to reference lineEdits
+ self.attachmentOffsetEdits = [] #all edit boxes related to attachmentOffset
+ self.i_active_ref = -1 #index of reference being selected (-1 means no reaction to selecting)
+ self.auto_next = False #if true, references being selected are appended ('Selecting' state is automatically advanced to next button)
+
+ self.tv = None #TempoVis class instance
+
+ self.create_transaction = True # if false, dialog doesn't mess with transactions.
+ self.callback_OK = None
+ self.callback_Cancel = None
+ self.callback_Apply = None
+
+ self._freeze()
+
+ def __init__(self, obj_to_attach,
+ take_selection = False,
+ create_transaction = True,
+ callback_OK = None,
+ callback_Cancel = None,
+ callback_Apply = None):
+
+ self.__define_attributes()
+
+ self.create_transaction = create_transaction
+ self.callback_OK = callback_OK
+ self.callback_Cancel = callback_Cancel
+ self.callback_Apply = callback_Apply
+
+ self.obj = obj_to_attach
+ if hasattr(obj_to_attach,'Attacher'):
+ self.attacher = obj_to_attach.Attacher
+ elif hasattr(obj_to_attach,'AttacherType'):
+ self.attacher = Part.AttachEngine(obj_to_attach.AttacherType)
+ else:
+ movable = True
+ if not hasattr(self.obj, "Placement"):
+ movable = False
+ if 'Hidden' in self.obj.getEditorMode("Placement") or 'ReadOnly' in self.obj.getEditorMode("Placement"):
+ movable = False
+ if not movable:
+ if self.callback_Cancel:
+ self.callback_Cancel()
+ raise ValueError(_translate('AttachmentEditor',"Object {name} is neither movable nor attachable, can't edit attachment",None)
+ .format(name= self.obj.Label))
+
+ self.obj_is_attachable = False
+ self.attacher = Part.AttachEngine()
+
+ mb = QtGui.QMessageBox()
+ mb.setIcon(mb.Icon.Warning)
+ mb.setText(_translate('AttachmentEditor',
+ "{obj} is not attachable. You can still use attachment editor dialog to align the object, but the attachment won't be parametric."
+ ,None)
+ .format(obj= obj_to_attach.Label))
+ mb.setWindowTitle(_translate('AttachmentEditor',"Attachment",None))
+ btnAbort = mb.addButton(QtGui.QMessageBox.StandardButton.Abort)
+ btnOK = mb.addButton(_translate('AttachmentEditor',"Continue",None),QtGui.QMessageBox.ButtonRole.ActionRole)
+ mb.setDefaultButton(btnOK)
+ mb.exec_()
+ if mb.clickedButton() is btnAbort:
+ if self.callback_Cancel:
+ self.callback_Cancel()
+ raise CancelError()
+
+ import os
+ self.form=uic.loadUi(os.path.dirname(__file__) + os.path.sep + 'TaskAttachmentEditor.ui')
+ self.form.setWindowIcon(QtGui.QIcon(':/icons/Part_Attachment.svg'))
+ self.form.setWindowTitle(_translate('AttachmentEditor',"Attachment",None))
+
+ self.refLines = [self.form.lineRef1,
+ self.form.lineRef2,
+ self.form.lineRef3,
+ self.form.lineRef4]
+ self.refButtons = [self.form.buttonRef1,
+ self.form.buttonRef2,
+ self.form.buttonRef3,
+ self.form.buttonRef4]
+ self.attachmentOffsetEdits = [self.form.attachmentOffsetX,
+ self.form.attachmentOffsetY,
+ self.form.attachmentOffsetZ,
+ self.form.attachmentOffsetYaw,
+ self.form.attachmentOffsetPitch,
+ self.form.attachmentOffsetRoll]
+
+ self.block = False
+
+ for i in range(len(self.refLines)):
+ QtCore.QObject.connect(self.refLines[i], QtCore.SIGNAL('textEdited(QString)'), lambda txt, i=i: self.lineRefChanged(i,txt))
+
+ for i in range(len(self.refLines)):
+ QtCore.QObject.connect(self.refButtons[i], QtCore.SIGNAL('clicked()'), lambda i=i: self.refButtonClicked(i))
+
+ for i in range(len(self.attachmentOffsetEdits)):
+ QtCore.QObject.connect(self.attachmentOffsetEdits[i], QtCore.SIGNAL('valueChanged(double)'), lambda val, i=i: self.attachmentOffsetChanged(i,val))
+
+ QtCore.QObject.connect(self.form.checkBoxFlip, QtCore.SIGNAL('clicked()'), self.checkBoxFlipClicked)
+
+ QtCore.QObject.connect(self.form.listOfModes, QtCore.SIGNAL('itemSelectionChanged()'), self.modeSelected)
+
+ if self.create_transaction:
+ self.obj.Document.openTransaction(_translate('AttachmentEditor',"Edit attachment of {feat}",None).format(feat= self.obj.Name))
+
+
+ self.readParameters()
+
+
+ if len(self.attacher.References) == 0 and take_selection:
+ sel = GetSelectionAsLinkSubList()
+ for i in range(len(sel))[::-1]:
+ if sel[i][0] is obj_to_attach:
+ sel.pop(i)
+ self.attacher.References = sel
+ # need to update textboxes
+ self.fillAllRefLines()
+
+ if len(self.attacher.References) == 0:
+ self.i_active_ref = 0
+ self.auto_next = True
+ else:
+ self.i_active_ref = -1
+ self.auto_next = False
+
+ Gui.Selection.addObserver(self)
+
+ self.updatePreview()
+ self.updateRefButtons()
+
+ self.tv = TempoVis(self.obj.Document, tag= "PartGui.TaskAttachmentEditor")
+ if self.tv: # tv will still be None if Show module is unavailable
+ self.tv.hide_all_dependent(self.obj)
+ self.tv.show(self.obj)
+ self.tv.setUnpickable(self.obj)
+ self.tv.modifyVPProperty(self.obj, "Transparency", 70)
+ self.tv.show([obj for (obj,subname) in self.attacher.References])
+
+ # task dialog handling
+ def getStandardButtons(self):
+ return int(QtGui.QDialogButtonBox.Ok) | int(QtGui.QDialogButtonBox.Cancel)| int(QtGui.QDialogButtonBox.Apply)
+
+ def clicked(self,button):
+ if button == QtGui.QDialogButtonBox.Apply:
+ if self.obj_is_attachable:
+ self.writeParameters()
+ self.updatePreview()
+ if self.callback_Apply:
+ self.callback_Apply()
+
+ def accept(self):
+ if self.obj_is_attachable:
+ self.writeParameters()
+ if self.create_transaction:
+ self.obj.Document.commitTransaction()
+ self.cleanUp()
+ Gui.Control.closeDialog()
+ if self.callback_OK:
+ self.callback_OK()
+
+ def reject(self):
+ if self.create_transaction:
+ self.obj.Document.abortTransaction()
+ self.cleanUp()
+ Gui.Control.closeDialog()
+ if self.callback_Cancel:
+ self.callback_Cancel()
+
+
+ #selectionObserver stuff
+ def addSelection(self,docname,objname,subname,pnt):
+ i = self.i_active_ref
+ if i < 0:
+ #not selecting any reference
+ return
+ if i > 0 and self.auto_next:
+ prevref = LinkFromStr( self.refLines[i-1].text(), self.obj.Document )
+ if prevref[0].Name == objname and subname == '':
+ # whole object was selected by double-clicking
+ # its subelement was already written to line[i-1], so we decrease i to overwrite the lineRefChanged
+ i -= 1
+ if i > len(self.refLines)-1:
+ # all 4 references have been selected, finish
+ assert(self.auto_next)
+ self.i_active_ref = -1
+ self.updateRefButtons()
+ return
+ if i > -1:
+ # assign the selected reference
+ if objname == self.obj.Name:
+ self.form.message.setText(_translate('AttachmentEditor',"Ignored. Can't attach object to itself!",None))
+ return
+ if App.getDocument(docname).getObject(objname) in getAllDependent(self.obj):
+ self.form.message.setText(_translate('AttachmentEditor',"{obj1} depends on object being attached, can't use it for attachment",None).format(obj1= objname))
+ return
+
+ self.refLines[i].setText( StrFromLink(App.getDocument(docname).getObject(objname), subname) )
+ self.lineRefChanged(i,'')
+ if self.auto_next:
+ i += 1
+ self.i_active_ref = i
+ self.updateRefButtons()
+
+ # slots
+
+ def attachmentOffsetChanged(self, index, value):
+ if self.block:
+ return
+ plm = self.attacher.AttachmentOffset
+ pos = plm.Base
+ if index==0:
+ pos.x = Q(self.form.attachmentOffsetX.text()).getValueAs(mm)
+ if index==1:
+ pos.y = Q(self.form.attachmentOffsetY.text()).getValueAs(mm)
+ if index==2:
+ pos.z = Q(self.form.attachmentOffsetZ.text()).getValueAs(mm)
+ if index >= 0 and index <= 2:
+ plm.Base = pos
+
+ rot = plm.Rotation;
+ (yaw, pitch, roll) = rot.toEuler()
+ if index==3:
+ yaw = Q(self.form.attachmentOffsetYaw.text()).getValueAs(deg)
+ if index==4:
+ pitch = Q(self.form.attachmentOffsetPitch.text()).getValueAs(deg)
+ if index==5:
+ roll = Q(self.form.attachmentOffsetRoll.text()).getValueAs(deg)
+ if index >= 3 and index <= 5:
+ rot = App.Rotation(yaw,pitch,roll)
+ plm.Rotation = rot
+
+ self.attacher.AttachmentOffset = plm
+
+ self.updatePreview()
+
+ def checkBoxFlipClicked(self):
+ if self.block:
+ return
+ self.attacher.Reverse = self.form.checkBoxFlip.isChecked()
+ self.updatePreview()
+
+ def lineRefChanged(self, index, value):
+ if self.block:
+ return
+ # not parsing links here, because doing it in updatePreview will display error message
+ self.updatePreview()
+
+ def refButtonClicked(self, index):
+ if self.block:
+ return
+ if self.i_active_ref == index:
+ #stop selecting
+ self.i_active_ref = -1
+ else:
+ #start selecting
+ self.i_active_ref = index
+ self.auto_next = False
+ self.updateRefButtons()
+
+ def modeSelected(self):
+ if self.block:
+ return
+ self.attacher.Mode = self.getCurrentMode()
+ self.updatePreview()
+
+ #internal methods
+ def writeParameters(self):
+ 'Transfer from the dialog to the object'
+ self.attacher.writeParametersToFeature(self.obj)
+
+ def readParameters(self):
+ 'Transfer from the object to the dialog'
+ if self.obj_is_attachable:
+ self.attacher.readParametersFromFeature(self.obj)
+
+ plm = self.attacher.AttachmentOffset
+ try:
+ old_selfblock = self.block
+ self.block = True
+ self.form.attachmentOffsetX.setText ((plm.Base.x * mm).UserString)
+ self.form.attachmentOffsetY.setText ((plm.Base.y * mm).UserString)
+ self.form.attachmentOffsetZ.setText ((plm.Base.z * mm).UserString)
+ self.form.attachmentOffsetYaw.setText ((plm.Rotation.toEuler()[0] * deg).UserString)
+ self.form.attachmentOffsetPitch.setText((plm.Rotation.toEuler()[1] * deg).UserString)
+ self.form.attachmentOffsetRoll.setText ((plm.Rotation.toEuler()[2] * deg).UserString)
+
+ self.form.checkBoxFlip.setChecked(self.attacher.Reverse)
+
+ self.fillAllRefLines()
+ finally:
+ self.block = old_selfblock
+
+ def fillAllRefLines(self):
+ old_block = self.block
+ try:
+ self.block = True
+ strings = StrListFromRefs(self.attacher.References)
+ if len(strings) < len(self.refLines):
+ strings.extend(['']*(len(self.refLines) - len(strings)))
+ for i in range(len(self.refLines)):
+ self.refLines[i].setText(strings[i])
+ finally:
+ self.block = old_block
+
+ def parseAllRefLines(self):
+ self.attacher.References = RefsFromStrList([le.text() for le in self.refLines], self.obj.Document)
+
+ def updateListOfModes(self):
+ '''needs suggestor to have been called, and assigned to self.last_sugr'''
+ try:
+ old_selfblock = self.block
+ self.block = True
+ list_widget = self.form.listOfModes
+ list_widget.clear()
+ sugr = self.last_sugr
+ # always have the option to choose Deactivated mode
+ valid_modes = ['Deactivated'] + sugr['allApplicableModes']
+
+ # add valid modes
+ for m in valid_modes:
+ item = QtGui.QListWidgetItem()
+ txt = self.attacher.getModeInfo(m)['UserFriendlyName']
+ item.setText(txt)
+ item.setData(self.KEYmode,m)
+ item.setData(self.KEYon,True)
+ if m == sugr['bestFitMode']:
+ f = item.font()
+ f.setBold(True)
+ item.setFont(f)
+ list_widget.addItem(item)
+ item.setSelected(self.attacher.Mode == m)
+ # add potential modes
+ for m in sugr['reachableModes'].keys():
+ item = QtGui.QListWidgetItem()
+ txt = self.attacher.getModeInfo(m)['UserFriendlyName']
+ listlistrefs = sugr['reachableModes'][m]
+ if len(listlistrefs) == 1:
+ listrefs_userfriendly = [self.attacher.getRefTypeInfo(t)['UserFriendlyName'] for t in listlistrefs[0]]
+ txt = _translate('AttachmentEditor',"{mode} (add {morerefs})",None).format(mode= txt,
+ morerefs= u"+".join(listrefs_userfriendly))
+ else:
+ txt = _translate('AttachmentEditor',"{mode} (add more references)",None).format(mode= txt)
+ item.setText(txt)
+ item.setData(self.KEYmode,m)
+ item.setData(self.KEYon,True)
+ if m == sugr['bestFitMode']:
+ f = item.font()
+ f.setBold(True)
+ item.setFont(f)
+
+ #disable this item
+ f = item.flags()
+ f = f & ~(QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable)
+ item.setFlags(f)
+
+ list_widget.addItem(item)
+
+ # re-scan the list to fill in tooltips
+ for item in list_widget.findItems('', QtCore.Qt.MatchContains):
+ m = item.data(self.KEYmode)
+ on = item.data(self.KEYon)
+
+ mi = self.attacher.getModeInfo(m)
+ cmb = []
+ for refstr in mi['ReferenceCombinations']:
+ refstr_userfriendly = [self.attacher.getRefTypeInfo(t)['UserFriendlyName'] for t in refstr]
+ cmb.append(u", ".join(refstr_userfriendly))
+
+ tip = mi['BriefDocu']
+ if (m != 'Deactivated'):
+ tip += u"\n\n"
+ tip += _translate('AttachmentEditor', "Reference combinations:", None) + u" \n\n".join(cmb)
+
+ item.setToolTip(tip)
+
+ finally:
+ self.block = old_selfblock
+
+
+ def updateRefButtons(self):
+ try:
+ old_selfblock = self.block
+ self.block = True
+ for i in range(len(self.refButtons)):
+ btn = self.refButtons[i]
+ btn.setCheckable(True)
+ btn.setChecked(self.i_active_ref == i)
+ typ = _translate('AttachmentEditor',"Reference{i}",None).format(i= str(i+1))
+ if self.last_sugr is not None:
+ typestr = self.last_sugr['references_Types']
+ if i < len(typestr):
+ typ = self.attacher.getRefTypeInfo(typestr[i])['UserFriendlyName']
+ btn.setText(_translate('AttachmentEditor',"Selecting...",None) if self.i_active_ref == i else typ)
+ finally:
+ self.block = old_selfblock
+
+ def getCurrentMode(self):
+ list_widget = self.form.listOfModes
+ sel = list_widget.selectedItems()
+ if len(sel) == 1:
+ if sel[0].data(self.KEYon):
+ return str(sel[0].data(self.KEYmode)) # data() returns unicode, which confuses attacher
+ # nothing selected in list. Return suggested
+ if self.last_sugr is not None:
+ if self.last_sugr['message'] == 'OK':
+ return self.last_sugr['bestFitMode']
+ # no suggested mode. Return current, so it doesn't change
+ return self.attacher.Mode
+
+ def updatePreview(self):
+ new_plm = None
+
+ try:
+ self.parseAllRefLines()
+ self.last_sugr = self.attacher.suggestModes()
+ if self.last_sugr['message'] == 'LinkBroken':
+ raise ValueError(_translate('AttachmentEditor',"Failed to resolve links. {err}",None).format(err= self.last_sugr['error']))
+
+ self.updateListOfModes()
+
+ self.attacher.Mode = self.getCurrentMode()
+
+ new_plm = self.attacher.calculateAttachedPlacement(self.obj.Placement)
+ if new_plm is None:
+ self.form.message.setText(_translate('AttachmentEditor',"Not attached",None))
+ else:
+ self.form.message.setText( _translate('AttachmentEditor',"Attached with mode {mode}",None)
+ .format( mode= self.attacher.getModeInfo(self.getCurrentMode())['UserFriendlyName'] ) )
+ if PlacementsFuzzyCompare(self.obj.Placement, new_plm) == False:
+ # assign only if placement changed. this avoids touching the object
+ # when entering and extiting dialog without changing anything
+ self.obj.Placement = new_plm
+ except Exception as err:
+ self.form.message.setText(_translate('AttachmentEditor',"Error: {err}",None).format(err= str(err)))
+
+ if new_plm is not None:
+ self.form.groupBox_AttachmentOffset.setTitle(_translate('AttachmentEditor',"Attachment Offset (in local coordinates):",None))
+ self.form.groupBox_AttachmentOffset.setEnabled(True)
+ else:
+ self.form.groupBox_AttachmentOffset.setTitle(_translate('AttachmentEditor',"Attachment Offset (inactive - not attached):",None))
+ self.form.groupBox_AttachmentOffset.setEnabled(False)
+
+ def cleanUp(self):
+ '''stuff that needs to be done when dialog is closed.'''
+ Gui.Selection.removeObserver(self)
+ if self.tv:
+ self.tv.restore()
diff --git a/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.ui b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.ui
index cdd2fedbda..5dd8174005 100644
--- a/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.ui
+++ b/src/Mod/Part/AttachmentEditor/TaskAttachmentEditor.ui
@@ -121,7 +121,7 @@
- Attachment Offset:
+ Attachment Offset (in local coordinates):
-
diff --git a/src/Mod/Part/Gui/TaskAttacher.cpp b/src/Mod/Part/Gui/TaskAttacher.cpp
index 4668f3036f..fa51c7eb9e 100644
--- a/src/Mod/Part/Gui/TaskAttacher.cpp
+++ b/src/Mod/Part/Gui/TaskAttacher.cpp
@@ -318,7 +318,7 @@ bool TaskAttacher::updatePreview()
ui->message->setStyleSheet(QString::fromLatin1("QLabel{color: green;}"));
}
}
- QString splmLabelText = attached ? tr("Attachment Offset:") : tr("Attachment Offset (inactive - not attached):");
+ QString splmLabelText = attached ? tr("Attachment Offset (in local coordinates):") : tr("Attachment Offset (inactive - not attached):");
ui->groupBox_AttachmentOffset->setTitle(splmLabelText);
ui->groupBox_AttachmentOffset->setEnabled(attached);
diff --git a/src/Mod/Part/Gui/TaskAttacher.ui b/src/Mod/Part/Gui/TaskAttacher.ui
index d1319bc270..8e677d0687 100644
--- a/src/Mod/Part/Gui/TaskAttacher.ui
+++ b/src/Mod/Part/Gui/TaskAttacher.ui
@@ -121,7 +121,7 @@
- Attachment Offset:
+ Attachment Offset (in local coordinates):
-