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