From 21765dec9cb713390c30ce86fbe04df7c09bec0c Mon Sep 17 00:00:00 2001 From: sliptonic Date: Wed, 15 Dec 2021 09:12:44 -0600 Subject: [PATCH] Modified combobox selection cleaned up translation method Profile translation cleanup Incorporating additional suggestions --- src/Mod/Path/PathScripts/PathOpGui.py | 84 +++++++------- src/Mod/Path/PathScripts/PathProfile.py | 129 +++++++++++---------- src/Mod/Path/PathScripts/PathProfileGui.py | 76 ++++++------ 3 files changed, 153 insertions(+), 136 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py index 2f35d6338e..79eeec2e10 100644 --- a/src/Mod/Path/PathScripts/PathOpGui.py +++ b/src/Mod/Path/PathScripts/PathOpGui.py @@ -35,6 +35,7 @@ import PathScripts.PathSetupSheet as PathSetupSheet import PathScripts.PathUtil as PathUtil import PathScripts.PathUtils as PathUtils import importlib +from PySide.QtCore import QT_TRANSLATE_NOOP from PySide import QtCore, QtGui @@ -43,16 +44,18 @@ __author__ = "sliptonic (Brad Collette)" __url__ = "https://www.freecadweb.org" __doc__ = "Base classes and framework for Path operation's UI" -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -# PathLog.trackModule(PathLog.thisModule()) +translate = FreeCAD.Qt.translate - -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) +if False: + PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) + PathLog.trackModule(PathLog.thisModule()) +else: + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) class ViewProvider(object): - """Generic view provider for path objects. + """ + Generic view provider for path objects. Deducts the icon name from operation name, brings up the TaskPanel with pages corresponding to the operation's opFeatures() and forwards property change notifications to the page controllers. @@ -79,10 +82,12 @@ class ViewProvider(object): return def deleteObjectsOnReject(self): - """deleteObjectsOnReject() ... return true if all objects should + """ + deleteObjectsOnReject() ... return true if all objects should be created if the user hits cancel. This is used during the initial edit session, if the user does not press OK, it is assumed they've - changed their mind about creating the operation.""" + changed their mind about creating the operation. + """ PathLog.track() return hasattr(self, "deleteOnReject") and self.deleteOnReject @@ -186,7 +191,7 @@ class ViewProvider(object): PathLog.track() for action in menu.actions(): menu.removeAction(action) - action = QtGui.QAction(translate("Path", "Edit"), menu) + action = QtGui.QAction(translate("PathOp", "Edit"), menu) action.triggered.connect(self.setEdit) menu.addAction(action) @@ -353,30 +358,27 @@ class TaskPanelPage(object): # pylint: disable=unused-argument pass # pylint: disable=unnecessary-pass - # helpers def selectInComboBox(self, name, combo): """selectInComboBox(name, combo) ... helper function to select a specific value in a combo box.""" - index = combo.findText(name, QtCore.Qt.MatchFixedString) - if index >= 0: - combo.blockSignals(True) - combo.setCurrentIndex(index) - combo.blockSignals(False) - - def selectInComboBoxNew(self, name, combo): - '''selectInComboBox(name, combo) ... - helper function to select a specific value in a combo box.''' - combo.blockSignals(True) - cnt = combo.count() + blocker = QtCore.QSignalBlocker(combo) index = combo.currentIndex() # Save initial index - while cnt > 0: - cnt -= 1 - combo.setCurrentIndex(cnt) - if name == combo.currentData(): - combo.blockSignals(False) - return + + # Search using currentData and return if found + newindex = combo.findData(name) + if newindex >= 0: + combo.setCurrentIndex(newindex) + return + + # if not found, search using current text + newindex = combo.findText(name, QtCore.Qt.MatchFixedString) + if newindex >= 0: + combo.setCurrentIndex(newindex) + return + + # not found, return unchanged combo.setCurrentIndex(index) - combo.blockSignals(False) + return def populateCombobox(self, form, enumTups, comboBoxesPropertyMap): """fillComboboxes(form, comboBoxesPropertyMap) ... populate comboboxes with translated enumerations @@ -558,7 +560,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): if len(selection) != 1: if not ignoreErrors: msg = translate( - "PathProject", + "PathOp", "Please select %s from a single solid" % self.featureName(), ) FreeCAD.Console.PrintError(msg + "\n") @@ -571,30 +573,28 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): and selection[0].SubObjects[0].ShapeType == "Vertex" ): if not ignoreErrors: - PathLog.error( - translate("PathProject", "Vertexes are not supported") - ) + PathLog.error(translate("PathOp", "Vertexes are not supported")) return False if ( not self.supportsEdges() and selection[0].SubObjects[0].ShapeType == "Edge" ): if not ignoreErrors: - PathLog.error(translate("PathProject", "Edges are not supported")) + PathLog.error(translate("PathOp", "Edges are not supported")) return False if ( not self.supportsFaces() and selection[0].SubObjects[0].ShapeType == "Face" ): if not ignoreErrors: - PathLog.error(translate("PathProject", "Faces are not supported")) + PathLog.error(translate("PathOp", "Faces are not supported")) return False else: if not self.supportsPanels() or "Panel" not in sel.Object.Name: if not ignoreErrors: PathLog.error( translate( - "PathProject", + "PathOp", "Please select %s of a solid" % self.featureName(), ) ) @@ -653,7 +653,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): opLabel = str(self.form.geometryImportList.currentText()) ops = FreeCAD.ActiveDocument.getObjectsByLabel(opLabel) if len(ops) > 1: - msg = translate("PathOpGui", "Mulitiple operations are labeled as") + msg = translate("PathOp", "Mulitiple operations are labeled as") msg += " {}\n".format(opLabel) FreeCAD.Console.PrintWarning(msg) (base, subList) = ops[0].Base[0] @@ -850,7 +850,7 @@ class TaskPanelHeightsPage(TaskPanelPage): ) def getTitle(self, obj): - return translate("Path", "Heights") + return translate("PathOp", "Heights") def getFields(self, obj): self.safeHeight.updateProperty() @@ -1056,7 +1056,7 @@ class TaskPanelDiametersPage(TaskPanelPage): ) def getTitle(self, obj): - return translate("Path", "Diameters") + return translate("PathOp", "Diameters") def getFields(self, obj): self.minDiameter.updateProperty() @@ -1090,7 +1090,7 @@ class TaskPanel(object): def __init__(self, obj, deleteOnReject, opPage, selectionFactory): PathLog.track(obj.Label, deleteOnReject, opPage, selectionFactory) - FreeCAD.ActiveDocument.openTransaction(translate("Path", "AreaOp Operation")) + FreeCAD.ActiveDocument.openTransaction(translate("PathOp", "AreaOp Operation")) self.obj = obj self.deleteOnReject = deleteOnReject self.featurePages = [] @@ -1220,7 +1220,7 @@ class TaskPanel(object): FreeCAD.ActiveDocument.abortTransaction() if self.deleteOnReject: FreeCAD.ActiveDocument.openTransaction( - translate("Path", "Uncreate AreaOp Operation") + translate("PathOp", "Uncreate AreaOp Operation") ) try: PathUtil.clearExpressionEngine(self.obj) @@ -1367,8 +1367,8 @@ class CommandSetStartPoint: def GetResources(self): return { "Pixmap": "Path_StartPoint", - "MenuText": QtCore.QT_TRANSLATE_NOOP("Path", "Pick Start Point"), - "ToolTip": QtCore.QT_TRANSLATE_NOOP("Path", "Pick Start Point"), + "MenuText": QT_TRANSLATE_NOOP("PathOp", "Pick Start Point"), + "ToolTip": QT_TRANSLATE_NOOP("PathOp", "Pick Start Point"), } def IsActive(self): diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py index 2b786e1079..d5afb71e21 100644 --- a/src/Mod/Path/PathScripts/PathProfile.py +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -30,8 +30,7 @@ import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils import math import numpy - -from PySide import QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader @@ -39,6 +38,7 @@ from lazy_loader.lazy_loader import LazyLoader Part = LazyLoader("Part", globals(), "Part") DraftGeomUtils = LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils") +translate = FreeCAD.Qt.translate __title__ = "Path Profile Operation" __author__ = "sliptonic (Brad Collette)" @@ -55,11 +55,6 @@ else: PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -# Qt translation handling -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - class ObjectProfile(PathAreaOp.ObjectOp): """Proxy object for Profile operations based on faces.""" @@ -79,23 +74,21 @@ class ObjectProfile(PathAreaOp.ObjectOp): """initAreaOpProperties(obj) ... create operation specific properties""" self.addNewProps = [] - for (prtyp, nm, grp, tt) in self.areaOpProperties(): - if not hasattr(obj, nm): - obj.addProperty(prtyp, nm, grp, tt) - self.addNewProps.append(nm) + for (propertytype, propertyname, grp, tt) in self.areaOpProperties(): + if not hasattr(obj, propertyname): + obj.addProperty(propertytype, propertyname, grp, tt) + self.addNewProps.append(propertyname) if len(self.addNewProps) > 0: # Set enumeration lists for enumeration properties ENUMS = self.areaOpPropertyEnumerations() for n in ENUMS: - if n in self.addNewProps: - setattr(obj, n, ENUMS[n]) + if n[0] in self.addNewProps: + setattr(obj, n[0], n[1]) if warn: - newPropMsg = translate("PathProfile", "New property added to") + newPropMsg = "New property added to" newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + ". " - newPropMsg += ( - translate("PathProfile", "Check its default value.") + "\n" - ) + newPropMsg += "Check its default value." + "\n" FreeCAD.Console.PrintWarning(newPropMsg) self.propertiesReady = True @@ -109,7 +102,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): "App::PropertyEnumeration", "Direction", "Profile", - QtCore.QT_TRANSLATE_NOOP( + QT_TRANSLATE_NOOP( "App::Property", "The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)", ), @@ -118,8 +111,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): "App::PropertyEnumeration", "HandleMultipleFeatures", "Profile", - QtCore.QT_TRANSLATE_NOOP( - "PathPocket", + QT_TRANSLATE_NOOP( + "App::Property", "Choose how to process multiple Base Geometry features.", ), ), @@ -127,7 +120,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): "App::PropertyEnumeration", "JoinType", "Profile", - QtCore.QT_TRANSLATE_NOOP( + QT_TRANSLATE_NOOP( "App::Property", "Controls how tool moves around corners. Default=Round", ), @@ -136,7 +129,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): "App::PropertyFloat", "MiterLimit", "Profile", - QtCore.QT_TRANSLATE_NOOP( + QT_TRANSLATE_NOOP( "App::Property", "Maximum distance before a miter join is truncated" ), ), @@ -144,7 +137,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): "App::PropertyDistance", "OffsetExtra", "Profile", - QtCore.QT_TRANSLATE_NOOP( + QT_TRANSLATE_NOOP( "App::Property", "Extra value to stay away from final profile- good for roughing toolpath", ), @@ -153,7 +146,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): "App::PropertyBool", "processHoles", "Profile", - QtCore.QT_TRANSLATE_NOOP( + QT_TRANSLATE_NOOP( "App::Property", "Profile holes as well as the outline" ), ), @@ -161,50 +154,78 @@ class ObjectProfile(PathAreaOp.ObjectOp): "App::PropertyBool", "processPerimeter", "Profile", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the outline"), + QT_TRANSLATE_NOOP("App::Property", "Profile the outline"), ), ( "App::PropertyBool", "processCircles", "Profile", - QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile round holes"), + QT_TRANSLATE_NOOP("App::Property", "Profile round holes"), ), ( "App::PropertyEnumeration", "Side", "Profile", - QtCore.QT_TRANSLATE_NOOP( - "App::Property", "Side of edge that tool should cut" - ), + QT_TRANSLATE_NOOP("App::Property", "Side of edge that tool should cut"), ), ( "App::PropertyBool", "UseComp", "Profile", - QtCore.QT_TRANSLATE_NOOP( + QT_TRANSLATE_NOOP( "App::Property", "Make True, if using Cutter Radius Compensation" ), ), ] - def areaOpPropertyEnumerations(self): - """areaOpPropertyEnumerations() ... returns a dictionary of enumeration lists - for the operation's enumeration type properties.""" + @classmethod + def areaOpPropertyEnumerations(self, dataType="data"): + + """opPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType. + Args: + dataType = 'data', 'raw', 'translated' + Notes: + 'data' is list of internal string literals used in code + 'raw' is list of (translated_text, data_string) tuples + 'translated' is list of translated string literals + """ + # Enumeration lists for App::PropertyEnumeration properties - return { - "Direction": ["CW", "CCW"], # this is the direction that the profile runs - "HandleMultipleFeatures": ["Collectively", "Individually"], + enums = { + "Direction": [ + (translate("PathProfile", "CW"), "CW"), + (translate("PathProfile", "CCW"), "CCW"), + ], # this is the direction that the profile runs + "HandleMultipleFeatures": [ + (translate("PathProfile", "Collectively"), "Collectively"), + (translate("PathProfile", "Individually"), "Individually"), + ], "JoinType": [ - "Round", - "Square", - "Miter", + (translate("PathProfile", "Round"), "Round"), + (translate("PathProfile", "Square"), "Square"), + (translate("PathProfile", "Miter"), "Miter"), ], # this is the direction that the Profile runs "Side": [ - "Outside", - "Inside", + (translate("PathProfile", "Outside"), "Outside"), + (translate("PathProfile", "Inside"), "Inside"), ], # side of profile that cutter is on in relation to direction of profile } + if dataType == "raw": + return enums + + data = list() + idx = 0 if dataType == "translated" else 1 + + PathLog.debug(enums) + + for k, v in enumerate(enums): + # data[k] = [tup[idx] for tup in v] + data.append((v, [tup[idx] for tup in enums[v]])) + PathLog.debug(data) + + return data + def areaOpPropertyDefaults(self, obj, job): """areaOpPropertyDefaults(obj, job) ... returns a dictionary of default values for the operation's properties.""" @@ -350,8 +371,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): shapes = [] remainingObjBaseFeatures = [] self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False - self.inaccessibleMsg = translate( - "PathProfile", + self.inaccessibleMsg = translate("PathProfile", "The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.", ) self.offsetExtra = obj.OffsetExtra.Value @@ -418,10 +438,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): else: PathLog.track() ignoreSub = base.Name + "." + sub - msg = translate( - "PathProfile", - "Found a selected object which is not a face. Ignoring:", - ) + msg = "Found a selected object which is not a face. Ignoring:" PathLog.warning(msg + " {}".format(ignoreSub)) for baseShape, wire in holes: @@ -458,9 +475,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): ) except Exception as ee: # pylint: disable=broad-except # PathUtils.getEnvelope() failed to return an object. - msg = translate( - "Path", "Unable to create path for face(s)." - ) + msg = translate("PathProfile", "Unable to create path for face(s).") PathLog.error(msg + "\n{}".format(ee)) cont = False @@ -569,10 +584,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): # Attempt open-edges profile if self.JOB.GeometryTolerance.Value == 0.0: msg = self.JOB.Label + ".GeometryTolerance = 0.0. " - msg += translate( - "PathProfile", - "Please set to an acceptable value greater than zero.", - ) + msg += "Please set to an acceptable value greater than zero." PathLog.error(msg) else: flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value) @@ -606,10 +618,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): shapes.append(tup) else: if zDiff < self.JOB.GeometryTolerance.Value: - msg = translate( - "PathProfile", - "Check edge selection and Final Depth requirements for profiling open edge(s).", - ) + msg = translate("PathProfile", "Check edge selection and Final Depth requirements for profiling open edge(s).") PathLog.error(msg) else: PathLog.error(self.inaccessibleMsg) @@ -667,9 +676,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): fdv = obj.FinalDepth.Value extLenFwd = sdv - fdv if extLenFwd <= 0.0: - msg = translate( - "PathProfile", "For open edges, verify Final Depth for this operation." - ) + msg = "For open edges, verify Final Depth for this operation." FreeCAD.Console.PrintError(msg + "\n") # return False extLenFwd = 0.1 diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py index a7f75b99e9..0a5513d61a 100644 --- a/src/Mod/Path/PathScripts/PathProfileGui.py +++ b/src/Mod/Path/PathScripts/PathProfileGui.py @@ -22,12 +22,11 @@ import FreeCAD import FreeCADGui -import PathGui as PGui # ensure Path/Gui/Resources are loaded +import PathGui as PGui # ensure Path/Gui/Resources are loaded import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui import PathScripts.PathProfile as PathProfile - -from PySide import QtCore +from PySide.QtCore import QT_TRANSLATE_NOOP __title__ = "Path Profile Operation UI" @@ -36,47 +35,48 @@ __url__ = "http://www.freecadweb.org" __doc__ = "Profile operation page controller and command implementation." -FeatureSide = 0x01 +FeatureSide = 0x01 FeatureProcessing = 0x02 -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - class TaskPanelOpPage(PathOpGui.TaskPanelPage): - '''Base class for profile operation page controllers. Two sub features are supported: - FeatureSide ... Is the Side property exposed in the UI - FeatureProcessing ... Are the processing check boxes supported by the operation - ''' + """Base class for profile operation page controllers. Two sub features are supported: + FeatureSide ... Is the Side property exposed in the UI + FeatureProcessing ... Are the processing check boxes supported by the operation + """ def initPage(self, obj): self.setTitle("Profile - " + obj.Label) self.updateVisibility() def profileFeatures(self): - '''profileFeatures() ... return which of the optional profile features are supported. + """profileFeatures() ... return which of the optional profile features are supported. Currently two features are supported and returned: FeatureSide ... Is the Side property exposed in the UI FeatureProcessing ... Are the processing check boxes supported by the operation - .''' + .""" return FeatureSide | FeatureProcessing def getForm(self): - '''getForm() ... returns UI customized according to profileFeatures()''' + """getForm() ... returns UI customized according to profileFeatures()""" form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpProfileFullEdit.ui") + + comboToPropertyMap = [("cutSide", "Side"), ("direction", "Direction")] + enumTups = PathProfile.ObjectProfile.areaOpPropertyEnumerations(dataType="raw") + + self.populateCombobox(form, enumTups, comboToPropertyMap) return form def getFields(self, obj): - '''getFields(obj) ... transfers values from UI to obj's proprties''' + """getFields(obj) ... transfers values from UI to obj's proprties""" self.updateToolController(obj, self.form.toolController) self.updateCoolant(obj, self.form.coolantController) - if obj.Side != str(self.form.cutSide.currentText()): - obj.Side = str(self.form.cutSide.currentText()) - if obj.Direction != str(self.form.direction.currentText()): - obj.Direction = str(self.form.direction.currentText()) - PathGui.updateInputField(obj, 'OffsetExtra', self.form.extraOffset) + if obj.Side != str(self.form.cutSide.currentData()): + obj.Side = str(self.form.cutSide.currentData()) + if obj.Direction != str(self.form.direction.currentData()): + obj.Direction = str(self.form.direction.currentData()) + PathGui.updateInputField(obj, "OffsetExtra", self.form.extraOffset) if obj.UseComp != self.form.useCompensation.isChecked(): obj.UseComp = self.form.useCompensation.isChecked() @@ -91,13 +91,17 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): obj.processCircles = self.form.processCircles.isChecked() def setFields(self, obj): - '''setFields(obj) ... transfers obj's property values to UI''' + """setFields(obj) ... transfers obj's property values to UI""" self.setupToolController(obj, self.form.toolController) self.setupCoolant(obj, self.form.coolantController) self.selectInComboBox(obj.Side, self.form.cutSide) self.selectInComboBox(obj.Direction, self.form.direction) - self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) + self.form.extraOffset.setText( + FreeCAD.Units.Quantity( + obj.OffsetExtra.Value, FreeCAD.Units.Length + ).UserString + ) self.form.useCompensation.setChecked(obj.UseComp) self.form.useStartPoint.setChecked(obj.UseStartPoint) @@ -108,7 +112,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.updateVisibility() def getSignalsForUpdate(self, obj): - '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' + """getSignalsForUpdate(obj) ... return list of signals for updating obj""" signals = [] signals.append(self.form.toolController.currentIndexChanged) signals.append(self.form.coolantController.currentIndexChanged) @@ -127,13 +131,13 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): hasFace = False objBase = list() - if hasattr(self.obj, 'Base'): + if hasattr(self.obj, "Base"): objBase = self.obj.Base if objBase.__len__() > 0: for (base, subsList) in objBase: for sub in subsList: - if sub[:4] == 'Face': + if sub[:4] == "Face": hasFace = True break @@ -148,15 +152,21 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): def registerSignalHandlers(self, obj): self.form.useCompensation.stateChanged.connect(self.updateVisibility) + + # Eclass -Command = PathOpGui.SetupOperation('Profile', - PathProfile.Create, - TaskPanelOpPage, - 'Path_Contour', - QtCore.QT_TRANSLATE_NOOP("Path_Profile", "Profile"), - QtCore.QT_TRANSLATE_NOOP("Path_Profile", "Profile entire model, selected face(s) or selected edge(s)"), - PathProfile.SetupProperties) +Command = PathOpGui.SetupOperation( + "Profile", + PathProfile.Create, + TaskPanelOpPage, + "Path_Contour", + QT_TRANSLATE_NOOP("Path", "Profile"), + QT_TRANSLATE_NOOP( + "Path", "Profile entire model, selected face(s) or selected edge(s)" + ), + PathProfile.SetupProperties, +) FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n")