#!/usr/bin/env python #**************************************************************************** # treerightviews.py, provides classes for the data edit & data output views # # 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 import globalref, treedoc from qt import Qt, PYSIGNAL, SIGNAL, SLOT, qApp, qVersion, QApplication, \ QComboBox, QEvent, QFontMetrics, QFrame, QGridLayout, \ QGroupBox, QLabel, QLayout, QListBox, QListBoxItem, \ QListBoxText, QMimeSourceFactory, QPushButton, QScrollView, \ QSize, QSizePolicy, QStringList, QTextBrowser, QWidget if qVersion()[0] >= '3': from textedit3 import DataEditLine else: from textedit2 import DataEditLine from xml.sax.saxutils import unescape import os.path, sys, webbrowser class DataOutView(QTextBrowser): """Right pane view of database info, read-only""" def __init__(self, showChildren=True, parent=None, name=None): QTextBrowser.__init__(self, parent, name) self.showChildren = showChildren self.oldItem = None self.setTextFormat(Qt.RichText) self.setFocusPolicy(QWidget.NoFocus) self.source = QMimeSourceFactory() self.setMimeSourceFactory(self.source) self.connect(self, SIGNAL('highlighted(const QString&)'), self.showLink) def updateView(self): """Replace contents with selected item data list""" item = globalref.docRef.selection.currentItem if item: path = os.path.dirname(globalref.docRef.fileName) self.source.setFilePath(QStringList(path)) sep = globalref.docRef.lineBreaks and u'
\n' or u'\n' if not self.showChildren: self.setText(sep.join(item.formatText(True, True, True, True))) else: self.setText(sep.join(item.formatChildText(True, True, True))) if item is not self.oldItem: self.ensureVisible(0, 0) # reset scroll if root changed self.oldItem = item def setSource(self, name): """Called when user clicks on a URL, opens an internal link or an external browser""" name = unescape(unicode(name), treedoc.unEscDict) if name.startswith(u'#'): globalref.docRef.selection.findRefField(name[1:]) elif name.startswith(u'exec:'): if not globalref.options.boolData('EnableExecLinks'): globalref.setStatusBar(_('Executable links are not enabled')) elif sys.platform == 'win32': # windows interprets first quoted text as a title! os.system(u'start "tl exec" %s' % name[5:]) else: os.system(u'%s &' % name[7:]) # remove extra leading slashes else: if sys.platform == 'win32': quoteParts = name.split('"') for i in range(1, len(quoteParts), 2): quoteParts[i] = quoteParts[i].replace(' ', '%20') name = ''.join(quoteParts) else: name = name.replace('\ ', '%20') webbrowser.open(name, True) def showLink(self, text): """Send link text to the statusbar""" text = unescape(unicode(text), treedoc.unEscDict) if text.startswith(u'exec:'): if sys.platform != 'win32': text = u'exec:%s' % text[7:] # remove extra leading slashes globalref.setStatusBar(text) def copyAvail(self): """Return 1 if there is selected text""" return self.hasSelectedText() def cut(self): """Substitute copy for cut command""" self.copy() def pasteText(self, text): """Null op for paste command""" pass def scrollPage(self, numPages=1): """Scrolls down by numPages (negative for up) leaving a one-line overlap""" delta = self.visibleHeight() - self.fontMetrics().height() if delta > 0: self.scrollBy(0, numPages * delta) class DataEditLabel(QLabel): """Upper label with size hint to avoid growth""" def __init__(self, parent=None, name=None): QLabel.__init__(self, parent, name) if qVersion()[0] >= '3': self.setSizePolicy(QSizePolicy(QSizePolicy.Minimum, \ QSizePolicy.Preferred)) def sizeHint(self): """Set prefered size""" return QSize(10, QLabel.sizeHint(self).height()) class DataEditComboItem(QListBoxText): """List box item for combo that shows annoted text but uses regular text for line edit, autocomplete, etc.""" def __init__(self, choiceStr, annotStr): QListBoxText.__init__(self, choiceStr) self.annotStr = annotStr def paint(self, painter): """Paint item using annotated text""" itemHeight = self.height(self.listBox()) fontMet = painter.fontMetrics() yPos = ((itemHeight - fontMet.height()) // 2) + fontMet.ascent() painter.drawText(3, yPos, self.annotStr) def width(self, listBox): """Return width of annotated text""" w = listBox and listBox.fontMetrics().width(self.annotStr) + 6 or 0 return max(w, qApp.globalStrut().height()) class DataEditListBox(QListBox): """List box for combo that does not select spacers""" def __init__(self, parent=None, name=None): QListBox.__init__(self, parent, name) def setCurrentItem(self, item): """Avoid selection of spacers""" if isinstance(item, QListBoxItem) and not item.isSelectable(): return QListBox.setCurrentItem(self, item) def keyPressEvent(self, event): """Bypass spacers for up/down keys""" if event.key() == Qt.Key_Up: item = self.item(self.currentItem()) if item: item = item.prev() while item and not item.isSelectable(): item = item.prev() if item: self.setCurrentItem(item) elif event.key() == Qt.Key_Down: item = self.item(self.currentItem()) if item: item = item.next() while item and not item.isSelectable(): item = item.next() if item: self.setCurrentItem(item) else: QListBox.keyPressEvent(self, event) class DataEditCombo(QComboBox): """Combo box for fields with choices, fills with options when it gets focus""" def __init__(self, key, item, labelRef, stdWidth, parent=None, name=None): QComboBox.__init__(self, True, parent, name) self.key = key self.item = item self.labelRef = labelRef self.setInsertionPolicy(QComboBox.NoInsertion) self.setAutoCompletion(True) self.lineEdit().installEventFilter(self) # filter focus event self.setListBox(DataEditListBox(self)) self.labelFont = labelRef.font() self.labelBoldFont = labelRef.font() self.labelBoldFont.setBold(True) self.format = item.nodeFormat.findField(key) editText, ok = self.format.editText(item) if not ok: self.labelRef.setFont(self.labelBoldFont) self.labelRef.update() self.setEditText(editText) self.setFixedWidth(stdWidth) self.connect(self, SIGNAL('textChanged(const QString&)'), \ self.readChange) def readChange(self, text): """Update variable from edit contents""" # text = unicode(text).strip() # bad results with autocomplete text = unicode(self.currentText()).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 loadListBox(self): """Populate list box for combo""" text = unicode(self.currentText()) if self.format.autoAddChoices: self.format.addChoice(text, True) strList = self.format.getEditChoices(text) self.blockSignals(True) self.listBox().clear() for choice, annot in strList: if choice == None: # separator item = QListBoxText('----------') self.listBox().insertItem(item) item.setSelectable(False) else: self.listBox().insertItem(DataEditComboItem(choice, annot)) try: choices = [choice for (choice, annot) in strList] i = choices.index(text) self.setCurrentItem(i) except ValueError: editText, ok = self.format.storedText(text) if ok and editText: item = QListBoxText('----------') self.listBox().insertItem(item, 0) item.setSelectable(0) self.insertItem(text, 0) # add missing item if valid self.setCurrentItem(0) self.blockSignals(False) def focusInEvent(self, event): """Load combo box when it gains focus""" # self.loadListBox() # didn't work with Qt2 QComboBox.focusInEvent(self, event) def eventFilter(self, object, event): """Check for focus change on line edit""" if object == self.lineEdit() and \ event.type() in (QEvent.FocusIn, QEvent.FocusOut): self.loadListBox() elif object == self.lineEdit() and event.type() == QEvent.KeyPress \ and event.key() == Qt.Key_V and event.state() == Qt.ControlButton: self.editPaste() return True return QComboBox.eventFilter(self, object, event) def popup(self): """Re-Load list box just before showing""" # self.loadListBox() # popup isn't virtual on Qt2 so it doesn't work QComboBox.popup(self) def copyAvail(self): """Return True if there is selected text""" if qVersion()[0] >= '3': return self.lineEdit().hasSelectedText() else: return self.lineEdit().hasMarkedText() def cut(self): """Pass cut command to lineEdit""" self.lineEdit().cut() def copy(self): """Pass copy command to lineEdit""" self.lineEdit().copy() def pasteText(self, text): """Paste text given in param""" self.lineEdit().insert(text) self.readChange(self.currentText()) 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 paste(self): """Override normal paste""" self.editPaste() class DataEditGroup(QGroupBox): """Collection of editors for one item""" def __init__(self, item, approxWidth, showChildren=True, parent=None, \ name=None): QGroupBox.__init__(self, item.nodeFormat.name, parent, name) self.item = item self.showChildren = showChildren layout = QGridLayout(self, 9, 3, 10, 5) layout.addRowSpacing(0, self.fontMetrics().lineSpacing() // 2 + 1) self.titleLabel = DataEditLabel(self) self.titleLabel.setFrameStyle(QFrame.Panel | QFrame.Sunken) self.titleLabel.setTextFormat(Qt.PlainText) self.titleLabel.setText(self.item.title()) self.titleLabel.setLineWidth(2) layout.addMultiCellWidget(self.titleLabel, 1, 1, 0, 2) fieldList = [field for field in item.nodeFormat.fieldList if \ not field.hidden] maxLabelWidth = 0 fontMet = QFontMetrics(self.titleLabel.font()) labels = [] for row, field in enumerate(fieldList): labels.append(QLabel(field.labelName(), self)) layout.addWidget(labels[-1], row + 2, 0) maxLabelWidth = max(maxLabelWidth, fontMet.width(labels[-1].text())) lineWidth = approxWidth - maxLabelWidth - 40 for row, field in enumerate(fieldList): if field.hasEditChoices: line = DataEditCombo(field.name, item, labels[row], \ lineWidth, self) layout.addMultiCellWidget(line, row + 2, row + 2, 1, 2) elif field.hasFileBrowse: line = DataEditLine(field.name, item, labels[row], \ lineWidth - 45, self) layout.addWidget(line, row + 2, 1) browseButton = QPushButton('...', self) browseButton.setFixedWidth(40) self.connect(browseButton, SIGNAL('clicked()'), line.fileBrowse) layout.addWidget(browseButton, row + 2, 2) else: line = DataEditLine(field.name, item, labels[row], \ lineWidth, self) layout.addMultiCellWidget(line, row + 2, row + 2, 1, 2) self.connect(line, PYSIGNAL('entryChanged'), self.checkTitleChange) if not fieldList: self.titleLabel.setFixedWidth(approxWidth - 40) layout.setResizeMode(QLayout.Fixed) def checkTitleChange(self): """Update item title based on signal""" globalref.updateViewTreeItem(self.item, True) self.setTitle(self.item.nodeFormat.name) self.titleLabel.setText(self.item.title()) globalref.updateViewMenuStat() class DataEditView(QScrollView): """Right pane view to edit database info""" groupMargin = 5 groupSpacing = 10 def __init__(self, showChildren=True, parent=None, name=None, flags=0): QScrollView.__init__(self, parent, name, flags) self.showChildren = showChildren self.oldItem = None self.enableClipper(True) self.viewport().setBackgroundMode(QWidget.PaletteBackground) self.dataGroups = [] self.box = None self.grp = None def updateView(self): """Replace contents with selected item data list""" item = globalref.docRef.selection.currentItem if not item: return for group in self.dataGroups: group.close(1) self.resizeContents(500, 50000) self.dataGroups = [] vertPos = DataEditView.groupMargin approxWidth = max(self.parent().parent().width(), 340) \ - 2 * DataEditView.groupMargin \ - 16 # account for scroll bar width ~16 maxWidth = 0 if not self.showChildren: group = DataEditGroup(item, approxWidth, 0, self.viewport()) self.dataGroups.append(group) self.addChild(group, DataEditView.groupMargin, vertPos) group.show() group.adjustSize() vertPos += group.height() + DataEditView.groupSpacing maxWidth = group.width() else: for child in item.childList: group = DataEditGroup(child, approxWidth, 1, self.viewport()) self.dataGroups.append(group) self.addChild(group, DataEditView.groupMargin, vertPos) group.show() group.adjustSize() vertPos += group.height() + DataEditView.groupSpacing maxWidth = max(maxWidth, group.width()) if self.dataGroups: self.resizeContents(maxWidth + 2 * DataEditView.groupMargin, \ vertPos) else: self.resizeContents(DataEditView.groupMargin, \ DataEditView.groupMargin) if item is not self.oldItem: self.ensureVisible(0, 0) # reset scroll if root changed self.oldItem = item def copyAvail(self): """Return 1 if there is selected text""" if hasattr(self.focusWidget(), 'copyAvail'): return self.focusWidget().copyAvail() return 0 def copy(self): """Copy selections to clipboard""" if hasattr(self.focusWidget(), 'copy'): self.focusWidget().copy() def cut(self): """Cut selections to clipboard""" if hasattr(self.focusWidget(), 'cut'): self.focusWidget().cut() def pasteText(self, text): """Paste text given in param""" if hasattr(self.focusWidget(), 'pasteText'): self.focusWidget().pasteText(u' '.join(text.split())) def scrollPage(self, numPages=1): """Scrolls down by numPages (negative for up)""" self.scrollBy(0, numPages * self.visibleHeight()) def viewportResizeEvent(self, event): """Override to redraw contents on resize if width changed not due to scroll bar show/hide""" oldSize = event.oldSize().width() altOldSize = self.verticalScrollBar().isHidden() and \ oldSize + self.verticalScrollBar().width() or \ oldSize - self.verticalScrollBar().width() newSize = event.size().width() if self.isVisible() and oldSize != newSize and altOldSize != newSize: self.updateView() QScrollView.viewportResizeEvent(self, event)