#!/usr/bin/env python #**************************************************************************** # textedit3.py, provides classes for the qt3 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, QPopupMenu, QScrollView, QSize, QString, QTextEdit import os.path, sys, tempfile class DataEditLine(QTextEdit): """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): QTextEdit.__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.setTextFormat(Qt.PlainText) self.setWordWrap(QTextEdit.FixedPixelWidth) self.setWrapColumnOrWidth(stdWidth - 5) self.setHScrollBarMode(QScrollView.AlwaysOff) # self.setTabChangesFocus(True) # not in Qt 3.0.x 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.lines()), maxNumLines) else: numLines = self.format.numLines lineHt = QFontMetrics(self.font()).lineSpacing() self.setFixedSize(stdWidth, numLines * lineHt + lineHt // 2) self.setWordWrap(QTextEdit.WidgetWidth) self.connect(self, SIGNAL('textChanged()'), self.readChange) 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.hasSelectedText() # removed - interfered with middle-button paste # def paste(self): # """Override normal paste""" # self.editPaste() def keyPressEvent(self, event): """Bind keys to functions""" if event.key() == Qt.Key_V and event.state() == Qt.ControlButton: self.editPaste() # override normal paste elif event.key() == Qt.Key_Tab: self.parent().focusNextPrevChild(event.state() != Qt.ShiftButton) else: QTextEdit.keyPressEvent(self, event) 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.hasSelectedText()) 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.selectedText()) 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() paraFrom, indexFrom, paraTo, indexTo = self.getSelection() self.insert('%s%s%s' % (openTag, text, closeTag)) indexFrom += len(openTag) if paraFrom == paraTo: indexTo += len(openTag) self.setSelection(paraFrom, indexFrom, paraTo, indexTo) def createPopupMenu(self, pos): """Returns popup menu, overridden to change paste command and add font tag menu item""" popup = QTextEdit.createPopupMenu(self, pos) popup.removeItemAt(5) popup.insertItem('%s\t%s' % (_('&Paste'), _('Ctrl+V')), \ self.editPaste, 0, -1, 5) try: text = unicode(QApplication.clipboard().text()) except UnicodeError: text = '' popup.setItemEnabled(popup.idAt(5), len(text)) popup.insertSeparator(False) popup.insertItem(_('&Add Font Tags'), self.tagSubMenu(), -1, 0) popup.insertItem(_('&External Editor...'), self.showExtEditor, 0, -1, 0) return popup class TitleListView(QTextEdit): """Right pane list edit view, titles of current selection or its children""" def __init__(self, showChildren=True, parent=None, name=None): QTextEdit.__init__(self, parent, name) self.showChildren = showChildren self.oldItem = None self.setTextFormat(Qt.PlainText) self.setWordWrap(QTextEdit.NoWrap) # self.setTabChangesFocus(True) # not in Qt 3.0.x 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())) if item is not self.oldItem: self.ensureVisible(0, 0) # reset scroll if root changed self.oldItem = item else: self.clear() 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.hasSelectedText() 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)""" self.scrollBy(0, numPages * self.visibleHeight()) def keyPressEvent(self, event): """Bind keys to functions""" if event.key() == Qt.Key_V and event.state() == Qt.ControlButton: self.editPaste() # override normal paste elif event.key() == Qt.Key_Tab: self.parent().focusNextPrevChild(event.state() != Qt.ShiftButton) else: QTextEdit.keyPressEvent(self, event) def createPopupMenu(self, pos): """Returns popup menu, overridden to change paste command""" popup = QTextEdit.createPopupMenu(self, pos) popup.removeItemAt(5) popup.insertItem('%s\t%s' % (_('&Paste'), _('Ctrl+V')), \ self.editPaste, 0, -1, 5) try: text = unicode(QApplication.clipboard().text()) except UnicodeError: text = '' popup.setItemEnabled(popup.idAt(5), len(text)) return popup class FormatEdit(QTextEdit): """Editor signals cursor movement""" def __init__(self, parent=None, name=None): QTextEdit.__init__(self, parent, name) self.setTextFormat(Qt.PlainText) self.setWordWrap(QTextEdit.NoWrap) # self.setTabChangesFocus(True) # not in Qt 3.0.x self.setMinimumSize(QTextEdit.minimumSize(self).width(), \ self.fontMetrics().lineSpacing() * 4) self.connect(self, SIGNAL('cursorPositionChanged(int, int)'), \ self.cursorChg) def cursorChg(self): """Signal cursor movement""" self.emit(PYSIGNAL('cursorMove'), ()) def numLines(self): """Wrap multilinedit command""" return self.paragraphs() def textLine(self, lineNum): """Wrap multilinedit command""" return self.text(lineNum) def deselect(self): """Wrap multilinedit command""" self.removeSelection() def keyPressEvent(self, event): """Bind keys to functions""" if event.key() == Qt.Key_Tab: self.parent().focusNextPrevChild(event.state() != Qt.ShiftButton) else: QTextEdit.keyPressEvent(self, event) class SpellContextEdit(QTextEdit): """Editor for spell check word context""" def __init__(self, parent=None, name=None): QTextEdit.__init__(self, parent, name) self.setTextFormat(Qt.PlainText) def sizeHint(self): """Set prefered size""" try: fontHeight = QFontMetrics(self.currentFont()).lineSpacing() except AttributeError: fontHeight = QFontMetrics(self.font()).lineSpacing() # in early vers return QSize(QTextEdit.sizeHint(self).width(), fontHeight * 3) def setSelection(self, fromPos, toPos): """Select given range in first paragraph""" QTextEdit.setSelection(self, 0, fromPos, 0, toPos) def keyPressEvent(self, event): """Bind keys to functions""" if event.key() == Qt.Key_Tab: self.parent().focusNextPrevChild(event.state() != Qt.ShiftButton) else: QTextEdit.keyPressEvent(self, event)