#!/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)