From e1a5b9a9e18254806e3218b5f03a2c1daa0ac03f Mon Sep 17 00:00:00 2001 From: ml Date: Sun, 23 Oct 2016 22:23:38 -0700 Subject: [PATCH] Added suppor for default OutputFile with templates and a policy on how to deal with file name conflicts. --- src/Mod/Path/Gui/Resources/panels/JobEdit.ui | 68 ++++++-- .../Path/Gui/Resources/preferences/PathJob.ui | 165 +++++++++++++----- src/Mod/Path/InitGui.py | 8 +- src/Mod/Path/PathScripts/PathJob.py | 36 +++- src/Mod/Path/PathScripts/PathPost.py | 94 +++++++++- .../PathScripts/PathPreferencesPathJob.py | 28 ++- 6 files changed, 321 insertions(+), 78 deletions(-) diff --git a/src/Mod/Path/Gui/Resources/panels/JobEdit.ui b/src/Mod/Path/Gui/Resources/panels/JobEdit.ui index 76180f9cbf5d..1b1c7921d432 100644 --- a/src/Mod/Path/Gui/Resources/panels/JobEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/JobEdit.ui @@ -42,8 +42,8 @@ 0 0 - 363 - 443 + 378 + 409 @@ -146,7 +146,7 @@ 0 0 378 - 391 + 409 @@ -216,7 +216,7 @@ 0 0 378 - 391 + 409 @@ -229,7 +229,7 @@ - + @@ -237,6 +237,19 @@ + + + + + 0 + 0 + + + + ... + + + @@ -245,19 +258,54 @@ 0 + + <html><head/><body><p>Enter a path and optionally file name (see below) to be used as the default for the post processor export.</p><p>The following substitutions are performed before the name is resolved at the time of the post processing:</p><p>%D ... directory of the active document<br/>%d ... name of the active document (with extension)<br/>%M ... user macro directory<br/>%j ... name of the active Job object</p><p>The following example store all files with the same name as the document the directory /home/freecad (please remove quotes):</p><p>&quot;/home/cnc/%d.g-code&quot;</p><p>See the file save policy below on how to deal with name conflicts.</p></body></html> + - - + + + + File Save Policy + + + + + - + 0 0 - - ... + + <html><head/><body><p>Choose how to deal with potential file name conflicts. Always open a dialog, only open a dialog if the output file already exists, overwrite any existing file or add a unique (3 digit) sequential ID to the file name.</p></body></html> + + + Use default + + + + + Open File Dialog + + + + + Open File Dialog on conflict + + + + + Overwrite existing file + + + + + Append Unique ID on conflict + + diff --git a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui index 17e82cab4687..16efd86c1abf 100644 --- a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui +++ b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui @@ -22,32 +22,115 @@ + + + 0 + 0 + + - General Path settings + Output File - - - - - - - If this option is enabled, new paths will automatically be placed in the active project, which will be created if necessary. - - - Automatic project handling - - - true - - - pathAutoProject - - - Mod/Path - - - - + + + + + + + + Default Path + + + + + + + + + + + 0 + 0 + + + + + + + <html><head/><body><p>Enter a path and optionally file name (see below) to be used as the default for the post processor export.</p><p>The following substitutions are performed before the name is resolved at the time of the post processing:</p><p>%D ... directory of the active document<br/>%d ... name of the active document (with extension)<br/>%M ... user macro directory<br/>%j ... name of the active Job object</p><p>The following example store all files with the same name as the document the directory /home/freecad (please remove quotes):</p><p>&quot;/home/cnc/%d.g-code&quot;</p><p>See the file save policy below on how to deal with name conflicts.</p></body></html> + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Open a file system browser to select default path for output files.</p></body></html> + + + ... + + + + + + + + + + + + + File Save Policy + + + + + + + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Choose how to deal with potential file name conflicts. Always open a dialog, only open a dialog if the output file already exists, overwrite any existing file or add a unique (3 digit) sequential ID to the file name.</p></body></html> + + + + Open File Dialog + + + + + Open File Dialog on conflict + + + + + Overwrite existing file + + + + + Append Unique ID on conflict + + + + + + @@ -61,6 +144,23 @@ QFormLayout::AllNonFixedFieldsGrow + + + + Post Processors Selection + + + + + + + true + + + <html><head/><body><p>It doesn't seem there are any post processor scripts installed. Pleas add some into your macro directory and make sure the file name ends with &quot;_post.py&quot;.</p></body></html> + + + @@ -101,23 +201,6 @@ - - - - Enabled Post Processors - - - - - - - true - - - <html><head/><body><p>It doesn't seem there are any post processor scripts installed. Pleas add some into your macro directory and make sure the file name ends with &quot;_post.py&quot;.</p></body></html> - - - diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index f620d22f2acc..9894b2201709 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -31,6 +31,10 @@ def __init__(self): self.__class__.ToolTip = "Path workbench" def Initialize(self): + # Add preferences pages - before loading PathGui to properly order pages of Path group + from PathScripts import PathPreferencesPathJob + FreeCADGui.addPreferencePage(PathPreferencesPathJob.Page, "Path") + # load the builtin modules import Path import PathGui @@ -70,7 +74,6 @@ def Initialize(self): from PathScripts import PathContour from PathScripts import PathProfileEdges from PathScripts import DogboneDressup - from PathScripts import PathPreferencesPathJob import PathCommands # build commands list @@ -115,9 +118,6 @@ def translate(context, text): # "Path", "Remote Operations")], remotecmdlist) self.appendMenu([translate("Path", "&Path")], extracmdlist) - # Add preferences pages - FreeCADGui.addPreferencePage(PathPreferencesPathJob.Page, "Path") - Log('Loading Path workbench... done\n') def GetClassName(self): diff --git a/src/Mod/Path/PathScripts/PathJob.py b/src/Mod/Path/PathScripts/PathJob.py index 881aae939c2b..dc1ddb361431 100644 --- a/src/Mod/Path/PathScripts/PathJob.py +++ b/src/Mod/Path/PathScripts/PathJob.py @@ -28,6 +28,7 @@ import os import glob from PathScripts.PathPostProcessor import PostProcessor +from PathScripts.PathPost import CommandPathPost as PathPost import Draft @@ -47,12 +48,24 @@ def translate(context, text, disambig=None): def translate(context, text, disambig=None): return QtGui.QApplication.translate(context, text, disambig) +class OutputPolicy: + Default = 'Use default' + Dialog = 'Open File Dialog' + DialogOnConflict = 'Open File Dialog on conflict' + Overwrite = 'Overwrite existing file' + AppendID = 'Append Unique ID on conflict' + All = [Default, Dialog, DialogOnConflict, Overwrite, AppendID] + class ObjectPathJob: def __init__(self, obj): # obj.addProperty("App::PropertyFile", "PostProcessor", "CodeOutput", "Select the Post Processor file for this project") obj.addProperty("App::PropertyFile", "OutputFile", "CodeOutput", QtCore.QT_TRANSLATE_NOOP("App::Property","The NC output file for this project")) + obj.OutputFile = PathPost.defaultOutputFile() obj.setEditorMode("OutputFile", 0) # set to default mode + obj.addProperty("App::PropertyEnumeration", "OutputPolicy", "CodeOutput", QtCore.QT_TRANSLATE_NOOP("App::Property","The policy on how to save output files and resolve name conflicts")) + obj.OutputPolicy = OutputPolicy.All + obj.OutputPolicy = OutputPolicy.Default obj.addProperty("App::PropertyString", "Description", "Path", QtCore.QT_TRANSLATE_NOOP("App::Property","An optional description for this job")) obj.addProperty("App::PropertyEnumeration", "PostProcessor", "Output", QtCore.QT_TRANSLATE_NOOP("App::Property","Select the Post Processor")) @@ -270,6 +283,8 @@ def getFields(self): self.obj.Label = str(self.form.leLabel.text()) if hasattr(self.obj, "OutputFile"): self.obj.OutputFile = str(self.form.leOutputFile.text()) + if hasattr(self.obj, "OutputPolicy"): + self.obj.OutputPolicy = str(self.form.cboOutputPolicy.currentText()) oldlist = self.obj.Group newlist = [] @@ -291,21 +306,23 @@ def getFields(self): self.obj.Proxy.execute(self.obj) + def selectComboBoxText(self, widget, text): + index = widget.findText(text, QtCore.Qt.MatchFixedString) + if index >= 0: + widget.blockSignals(True) + widget.setCurrentIndex(index) + widget.blockSignals(False) + def setFields(self): '''sets fields in the form to match the object''' self.form.leLabel.setText(self.obj.Label) self.form.leOutputFile.setText(self.obj.OutputFile) + self.selectComboBoxText(self.form.cboOutputPolicy, self.obj.OutputPolicy) - postindex = self.form.cboPostProcessor.findText( - self.obj.PostProcessor, QtCore.Qt.MatchFixedString) - if postindex >= 0: - self.form.cboPostProcessor.blockSignals(True) - self.form.cboPostProcessor.setCurrentIndex(postindex) - self.form.cboPostProcessor.blockSignals(False) - # make sure the proxy loads post processor script values and settings - self.obj.Proxy.onChanged(self.obj, "PostProcessor") - self.updateTooltips() + self.selectComboBoxText(self.form.cboPostProcessor, self.obj.PostProcessor) + self.obj.Proxy.onChanged(self.obj, "PostProcessor") + self.updateTooltips() for child in self.obj.Group: self.form.PathsList.addItem(child.Name) @@ -335,6 +352,7 @@ def setupUi(self): # Connect Signals and Slots self.form.cboPostProcessor.currentIndexChanged.connect(self.getFields) self.form.leOutputFile.editingFinished.connect(self.getFields) + self.form.cboOutputPolicy.currentIndexChanged.connect(self.getFields) self.form.leLabel.editingFinished.connect(self.getFields) self.form.btnSelectFile.clicked.connect(self.setFile) self.form.PathsList.indexesMoved.connect(self.getFields) diff --git a/src/Mod/Path/PathScripts/PathPost.py b/src/Mod/Path/PathScripts/PathPost.py index 67afad23b0bd..cae311c93214 100644 --- a/src/Mod/Path/PathScripts/PathPost.py +++ b/src/Mod/Path/PathScripts/PathPost.py @@ -42,6 +42,85 @@ def translate(context, text, disambig=None): class CommandPathPost: + DefaultOutputFile = "DefaultOutputFile" + DefaultOutputPolicy = "DefaultOutputPolicy" + + @classmethod + def saveDefaults(cls, path, policy): + preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") + preferences.SetString(cls.DefaultOutputFile, path) + preferences.SetString(cls.DefaultOutputPolicy, policy) + + @classmethod + def defaultOutputFile(cls): + preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") + return preferences.GetString(cls.DefaultOutputFile, "") + + @classmethod + def defaultOutputPolicy(cls): + preferences = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Path") + return preferences.GetString(cls.DefaultOutputPolicy, "") + + def resolveFileName(self, job): + path = "tmp.tap" + if job.OutputFile: + path = job.OutputFile + filename = path + if '%D' in filename: + D = FreeCAD.ActiveDocument.FileName + if D: + D = os.path.dirname(D) + else: + FreeCAD.Console.PrintError("Please save document in order to resolve output path!\n") + return None + filename = filename.replace('%D', D) + + if '%d' in filename: + d = FreeCAD.ActiveDocument.Label + filename = filename.replace('%d', d) + + if '%j' in filename: + j = job.Label + filename = filename.replace('%j', j) + + if '%M' in filename: + pref = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Macro") + M = pref.GetString("MacroPath", FreeCAD.getUserAppDataDir()) + filename = filename.replace('%M', M) + + policy = job.OutputPolicy + if not policy or policy == 'Use default': + policy = self.defaultOutputPolicy() + + openDialog = policy == 'Open File Dialog' + if os.path.isdir(filename) or not os.path.isdir(os.path.dirname(filename)): + # Either the entire filename resolves into a directory or the parent directory doesn't exist. + # Either way I don't know what to do - ask for help + openDialog = True + + if os.path.isfile(filename) and not openDialog: + if policy == 'Open File Dialog on conflict': + openDialog = True + elif policy == 'Append Unique ID on conflict': + fn, ext = os.path.splitext(filename) + nr = fn[-3:] + n = 1 + if nr.isdigit(): + n = int(nr) + while os.path.isfile("%s%03d%s" % (fn, n, ext)): + n = n + 1 + filename = "%s%03d%s" % (fn, n, ext) + + if openDialog: + foo = QtGui.QFileDialog.getSaveFileName(QtGui.qApp.activeWindow(), "Output File", filename) + if foo: + filename = foo[0] + else: + filename = None + + #print("resolveFileName(%s, %s) -> '%s'" % (path, policy, filename)) + return filename + def GetResources(self): return {'Pixmap': 'Path-Post', 'MenuText': QtCore.QT_TRANSLATE_NOOP("Path_Post", "Post Process"), @@ -64,7 +143,6 @@ def Activated(self): # default to the dumper post and default .tap file postname = "dumper" - filename = "tmp.tap" postArgs = "" print "in activated %s" %(obj) @@ -73,7 +151,7 @@ def Activated(self): # output filename if hasattr(obj[0], "Group") and hasattr(obj[0], "Path"): # # Check for a selected post post processor if it's set - proj = obj[0] + job = obj[0] if hasattr(obj[0], "PostProcessor"): postobj = obj[0] @@ -85,15 +163,17 @@ def Activated(self): lessextn = os.path.splitext(postobj.PostProcessor)[0] postname = os.path.split(lessextn)[1] - if proj.OutputFile: - filename = proj.OutputFile if hasattr(postobj, "PostProcessorArgs"): postArgs = postobj.PostProcessorArgs - processor = PostProcessor.load(postname) - processor.export(obj, filename, postArgs) + filename = self.resolveFileName(job) + if filename: + processor = PostProcessor.load(postname) + processor.export(obj, filename, postArgs) - FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.commitTransaction() + else: + FreeCAD.ActiveDocument.abortTransaction() FreeCAD.ActiveDocument.recompute() if FreeCAD.GuiUp: diff --git a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py index d9581de29b8d..2cba8164c625 100644 --- a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py +++ b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py @@ -26,6 +26,7 @@ import FreeCADGui from PySide import QtCore, QtGui from PathScripts.PathPostProcessor import PostProcessor +from PathScripts.PathPost import CommandPathPost as PathPost class Page: @@ -46,6 +47,17 @@ def saveSettings(self): blacklist.append(item.text()) PostProcessor.saveDefaults(processor, args, blacklist) + path = str(self.form.leOutputFile.text()) + policy = str(self.form.cboOutputPolicy.currentText()) + PathPost.saveDefaults(path, policy) + + def selectComboEntry(self, widget, text): + index = widget.findText(text, QtCore.Qt.MatchFixedString) + if index >= 0: + widget.blockSignals(True) + widget.setCurrentIndex(index) + widget.blockSignals(False) + def loadSettings(self): self.form.defaultPostProcessor.addItem("") blacklist = PostProcessor.blacklist() @@ -58,18 +70,15 @@ def loadSettings(self): item.setCheckState(QtCore.Qt.CheckState.Checked) item.setFlags( QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable) self.form.postProcessorList.addItem(item) - - postindex = self.form.defaultPostProcessor.findText(PostProcessor.default(), QtCore.Qt.MatchFixedString) - - if postindex >= 0: - self.form.defaultPostProcessor.blockSignals(True) - self.form.defaultPostProcessor.setCurrentIndex(postindex) - self.form.defaultPostProcessor.blockSignals(False) + self.selectComboEntry(self.form.defaultPostProcessor, PostProcessor.default()) self.form.defaultPostProcessorArgs.setText(PostProcessor.defaultArgs()) + self.form.leOutputFile.setText(PathPost.defaultOutputFile()) + self.selectComboEntry(self.form.cboOutputPolicy, PathPost.defaultOutputPolicy()) self.form.postProcessorList.itemEntered.connect(self.setProcessorListTooltip) self.form.defaultPostProcessor.currentIndexChanged.connect(self.updateDefaultPostProcessorToolTip) + self.form.browseFileSystem.clicked.connect(self.browseFileSystem) def getPostProcessor(self, name): if not name in self.processor.keys(): @@ -100,3 +109,8 @@ def updateDefaultPostProcessorToolTip(self): else: self.form.defaultPostProcessor.setToolTip(self.postProcessorDefaultTooltip) self.form.defaultPostProcessorArgs.setToolTip(self.postProcessorArgsDefaultTooltip) + + def browseFileSystem(self): + foo = QtGui.QFileDialog.getExistingDirectory(QtGui.qApp.activeWindow(), "Path - Output File/Directory", self.form.defaultOutputPath.text()) + if foo: + self.form.defaultOutputPath.setText(foo)