diff --git a/src/Mod/Path/Gui/Resources/panels/JobEdit.ui b/src/Mod/Path/Gui/Resources/panels/JobEdit.ui index 76180f9cb..1b1c7921d 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,16 +237,6 @@ - - - - - 0 - 0 - - - - @@ -260,6 +250,64 @@ + + + + + 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> + + + + + + + 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 17e82cab4..16efd86c1 100644 --- a/src/Mod/Path/Gui/Resources/preferences/PathJob.ui +++ b/src/Mod/Path/Gui/Resources/preferences/PathJob.ui @@ -22,32 +22,115 @@ - - General Path settings + + + 0 + 0 + - - - - - - - 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 - - - - + + Output File + + + + + + + + + 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 f620d22f2..9894b2201 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -31,6 +31,10 @@ class PathWorkbench (Workbench): 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 @@ class PathWorkbench (Workbench): 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 @@ class PathWorkbench (Workbench): # "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 881aae939..dc1ddb361 100644 --- a/src/Mod/Path/PathScripts/PathJob.py +++ b/src/Mod/Path/PathScripts/PathJob.py @@ -28,6 +28,7 @@ from PySide import QtCore, QtGui import os import glob from PathScripts.PathPostProcessor import PostProcessor +from PathScripts.PathPost import CommandPathPost as PathPost import Draft @@ -47,12 +48,24 @@ except AttributeError: 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 @@ class TaskPanel: 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 @@ class TaskPanel: 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 @@ class TaskPanel: # 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 67afad23b..cae311c93 100644 --- a/src/Mod/Path/PathScripts/PathPost.py +++ b/src/Mod/Path/PathScripts/PathPost.py @@ -42,6 +42,85 @@ except AttributeError: 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 @@ class CommandPathPost: # default to the dumper post and default .tap file postname = "dumper" - filename = "tmp.tap" postArgs = "" print "in activated %s" %(obj) @@ -73,7 +151,7 @@ class CommandPathPost: # 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 @@ class CommandPathPost: 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 d9581de29..2cba8164c 100644 --- a/src/Mod/Path/PathScripts/PathPreferencesPathJob.py +++ b/src/Mod/Path/PathScripts/PathPreferencesPathJob.py @@ -26,6 +26,7 @@ import FreeCAD 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 @@ class Page: 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 @@ class Page: 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 @@ class Page: 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)