Skip to content

Instantly share code, notes, and snippets.

@QiangF
Forked from kkoksvik/Python Assistant Window.py
Created November 4, 2015 13:56
Show Gist options
  • Save QiangF/8b633ac61d3e71a4c105 to your computer and use it in GitHub Desktop.
Save QiangF/8b633ac61d3e71a4c105 to your computer and use it in GitHub Desktop.
#
#
# Python Assistant Window
# v 0.1 initial release
#
#
#***********************************************************************************
#
# provide a text editing window with functions to aid in coding Python within FreeCAD
#
############ To Do ############
# - contextual window doesn't fire if cursor is on last position in file
# - executing "from PySide import QtGui, QtCore" on console seems to close window
# - is it possible to copy code to console and then select and execute it?
##############################
#***********************************************************************************
# The next three variables define the width and height and vertical positioning
# of the Python Assistant Window
# 'pawWidthPercent' specifies the percentage of the screen width to be assigned to the Python Assistant Window
# 'pawHeightPercent' specifies the percentage of the screen height to be assigned to the Python Assistant Window
# 'pawAtBottomFlag' specifies if the Python Assistant Window is at the top or the bottom
# The Python Assistant Window is automatically placed at the left,
# so pawWidthPercent = 26, pawHeightPercent = 41, pawAtBottomFlag = False will cause the
# following:
# 1) the main FreeCAD window will be placed in the upper left corner of the screen,
# it's height will be 100% of the screen height,
# it's width will be 74% (=100%-26%) of the screen
# 2) the Python Assistant Window will be placed in the left side of the screen,
# it's height will be 41% of the screen height,
# it's width will be 26% of the screen
# it will be at the top (leaving empty space below it)
# The empty space (either above or below the Python Assistant Window),
# is left for the text editor (for editing the Macros) to be placed in.
#
pawWidthPercentInitial = 37.5 # percent of the screen width
pawHeightPercentInitial = 32.0 # percent of the screen height
pawAtBottomFlagInitial = True
#***********************************************************************************
# import statements
import sys, operator, os
from os.path import expanduser
from PySide import QtGui, QtCore
# UI Class definitions
class PythonAssistantWindow(QtGui.QMainWindow):
""""""
def __init__(self, pythonTextToEdit):
self.textIn = pythonTextToEdit
super(PythonAssistantWindow, self).__init__()
self.initUI(pythonTextToEdit)
def initUI(self, pythonTextToEdit):
"""Constructor"""
# set default return value and pointer to subsequent child window
self.result = userCancelled
self.childWindow = None
self.alertWindow = None
# set window dimensions for Python Advisor Window from the constants at the top of Macro file
self.pawWinWidth = pawWidthPercentInitial/100.0 * availableWidth
self.pawWinHeight = pawHeightPercentInitial/100.0 * availableHeight
self.left = screenWidth - self.pawWinWidth
if pawAtBottomFlagInitial:
self.top = screenHeight - self.pawWinHeight
else:
self.top = 0
self.editorHeight = self.pawWinHeight
# set dimensions for main FreeCAD window
self.mainWinWidth = availableWidth - (self.pawWinWidth+interWindowGap)
self.mainWinHeight = availableHeight
# define main window
FreeCADGui.getMainWindow().setGeometry(0, 0, self.mainWinWidth, self.mainWinHeight)
# now set up this window
self.setGeometry(self.left, self.top, self.pawWinWidth, self.pawWinHeight)
self.setWindowTitle("Python Assistant Window")
#
centralWidget = QtGui.QWidget(self)
layout = QtGui.QGridLayout()
centralWidget.setLayout(layout)
# set up text editing widget
self.text_editor = QtGui.QPlainTextEdit(self)
self.text_editor.move(0,0)
self.text_editor.resize(self.pawWinWidth,self.editorHeight)
self.text_editor.appendPlainText(self.textIn)
self.text_editor.setSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)
self.text_editor.textChanged.connect(self.onTextChanged)
# set up a monospace font for the text editor to match the Python console
font = QtGui.QFont()
font.setFamily("Courier")
font.setStyleHint(QtGui.QFont.Monospace)
font.setFixedPitch(True)
font.setPointSize(12)
self.text_editor.setFont(font)
#self.text_editor.cursorPositionChanged.connect(self.onCursorPosition)
self.cursor = self.text_editor.textCursor()
# populate layout
layout.addWidget(self.text_editor,0,0)
self.setCentralWidget(centralWidget)
# set contextual menu options for text editing widget
# menu dividers
mnuDivider1 = QtGui.QAction(self)
mnuDivider1.setText(menuDividerText)
mnuDivider1.triggered.connect(self.onMenuDivider)
mnuDivider2 = QtGui.QAction(self)
mnuDivider2.setText(menuDividerText)
mnuDivider2.triggered.connect(self.onMenuDivider)
mnuDivider3 = QtGui.QAction(self)
mnuDivider3.setText(menuDividerText)
mnuDivider3.triggered.connect(self.onMenuDivider)
mnuDivider4 = QtGui.QAction(self)
mnuDivider4.setText(menuDividerText)
mnuDivider4.triggered.connect(self.onMenuDivider)
# clear text
mnuClear = QtGui.QAction(self)
mnuClear.setText("Clear")
mnuClear.triggered.connect(self.onClear)
# paste copy/paste buffer
mnuPaste = QtGui.QAction(self)
mnuPaste.setText("Paste")
mnuPaste.triggered.connect(self.onPaste)
# paste contents of console
mnuAppendFromConsole = QtGui.QAction(self)
mnuAppendFromConsole.setText("Append contents of console")
mnuAppendFromConsole.triggered.connect(self.onAppendFromConsole)
# select between markers
mnuSelectMarkers = QtGui.QAction(self)
mnuSelectMarkers.setText("Select between markers")
mnuSelectMarkers.triggered.connect(self.onSelectMarkers)
# select all
mnuSelectAll = QtGui.QAction(self)
mnuSelectAll.setText("Select all")
mnuSelectAll.triggered.connect(self.onSelectAll)
# insert marker
mnuInsertMarker = QtGui.QAction(self)
mnuInsertMarker.setText("Insert marker")
mnuInsertMarker.triggered.connect(self.onInsertMarker)
# remove console generated ">>> " character strings
mnuStripPrefix = QtGui.QAction(self)
mnuStripPrefix.setText("Remove '>>> '")
mnuStripPrefix.triggered.connect(self.onStripPrefix)
# remove blank lines
mnuReduceBlankLines = QtGui.QAction(self)
mnuReduceBlankLines.setText("Delete multiple blank lines")
mnuReduceBlankLines.triggered.connect(self.onReduceBlankLines)
# copy selection
mnuCopy = QtGui.QAction(self)
mnuCopy.setText("Copy")
mnuCopy.triggered.connect(self.onCopy)
# copy selection to console
mnuCopySelectionToConsole = QtGui.QAction(self)
mnuCopySelectionToConsole.setText("Copy selection to console")
mnuCopySelectionToConsole.triggered.connect(self.onCopySelectionToConsole)
# copy to console
mnuCopyToConsole = QtGui.QAction(self)
mnuCopyToConsole.setText("Copy contents to console")
mnuCopyToConsole.triggered.connect(self.onCopyToConsole)
# save as file
mnuSaveAsFile = QtGui.QAction(self)
mnuSaveAsFile.setText("Save contents to file")
mnuSaveAsFile.triggered.connect(self.onSaveAsFile)
# close window
mnuCloseWindow = QtGui.QAction(self)
mnuCloseWindow.setText("Close window")
mnuCloseWindow.triggered.connect(self.onCloseWindow)
# alter GUI settings
mnuSettings = QtGui.QAction(self)
mnuSettings.setText("=Alter GUI settings=")
mnuSettings.triggered.connect(self.onSettings)
# define menu and add options
self.text_editor.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
self.text_editor.addAction(mnuCopy)
self.text_editor.addAction(mnuCopySelectionToConsole)
self.text_editor.addAction(mnuCopyToConsole)
self.text_editor.addAction(mnuDivider1)
self.text_editor.addAction(mnuPaste)
self.text_editor.addAction(mnuAppendFromConsole)
self.text_editor.addAction(mnuSelectMarkers)
self.text_editor.addAction(mnuSelectAll)
self.text_editor.addAction(mnuClear)
self.text_editor.addAction(mnuDivider2)
self.text_editor.addAction(mnuInsertMarker)
self.text_editor.addAction(mnuStripPrefix)
self.text_editor.addAction(mnuReduceBlankLines)
self.text_editor.addAction(mnuDivider3)
self.text_editor.addAction(mnuSaveAsFile)
self.text_editor.addAction(mnuSettings)
self.text_editor.addAction(mnuCloseWindow)
#
self.show()
#----------------------------------------------------------------------
def onMenuDivider(self):
# just a divider in the menu so we don't do anything
pass
def onClear(self):
# clear editing field
self.text_editor.clear()
def onPaste(self):
# paste contents of system copy/paste buffer into QPlainTextEdit field
self.text_editor.paste()
def onAppendFromConsole(self):
# copy text from "Python console"
mainWindow = FreeCADGui.getMainWindow()
pcDW = mainWindow.findChild(QtGui.QDockWidget, "Python console")
pcPTE = pcDW.findChild(QtGui.QPlainTextEdit, "Python console")
consoleStr = pcPTE.document().toPlainText()
self.text_editor.appendPlainText(copyFromConsoleText)
self.text_editor.appendPlainText("")
self.text_editor.appendPlainText(consoleStr)
def onCopy(self):
# copy selected text to system copy/paste buffer
self.text_editor.copy()
def onCopySelectionToConsole(self):
# copy selected text to "Python console"
mainWindow = FreeCADGui.getMainWindow()
pcDW = mainWindow.findChild(QtGui.QDockWidget, "Python console")
pcPTE = pcDW.findChild(QtGui.QPlainTextEdit, "Python console")
#
cursor = self.text_editor.textCursor()
cursorText = self.text_editor.toPlainText()
textToCopy = cursorText[cursor.selectionStart():cursor.selectionEnd()]
if len(textToCopy)>0:
pcPTE.appendPlainText(textToCopy)
def onCopyToConsole(self):
# copy text to "Python console"
mainWindow = FreeCADGui.getMainWindow()
pcDW = mainWindow.findChild(QtGui.QDockWidget, "Python console")
pcPTE = pcDW.findChild(QtGui.QPlainTextEdit, "Python console")
pcPTE.appendPlainText(copyToConsoleText)
pcPTE.appendPlainText()
def onInsertMarker(self):
# insert marker
self.text_editor.insertPlainText(markerText)
def onStripPrefix(self):
# strip out ">>> " from text edit window
self.text_editor.selectAll()
if len(self.text_editor.toPlainText())>0:
self.text_editor.selectAll()
tmp = self.text_editor.toPlainText()
self.text_editor.clear()
self.text_editor.appendPlainText(tmp.replace(">>> ",""))
def onReduceBlankLines(self):
# reduce multiple blank lines to single blank lines
contents = self.text_editor.toPlainText()
self.text_editor.clear()
self.text_editor.appendPlainText(os.linesep.join([s for s in contents.splitlines() if s]))
def onSelectMarkers(self):
cursor = self.text_editor.textCursor()
cursorText = self.text_editor.toPlainText()
bNum = cursor.blockNumber(); cNum = cursor.columnNumber()
pos = cursor.position(); cursorTextLength = len(cursorText)
occurrences = [i for i in range(len(cursorText)) if cursorText.startswith(markerText, i)]
if len(occurrences)==0:
self.alertWindow = QtGui.QMessageBox()
self.alertWindow.setText("There are no markers...")
self.alertWindow.show()
elif len(occurrences)==1:
hdrStart = occurrences[0]
hdrEnd = hdrStart + markerTextLength
if pos<hdrStart:
selectStart = 0; selectEnd = hdrStart
self.cursor.setPosition(selectStart)
self.cursor.setPosition(selectEnd, QtGui.QTextCursor.KeepAnchor)
self.text_editor.setTextCursor(self.cursor)
if pos>hdrEnd:
selectStart = hdrEnd; selectEnd = cursorTextLength
self.cursor.setPosition(selectStart)
self.cursor.setPosition(selectEnd, QtGui.QTextCursor.KeepAnchor)
self.text_editor.setTextCursor(self.cursor)
else:
startOccurrences = list(); endOccurrences = list(occurrences)
for i in range(len(occurrences)):
startOccurrences.append(occurrences[i] + markerTextLength + 1)
startOccurrences.insert( 0, 0)
endOccurrences.insert( len(occurrences), cursorTextLength)
for i in range(len(occurrences)+1):
if startOccurrences[i]<pos<endOccurrences[i]:
if i==0:
selectStart = startOccurrences[i]
else:
selectStart = startOccurrences[i]-1
selectEnd = endOccurrences[i]
self.cursor.setPosition(selectStart)
self.cursor.setPosition(selectEnd, QtGui.QTextCursor.KeepAnchor)
self.text_editor.setTextCursor(self.cursor)
break
def onSelectAll(self):
self.text_editor.selectAll()
def onCloseWindow(self):
self.close()
def onSettings(self):
# get new width (as %), height (as %), vertical flag
self.childWindow = GetGuiConfigParams(self)
pass
def onTextChanged(self):
FreeCAD.PythonAssistantWindowStatus[1] = True
def onCursorPosition(self):
#print ("Line: {} | 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()
#----------------------------------------------------------------------
# Class definitions
# 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 | QtGui.QMessageBox.No, QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
# write out contents for next session
file = open(persistenceFile,"w")
file.write(FreeCAD.PythonAssistantWindowStatus[0].text_editor.toPlainText())
file.close()
del FreeCAD.PythonAssistantWindowStatus
# Constant definitions
userCancelled = "Cancelled"
markerText = "#=========== marker ===========\n"
markerTextLength = len(markerText)
copyFromConsoleText = "#=========== copy from console ==========="
copyToConsoleText = "#============ copy to console ============"
menuDividerText = "--------"
interWindowGap = 3 # space between 2 windows for appearance sake
persistenceFile = App.ConfigGet("UserAppData")+"PythonAssistantWindow.txt"
# code ***********************************************************************************
# put down user data saving routine for when FreeCAD exits
mw=Gui.getMainWindow()
mw.mainWindowClosed.connect(onFreeCADShutdown)
# get screen dimensions
screenWidth = QtGui.QDesktopWidget().screenGeometry().width()
screenHeight = QtGui.QDesktopWidget().screenGeometry().height()
# get dimensions for available space on screen
availableWidth = QtGui.QDesktopWidget().availableGeometry().width()
availableHeight = QtGui.QDesktopWidget().availableGeometry().height()
if not hasattr(FreeCAD,"PythonAssistantWindowStatus"):
previousContents = ""
if os.path.isfile(persistenceFile):
# read contents of last session
file = open(persistenceFile,"r")
previousContents = file.read()
file.close()
# open window with contents from last session
form = PythonAssistantWindow(previousContents)
# save pointer to window so it can be located again and Raised when it becomes obscured
FreeCAD.PythonAssistantWindowStatus = [None, False]
FreeCAD.PythonAssistantWindowStatus[0] = form
else:
# window is open so Raise it so it is visible
FreeCAD.PythonAssistantWindowStatus[0].raise_()
pass
#
#OS: Mac OS X
#Word size: 64-bit
#Version: 0.14.3703 (Git)
#Branch: releases/FreeCAD-0-14
#Hash: c6edd47334a3e6f209e493773093db2b9b4f0e40
#Python version: 2.7.5
#Qt version: 4.8.6
#Coin version: 3.1.3
#SoQt version: 1.5.0
#OCC version: 6.7.0
#
#thus ends the macro...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment