Macro Python Assistant Window

One of Python's powerful aspects is the Python Console which serves both as an output device and a dynamic interpreter of source code. The Python Assistant Window (subsequently referred to as 'PAW') provides additional functionality to the Python Console.



Description
As a modern development environment, Python has a lot of advantages over older languages and their development environments. One large benefit is the Python Console where code can be interactively entered and the results received. Those results can then be copied and used to either build up Python source code (in a source editor) or pasted back into the Python Console in an altered form to receive further output. This is a very powerful method of developing code.

As powerful as it may be, two readily apparent shortcomings with the Python Console are:
 * the console is of limited size and the results of your work from 20 minutes ago may be scrolled far off the screen, they are not lost but they are awkward to retrieve
 * the console is cleared each time you quite your FreeCAD session, the console is empty (aside from the Python startup message) next time you start FreeCAD

The PAW provides the following:
 * it is persistent between FreeCAD sessions, things will not "disappear" from it unless the user removes them
 * it has a contextual menu that allows the following:
 * the standard editing operands: Copy & Paste & Select All
 * Copy a selection to the Python Console
 * Copy the complete contents of the PAW to the Python Console
 * Append the contents of the Python Console to the PAW
 * insert textual markers that facilitate management of the text
 * selection between any two consecutive markers
 * remove the prefix ">>> " which the Python Console uses to denote output
 * reduce multiple blank lines to single blank lines
 * personalise the working environment by managing the PAW (as well as the main FreeCAD window) by a slider-based GUI

Installation
All the code for pythonAssistantWindow.FCMacro is in one macro. So installation is comprised of copying the code to the appropriate Macro directory and invoking pythonAssistantWindow from the Macro menu, the Python console or a toolbar button (the preferred method).
 * see How to install macros for information on how to install this macro code
 * see Customize Toolbars for information how to install as a button on a toolbar

Note: A global variable within FreeCAD is used to coordinate the persistent storage.

Note: A text file in the "UserAppData" directory is used to store the textual contents of the PAW between FreeCAD sessions.

Usage
The PAW is best used as a button on a toolbar. It can be run from the Macro Menu or the code pasted onto the Python Console but the latter two options really detract from it's ease of use.

When FreeCAD is started there will be no sign of the PAW, other than a button on a toolbar. Clicking the button will cause:
 * the PAW to open in the lower right corner
 * the default settings are for the about 1/3 of the screen width to be dedicated to the PAW with the remainder being used by the main FreeCAD window, the height of the PAW will be about 1/3 of the window height
 * the contents of the PAW when it last ran will appear in the window - there should be no difference from the last time it was used
 * if the PAW has not run before then the contents will be empty
 * if the PAW is already open but hidden by other windows then it will be raised to the top so that it becomes visible
 * closing the PAW will cause the contents to be written to file and the window will close - there is no Dialog asking whether to save or not
 * however there is a Dialog asking whether to save if FreeCAD is quit (Menu->Quit FreeCAD) with unsaved changes in thePAW

Most of the functionality for the PAW is provided by the contextual menu, the options are: - provide the standard Copy function - the current selection is copied to the end of the Python Console - the complete contents of the PAW is copied to the end of the Python Console - provide the standard Paste function - the copies of the Python Console are placed at the end of the PAW - note that the contents of the Python Console may be a mixture of Python Code, output from Python Code, error message text, output from any part of FreeCAD - markers are used to divide up the text of the PAW into sections, once the contents are in sections then a section can be selected individually and worked with (e.g. Copy, Copy to the Python Console, Delete). The intention of Markers is to allow separate and unrelated sequences of Python statement to exist in the PAW, and then be managed and worked with individually. - provide the standard Select All function - provide the standard Clear function where all the text in the PAW is deleted - insert a textual Marker at the current cursor location - after Python Console output is copied to the PAW, any lines which were output from executed Python commands will be prefixed with ">>> ", this option removed those prefixes so the output can be used as context free data - compacts the text by removing multiple blank lines - brings up a modal window with three controls: - the contents of the PAW window are written out to a user selected file - the contents of the PAW are not altered - the PAW window is closed and the contents written out to the persistent storage file Note: there is no Dialog asking about saving, it is done automatically
 * Copy
 * Copy selection to console
 * Copy contents to console
 * Paste
 * Append contents of Python Console to PAW
 * Select between Markers
 * Select all
 * Clearbr
 * Insert marker
 * Remove ">>> "
 * Reduce multiple blank lines to single blank lines
 * Alter GUI settings
 * PythonAssistantWindowGui2.jpg
 * a slider to set the percentage of the screen width dedicated to the PAW (remembering that there is a certain width which the FreeCAD main window will not go below)
 * a slider to set the percentage of the height of the screen dedicated to the PAW
 * a pair of radio buttons to indicate whether the PAW should be placed at the top or bottom of the left hand side of the screen
 * Save as file
 * Close window

