#!/usr/bin/env python #**************************************************************************** # textedit2.py, provides classes for the qt2 text editors # # TreeLine, an information storage program # Copyright (C) 2005, Douglas W. Bell # # This is free software; you can redistribute it and/or modify it under the # terms of the GNU General Public License, Version 2. This program is # distributed in the hope that it will be useful, but WITTHOUT ANY WARRANTY. #***************************************************************************** from treedoc import TreeDoc from optiondefaults import OptionDefaults import globalref from qt import Qt, PYSIGNAL, SIGNAL, SLOT, QApplication, QColor, \ QColorDialog, QFileDialog, QFontMetrics, QInputDialog, \ QMessageBox, QMultiLineEdit, QPopupMenu, QSize, QString import os.path, sys, tempfile class DataEditLine(QMultiLineEdit): """Line editor within data edit view""" tagMenuEntries = [(_('&Bold'), '', ''), \ (_('&Italics'), '', ''), \ (_('&Underline'), '', ''), \ (_('&Size...'), '', ''), \ (_('&Color...'), '', '')] tagMenuFirstId = 100 def __init__(self, key, item, labelRef, stdWidth, parent=None, name=None): QMultiLineEdit.__init__(self, parent, name) self.key = key self.item = item self.labelRef = labelRef self.labelFont = labelRef.font() self.labelBoldFont = labelRef.font() self.labelBoldFont.setBold(True) self.stdWidth = stdWidth self.setMinimumWidth(stdWidth) self.setWordWrap(QMultiLineEdit.WidgetWidth) self.undoAvail = False self.redoAvail = False self.format = item.nodeFormat.findField(key) editText, ok = self.format.editText(item) if not ok: self.labelRef.setFont(self.labelBoldFont) self.labelRef.update() self.setText(editText) maxNumLines = globalref.options.intData('MaxEditLines', 1, \ OptionDefaults.maxNumLines) if self.format.numLines == 1: # can expand to maxNumLines if field set to default of 1 numLines = min(max(1, self.numLines()), maxNumLines) else: numLines = self.format.numLines self.setFixedVisibleLines(numLines + \ (self.maxLineWidth() > stdWidth and 2 or 0)) self.connect(self, SIGNAL('textChanged()'), self.readChange) self.connect(self, SIGNAL('undoAvailable(bool)'), self.setUndoAvail) self.connect(self, SIGNAL('redoAvailable(bool)'), self.setRedoAvail) def readChange(self): """Update variable from edit contents""" # text = u' '.join(unicode(self.text()).split()) text = unicode(self.text()).strip() editText, ok = self.format.editText(self.item) if text != editText: globalref.docRef.undoStore.addDataUndo(self.item, True) newText, ok = self.format.storedText(text) self.item.data[self.key] = newText self.labelRef.setFont(ok and self.labelFont or self.labelBoldFont) self.labelRef.update() globalref.docRef.modified = True self.emit(PYSIGNAL('entryChanged'), ()) if globalref.pluginInterface: globalref.pluginInterface.execCallback(globalref.\ pluginInterface.\ dataChangeCallbacks, \ self.item, [self.format]) def pasteText(self, text): """Paste text given in param""" self.insert(text) self.readChange() def editPaste(self): """Paste text from clipboard""" try: text = unicode(QApplication.clipboard().text()) except UnicodeError: return item = globalref.docRef.readXmlString(text, False) if item: text = item.title() self.pasteText(text) def fileBrowse(self): """Open file browser to set contents""" dfltPath = unicode(self.text()).strip() if not dfltPath or not os.path.exists(dfltPath): dfltPath = os.path.dirname(globalref.docRef.fileName) fileName = unicode(QFileDialog.getOpenFileName(dfltPath, \ '%s (*)' % _('All Files'), \ self, None, \ _('Browse for file name'))) if fileName: if ' ' in fileName: if sys.platform == 'win32': fileName = '"%s"' % fileName else: fileName = fileName.replace(' ', '\ ') self.setText(fileName) def showExtEditor(self): """Start external editor for the text in this edit box""" tmpPathName = self.writeTmpFile() if tmpPathName and self.findExtEditor(tmpPathName): try: f = file(tmpPathName, 'r') self.setText(f.read().strip().decode('utf-8')) f.close() except IOError: pass try: os.remove(tmpPathName) except OSError: print 'Could not remove tmp file "%s"' % tmpPathName def writeTmpFile(self): """Write tmp file with editor contents, return successful path""" fd, fullPath = tempfile.mkstemp(prefix='tl_', text=True) try: f = os.fdopen(fd, 'w') f.write(unicode(self.text()).strip().encode('utf-8')) f.close() except IOError: return '' return fullPath def findExtEditor(self, argument): """Find and launch external editor, look in option text, then EDITOR variable, then prompt for new option text, return True on success""" paths = [globalref.options.strData('ExtEditorPath', True), \ os.environ.get('EDITOR', '')] for path in paths: if path and sys.platform != 'win32': if os.system("%s '%s'" % (path, argument)) == 0: return True elif path: try: # spawnl for Win - os.system return value not relaible if os.spawnl(os.P_WAIT, path, os.path.basename(path), \ argument) <= 0: return True except OSError: pass ans = QMessageBox.warning(self, _('External Editor'), \ _('Could not find an external editor.\n'\ 'Manually locate?\n'\ '(or set EDITOR env variable)'), \ _('&Browse'), _('&Cancel'), QString.null, \ 0, 1) if ans == 0: filter = sys.platform == 'win32' and '%s (*.exe)' % _('Programs') \ or '%s (*)' % _('All Files') path = unicode(QFileDialog.getOpenFileName(QString.null, \ filter, self, '', \ _('Locate external editor'))) if path: globalref.options.changeData('ExtEditorPath', path, True) globalref.options.writeChanges() return self.findExtEditor(argument) return False def copyAvail(self): """Return True if there is selected text""" return self.hasMarkedText() def sizeHint(self): """Set prefered size""" return QSize(self.stdWidth, QMultiLineEdit.sizeHint(self).height()) def setUndoAvail(self, avail): """Set undo availability based on signal""" self.undoAvail = avail def setRedoAvail(self, avail): """Set redo availability based on signal""" self.redoAvail = avail def tagSubMenu(self): """Return menu for html tag additions""" menu = QPopupMenu(self) index = 0 for text, open, close in DataEditLine.tagMenuEntries: menu.insertItem(text, DataEditLine.tagMenuFirstId + index) menu.setItemEnabled(DataEditLine.tagMenuFirstId + index, \ self.hasMarkedText()) index += 1 self.connect(menu, SIGNAL('activated(int)'), self.addTag) return menu def addTag(self, num): """Add HTML tag based on popup menu""" label, openTag, closeTag = DataEditLine.\ tagMenuEntries[num - DataEditLine.tagMenuFirstId] text = unicode(self.markedText()) if label == _('&Size...'): num, ok = QInputDialog.getInteger(_('Font Size'), \ _('Enter size factor (-6 to +6)'), \ 1, -6, 6, 1, self) if not ok or num == 0: return openTag = openTag % num elif label == _('&Color...'): color = QColorDialog.getColor(QColor(), self) if not color.isValid(): return openTag = openTag % color.name() self.insert('%s%s%s' % (openTag, text, closeTag)) # lots of code to leave text marked (but not the tags) line, col = self.getCursorPosition() endLine, endCol = line, col - len(closeTag) while endCol < 0: endLine -= 1 endCol += len(unicode(self.textLine(endLine))) if self.isEndOfParagraph(endLine): endCol += 1 startLine, startCol = endLine, endCol - len(text) while startCol < 0: startLine -= 1 startCol += len(unicode(self.textLine(startLine))) if self.isEndOfParagraph(startLine): startCol += 1 self.setCursorPosition(startLine, startCol) self.setCursorPosition(endLine, endCol, True) self.readChange() def mousePressEvent(self, event): """Mouse press down event for custom popup menu""" if event.button() == Qt.RightButton: popup = QPopupMenu(self) popup.insertItem(_('&External Editor...'), self.showExtEditor) popup.insertItem(_('&Add Font Tags'), self.tagSubMenu()) popup.insertSeparator() id = popup.insertItem(_('&Undo'), self, SLOT('undo()'), \ Qt.CTRL+Qt.Key_Z) popup.setItemEnabled(id, self.undoAvail) id = popup.insertItem(_('&Redo'), self, SLOT('redo()'), \ Qt.CTRL+Qt.Key_Y) popup.setItemEnabled(id, self.redoAvail) popup.insertSeparator() id = popup.insertItem(_('C&ut'), self, SLOT('cut()'), \ Qt.CTRL+Qt.Key_X) popup.setItemEnabled(id, self.copyAvail()) id = popup.insertItem(_('&Copy'), self, SLOT('copy()'), \ Qt.CTRL+Qt.Key_C) popup.setItemEnabled(id, self.copyAvail()) id = popup.insertItem(_('&Paste'), self, SLOT('paste()'), \ Qt.CTRL+Qt.Key_V) try: text = unicode(QApplication.clipboard().text()) except UnicodeError: text = '' popup.setItemEnabled(id, len(text)) id = popup.insertItem(_('C&lear'), self, SLOT('clear()')) text = unicode(self.text()) popup.setItemEnabled(id, len(text)) popup.insertSeparator() id = popup.insertItem(_('&Select All'), self, SLOT('selectAll()')) popup.setItemEnabled(id, len(text)) popup.popup(event.globalPos()) else: QMultiLineEdit.mousePressEvent(self, event) def keyPressEvent(self, event): """Bind keys to functions""" if event.key() == Qt.Key_V and event.state() == Qt.ControlButton: self.editPaste() # override normal paste event.accept() elif event.key() == Qt.Key_Tab: self.focusNextPrevChild(True) event.accept() else: QMultiLineEdit.keyPressEvent(self, event) class TitleListView(QMultiLineEdit): """Right pane list edit view, titles of current selection or its children""" def __init__(self, showChildren=True, parent=None, name=None): QMultiLineEdit.__init__(self, parent, name) self.showChildren = showChildren self.connect(self, SIGNAL('textChanged()'), self.readChange) def updateView(self): """Replace contents with selected item child list""" self.blockSignals(True) self.clear() item = globalref.docRef.selection.currentItem if item: if not self.showChildren: self.setText(item.title()) else: self.setText(u'\n'.join(item.childText())) self.blockSignals(False) def readChange(self): """Update doc from edit view contents""" item = globalref.docRef.selection.currentItem if item: if self.showChildren: item.editChildList(unicode(self.text()).split('\n')) else: if not item.setTitle(unicode(self.text()), True): return globalref.updateViewTreeItem(item, True) globalref.updateViewMenuStat() def copyAvail(self): """Return True if there is selected text""" return self.hasMarkedText() def pasteText(self, text): """Paste text given in param""" self.insert(text) self.emit(SIGNAL('textChanged()'), ()) def editPaste(self): """Paste text from clipboard""" try: text = unicode(QApplication.clipboard().text()) except UnicodeError: return item = globalref.docRef.readXmlString(text, False) if item: text = item.title() self.pasteText(text) def scrollPage(self, numPages=1): """Scrolls down by numPages (negative for up)""" if numPages > 0: for num in range(numPages): self.pageDown() else: for num in range(-numPages): self.pageUp() def dropEvent(self, event): """Force update after text drop""" QMultiLineEdit.dropEvent(self, event) self.emit(SIGNAL('textChanged()'), ()) def keyPressEvent(self, event): """Bind keys to functions""" if event.key() == Qt.Key_V and event.state() == Qt.ControlButton: self.editPaste() # override normal paste event.accept() elif event.key() == Qt.Key_Tab: self.focusNextPrevChild(True) event.accept() else: QMultiLineEdit.keyPressEvent(self, event) class FormatEdit(QMultiLineEdit): """Editor signals cursor movement""" def __init__(self, parent=None, name=None): QMultiLineEdit.__init__(self, parent, name) self.setMinimumSize(QMultiLineEdit.minimumSize(self).width(), \ self.fontMetrics().lineSpacing() * 4) def event(self, event): """Signal cursor movement if text didn't also change""" pos = self.getCursorPosition() self.setEdited(False) result = QMultiLineEdit.event(self, event) if not self.edited() and pos != self.getCursorPosition(): self.emit(PYSIGNAL('cursorMove'), ()) return result def keyPressEvent(self, event): """Ignore tab key to allow focus change""" if event.key() == Qt.Key_Tab: self.focusNextPrevChild(True) event.accept() else: QMultiLineEdit.keyPressEvent(self, event) class SpellContextEdit(QMultiLineEdit): """Editor for spell check word context""" def __init__(self, parent=None, name=None): QMultiLineEdit.__init__(self, parent, name) self.setWordWrap(QMultiLineEdit.WidgetWidth) def sizeHint(self): """Set prefered size""" fontHeight = QFontMetrics(self.font()).lineSpacing() return QSize(QMultiLineEdit.sizeHint(self).width(), fontHeight * 3) def setSelection(self, fromPos, toPos): """Select given range""" fromLine, fromCol = self.posToLinePos(fromPos) self.setCursorPosition(fromLine, fromCol, False) toLine, toCol = self.posToLinePos(toPos) self.setCursorPosition(toLine, toCol, True) def posToLinePos(self, pos): """Return line number and linePos tuple for absolute position pos""" beginPos = 0 for lineNum in range(self.numLines()): lineLen = len(unicode(self.textLine(lineNum))) if pos <= beginPos + lineLen: return (lineNum, pos - beginPos) beginPos += lineLen return (-1, 0) # error value