Source code for fancywidgets.pyQtBased.CodeEditor

from PyQt4 import QtGui, QtCore
import __builtin__
import importlib
from pkginfo import Installed
import warnings
# OWN
from fancywidgets.pyQtBased.highlighter import Highlighter
from fancytools.fcollections.naturalSorting import naturalSorting

# FIXME
# in order to also have in in a frozen environment
# see _getInstalledModules
import _installed_modules


[docs]class CodeEditor(QtGui.QWidget): ''' A simple code editor with a QPlainTextEdit and line numbers of the left side ''' def __init__(self, dialog=None): QtGui.QWidget.__init__(self) if dialog is None: dialog = QtGui.QFileDialog self.dialog = dialog self.editor = _CodeTextEdit(self) self.lineNumbers = _LineNumberArea(self.editor) l = QtGui.QHBoxLayout() self.setLayout(l) l.addWidget(self.lineNumbers) l.addWidget(self.editor)
[docs] def addGlobals(self, g): ''' g ==> {name:description,...} ''' self.editor._globals.update(g) pass
class _CodeTextEdit(QtGui.QPlainTextEdit): ''' a text editor with ... * monospace font, * tab2spaces, * python syntax highlighter * 'save to file' in context menu ''' def __init__(self, codeEditor): QtGui.QPlainTextEdit.__init__(self) self._globals = {} self._mg = None self.codeEditor = codeEditor # FONT: set monospace font = QtGui.QFont() font.setFamily("Courier") font.setStyleHint(QtGui.QFont.Monospace) font.setFixedPitch(True) font.setPointSize(10) self.setFont(font) # TAB to spaces tabStop = 4 # 4 characters metrics = QtGui.QFontMetrics(font) self.setTabStopWidth(tabStop * metrics.width(' ')) # SCROLL BAR - horizontal: self.setLineWrapMode(QtGui.QPlainTextEdit.NoWrap) # SYNTAX highlighter: self.highlighter = Highlighter(self.document(), "python") @staticmethod def _nameFromModName(name): #mod name is given as 'mon (v0.1)' #return 'mod' return name[:name.index(' ')] def _addImportModule(self, name): #insert 'import <mod>' at the begin name = self._nameFromModName(name) c = self.textCursor() p = c.position() c.setPosition(0) self.setTextCursor(c) self.insertPlainText('import %s\n' %name) c.setPosition(p) self.setTextCursor(c) self.moveCursor(QtGui.QTextCursor.Down) def _addObject(self, name): cc = QtGui.QTextCursor c = self.textCursor() c.select(cc.WordUnderCursor) txt = c.selectedText() if txt: c.movePosition(cc.EndOfWord) self.setTextCursor(c) if c.hasSelection(): c.removeSelectedText() txt = txt + ' ' else: txt = ' ' c.clearSelection() try: if callable(eval(name)): txt += name + '()' else: txt += name except (NameError, SyntaxError): txt += name self.insertPlainText(txt) def _addMenuEntries(self, menu, entries, fn): #limit number of entries to be shown in a menu #create sub menus (e.g. 'A-F') entries = sorted(entries) def getLetter(): try: return entries[i+mx][0].capitalize() except IndexError: return entries[-1][0].capitalize() c = -1 i = 0 mx = 15 if len(entries) < mx: sub = menu else: letter = getLetter() sub = menu.addMenu('%s-%s' % (entries[0][0].capitalize(),letter) ) for e in entries: sub.addAction(e).triggered.connect( lambda checked, n=e: fn(n)) c += 1 i += 1 if c == mx: new_letter = getLetter() sub = menu.addMenu(letter + '-' + new_letter) letter = new_letter c = 0 def _buildGlobalsMenu(self): mg = self._mg mg.aboutToShow.disconnect(self._buildGlobalsMenu) # GIVEN GLOBALS: for gi in naturalSorting(self._globals.keys()): mg.addAction(gi).triggered.connect( lambda checked, n=gi: self._addObject(n)) if self._globals: mg.addSeparator() # BUILT-INs mb = mg.addMenu('Built-in Objects') l = [i for i in dir(__builtin__)] # exclude warnings and errors: err = [] war = [] for i in xrange(len(l)-1,-1,-1): v = l[i] if v.endswith('Error') or v.endswith('Exception'): err.append(l.pop(i)) elif v.endswith('Warning'): war.append(l.pop(i)) self._addMenuEntries(mb, l, self._addObject) m = mb.addMenu('Errors') self._addMenuEntries(m, err, self._addObject) m = mb.addMenu('Warnings') self._addMenuEntries(m, war, self._addObject) # MODULES mm = mg.addMenu('Installed modules') self._addMenuEntries(mm, self._getInstalledModules(), self._addImportModule) def _getInstalledModules(self): pip = importlib.import_module('pip')#save some startup time l = sorted( ["%s (%s)" % (i.key, i.version) for i in pip.get_installed_distributions()] ) #FIXME: with pip v8.1.1. l will be [] #if executed in a frozen environment #for this case load infos from file: if not l: l = _installed_modules.l else: #update file with open(_installed_modules.__file__, 'w') as f: f.write('''#this file is auto generated by #CodeEditor.py - do not delete it l=''') f.write(str(l)) return l def _globalMenuHovered(self, action): #show a functions/modules __doc__ as tooltip mg = self._mg s = str(action.text()) txt = None try: txt = eval(s).__doc__ except (NameError, SyntaxError): #action text is not a global #maybe because its just menu text #or if will be become global, when code txt is compiled: if s in self._globals: txt = self._globals[s] #in case a module is hovered: elif '(' in s: s = self._nameFromModName(s) with warnings.catch_warnings(): #ignore-> UserWarning: No PKG-INFO found for package: ... warnings.simplefilter("ignore") #FIXME: txt will be empty if executed in frozen environment: txt = Installed(s).description if txt is None: txt = '' elif len(txt)>1000: txt = txt[:1000] txt += '\n...' QtGui.QToolTip.showText( QtGui.QCursor.pos(), txt, mg, mg.actionGeometry(action)) def getGlobalsMenu(self): if self._mg is not None: return self._mg #add globals to menu: self._mg = mg = QtGui.QMenu('Globals') #to show tooltips of its containing actions: mg.hovered.connect(self._globalMenuHovered) #make font bold: mga = mg.menuAction() f = mga.font() f.setBold(True) mga.setFont(f) #only build, when needed: mg.aboutToShow.connect(self._buildGlobalsMenu) # mg.aboutToHide.connect(mg.clear) #this doesnt free memory,so leave it return mg def contextMenuEvent(self, event): ''' Add menu action: * 'Show line numbers' * 'Save to file' ''' menu = QtGui.QPlainTextEdit.createStandardContextMenu(self) mg = self.getGlobalsMenu() a0 = menu.actions()[0] menu.insertMenu(a0, mg) menu.insertSeparator(a0) menu.addSeparator() a = QtGui.QAction('Show line numbers', menu) l = self.codeEditor.lineNumbers a.setCheckable(True) a.setChecked(l.isVisible()) a.triggered.connect(lambda checked: l.show() if checked else l.hide()) menu.addAction(a) menu.addSeparator() a = QtGui.QAction('Save to file', menu) a.triggered.connect(self.saveToFile) menu.addAction(a) menu.exec_(event.globalPos()) def saveToFile(self): ''' Save the current text to file ''' filename = self.codeEditor.dialog.getSaveFileName() if filename and filename != '.': with open(filename, 'w') as f: f.write(self.toPlainText()) print('saved script under %s' %filename) def toPlainText(self): ''' replace [tab] with 4 spaces ''' txt = QtGui.QPlainTextEdit.toPlainText(self) return txt.replace('\t', ' ') class _LineNumberArea(QtGui.QPlainTextEdit): ''' Left area to show line numbers of the code editor ''' def __init__(self, editor): QtGui.QPlainTextEdit.__init__(self) self.setReadOnly(True) self.setFont(editor.font()) self.setEnabled(False) self.setFixedWidth(35) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setFrameStyle(0) self.appendPlainText(str(1)) editor.blockCountChanged.connect(self._updateNumbers) editor.verticalScrollBar().sliderMoved.connect(self._syncHScrollBar) def _syncHScrollBar(self, val): ''' Synchronize the view with the code editor ''' self.verticalScrollBar().setValue(val) def _updateNumbers(self, linenumers): ''' add/remove line numbers ''' b = self.blockCount() c = b - linenumers if c > 0: # remove lines numbers for _ in range(c): # remove last line: self.setFocus() storeCursorPos = self.textCursor() self.moveCursor(QtGui.QTextCursor.End, QtGui.QTextCursor.MoveAnchor) self.moveCursor(QtGui.QTextCursor.StartOfLine, QtGui.QTextCursor.MoveAnchor) self.moveCursor(QtGui.QTextCursor.End, QtGui.QTextCursor.KeepAnchor) self.textCursor().removeSelectedText() self.textCursor().deletePreviousChar() self.setTextCursor(storeCursorPos) elif c < 0: # add line numbers for i in range(-c): self.appendPlainText(str(b+i+1)) if __name__ == '__main__': import sys app = QtGui.QApplication(sys.argv) w = CodeEditor() w.setWindowTitle(w.__class__.__name__) w.editor.setPlainText('#python highlighting\ni = int(5)\ndef fn(a,i):\n\tprint a,i') w.show() app.exec_()