User Interface
The user interface is a simple text editing window, there is one button to start the PAW and a contextual menu of options to perform on the text in the text editing window. The options are described in the Usage section.

Examples
A First Example

In January your co-worker needs some help with the Python code to update a file. You write and debug it on your computer and send him the source code. You have come up with 3 different ways to do the job and are not sure which is the best to keep. You copy all 3 finished versions to the PAW and separate them by Markers.

As the month of May starts, you are happily working on the bottle project using FreeCAD. There is some problem getting the exact Python code to generate what is required so you model this graphically and the equivalent Python code is generated on the Python Console. Using PAW you copy the contents of the Python Console to the PAW. You shape it by moving it back and running, copying the results, modifying them in PAW and copying them back until you are happy.

The next Monday your boss rushed over to say that there is a big requirement for a CAD operator at the plant where they are having problems with the folding sequence for the new packaging stream. You will fly there the same day and be there for 2 weeks. You complete your assignment and return to your normal office space.

When you get back it is obvious that people have been using your area for a lunch area so you have to clean up a bit. Upon starting FreeCAD and clicking the PAW button on the toolbar, your work from before is sitting there as if it is from the day before. You realise that the solution to your bottle design lies in the file code you wrote back in January along with what you left 2 weeks earlier. Quickly you can select and copy the code segments to the Python Console to execute and fine tune them.

Once the code is stable then you can save it to a file for either e Python file or a FreeCAD macro file.

Another Example

You are trying to find out what is going wrong with the Rotation values of various objects in an Assembly. Out of 27 objects you can't see any pattern as to which objects will be affected. So you put a few lines of Python together to isolate the incorrect objects, paste it into the PAW. Then take a couple of lines of Python to get the Label and Rotation values, paste that code to the PAW as well. Copy the code to detail all the objects you need to see listed and repeat it for each object - all in the PAW. The Python Console will have all these results (including any typing errors and error messages) but it will have scrolled off the visible portion of the window long ago. So now you have a concise set of Python lines, you copy it from the PAW, paste it into the Python Console and there is the exact list of the objects which need debugging - along with their specific values. If needed you could then paste the results from the Python Console back into the PAW - prefix it with the Python comment character ('#') if you want to save it in the Python code and keep it executable. Finally you can save this conglomeration to a file from the PAW so it is secure on disk. Ready for the next time you need to steal or adapt the logic in the code.

Options
About the only option for the PAW is the ability to alter it's initial display size and shape in accordance with the size and shape fo the main FreeCAD window. There are 3 constant values in the Python code which initial size and placement of the PAW.

Remarks
There is a very simple proof of concept for a persistent storage work in this code. It may be of use to anyone else requiring such a capacity.

Links
none (so far)

Script
Macro_Python_Assistant_Window.FCMacro

Column: {}".format(		#	self.text_editor.textCursor.blockNumber,		#	self.text_editor.textCursor.columnNumber))		#print self.text_editor.textCursor.position+self.text_editor.textCursor.columnNumber		pass	def onSaveAsFile(self):		filePath = QtGui.QFileDialog.getSaveFileName(parent=None,caption="Save contents as",dir=expanduser("~"),filter="*.txt")		file = open(filePath[0],"w")		file.write(self.text_editor.toPlainText)		file.close	def closeEvent(self,event):		# write out contents for next session		file = open(persistenceFile,"w")		file.write(self.text_editor.toPlainText)		file.close		# clear global flag		del FreeCAD.PythonAssistantWindowStatus		self.close

class GetGuiConfigParams(QtGui.QMainWindow): """"""	def __init__(self, parentWindow): self.parentWindow = parentWindow super(GetGuiConfigParams, self).__init__ self.initUI(parentWindow) def initUI(self, parentWindow): """Constructor""" self.result							= userCancelled # grab geometry from our parent so we can tell if user has changed values self.initialParentWindowX			= self.parentWindow.geometry.x		self.initialParentWindowY			= self.parentWindow.geometry.y		self.initialParentWindowH			= self.parentWindow.geometry.height self.initialParentWindowW			= self.parentWindow.geometry.width self.initialHeightSliderSetting		= self.initialParentWindowH/float(availableHeight)*100 self.initialWidthSliderSetting		= self.initialParentWindowW/float(availableWidth-interWindowGap)*100 # set some fixed GUI attributes width								= 450 height								= 40 buttonWidth							= 80 sliderWidth							= 100 self.setWindowTitle("GUI Configuration") self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) self.resize(width, height) self.widthSlider					= self.initialWidthSliderSetting self.heightSlider					= self.initialHeightSliderSetting #		centralWidget 						= QtGui.QWidget(self) layout 								= QtGui.QGridLayout centralWidget.setLayout(layout) verticalLine 						= QtGui.QFrame # sliders widthSlider = QtGui.QSlider(QtCore.Qt.Horizontal, self) widthSlider.setFocusPolicy(QtCore.Qt.NoFocus) widthSlider.valueChanged[int].connect(self.widthSliderChangeValue) widthSlider.setFixedWidth(sliderWidth) widthSlider.setValue(self.initialWidthSliderSetting) heightSlider = QtGui.QSlider(QtCore.Qt.Horizontal, self) heightSlider.setFocusPolicy(QtCore.Qt.NoFocus) heightSlider.valueChanged[int].connect(self.heightSliderChangeValue) heightSlider.setFixedWidth(sliderWidth) heightSlider.setValue(self.initialHeightSliderSetting) # labels pawWidthLbl		= QtGui.QLabel("Python Assistant Window width", self) pawHeightLbl	= QtGui.QLabel("Python Assistant Window height", self) # radio buttons - window top or bottom self.rb1 = QtGui.QRadioButton("Window at Top",self) self.rb1.clicked.connect(self.onRb1) self.rb2 = QtGui.QRadioButton("Window at Bottom",self) self.rb2.toggle # set default value self.rb2.clicked.connect(self.onRb2) if self.parentWindow.geometry.y==0: self.rb1.toggle # cancel button cancelButton = QtGui.QPushButton('Cancel', self) cancelButton.clicked.connect(self.onCancel) cancelButton.setFixedWidth(buttonWidth) # OK button okButton = QtGui.QPushButton('OK', self) okButton.clicked.connect(self.onOk) okButton.setFixedWidth(buttonWidth) #		verticalLine.setFrameStyle(QtGui.QFrame.VLine) verticalLine.setSizePolicy(QtGui.QSizePolicy.Minimum,QtGui.QSizePolicy.Expanding) #		pawWidthLbl.setSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding) pawHeightLbl.setSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding) self.rb1.setSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding) self.rb2.setSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding) # populate layout layout.addWidget(widthSlider,0,0) layout.addWidget(pawWidthLbl,0,1) layout.addWidget(heightSlider,2,0) layout.addWidget(pawHeightLbl,2,1) layout.addWidget(verticalLine,0,2,3,1) layout.addWidget(self.rb1,0,3) layout.addWidget(self.rb2,2,3) layout.addWidget(cancelButton,3,1,QtCore.Qt.AlignRight) layout.addWidget(okButton,3,3) #		self.setCentralWidget(centralWidget) #		self.show def widthSliderChangeValue(self, value): self.widthSliderValue = value def heightSliderChangeValue(self, value): self.heightSliderValue = value def onRb1(self): pass def onRb2(self): pass def onCancel(self): self.result = userCancelled self.close def onOk(self): self.result = "OK" # the two slider values are the width and height of the Python Assistant Window # resize main FreeCAD window freeCadMainWidth	= ((1-(self.widthSliderValue/100.0)) * availableWidth)-(3*interWindowGap) FreeCADGui.getMainWindow.setGeometry(0, 0, freeCadMainWidth, availableHeight) # resize the PAW window newPawWidth			= availableWidth-freeCadMainWidth newPawHeight		= (self.heightSliderValue/100.0) * availableHeight if self.rb1.isChecked: newPawTop		= 0 else: newPawTop		= availableHeight - newPawHeight self.parentWindow.setGeometry(freeCadMainWidth+interWindowGap, newPawTop, newPawWidth-interWindowGap, newPawHeight) self.close




 * 1) Class definitions
 * 1) Function definitions

def onFreeCADShutdown: # this will be invoked when FreeCAD is told to shut down #QtGui.QMessageBox.information(None,"","FreeCAD shutting down") if FreeCAD.PythonAssistantWindowStatus[1]: reply = QtGui.QMessageBox.question(None, "",			"The Python Assistant Window has changes, do you want to save them?",			QtGui.QMessageBox.Yes