#!/usr/bin/env python #**************************************************************************** # treeview.py, provides classes for the main tree view # # 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. #***************************************************************************** import copy, types, string from treedoc import TreeDoc import globalref from tmpcontrol import TmpEdit from treeitem import TreeItem import treemainwin from qt import Qt, PYSIGNAL, SIGNAL, SLOT, qVersion, QApplication, \ QDragObject, QFocusEvent, QListView, QListViewItem, QPoint, \ QRect, QString, QTextDrag, QTimer, QUriDrag class TreeViewItem(QListViewItem): """Qt tree item, contains ref to treecore TreeItem""" def __init__(self, parent, docItemRef): prev = docItemRef.prevSibling() if prev: prev = prev.viewData QListViewItem.__init__(self, parent, prev) self.docItemRef = docItemRef self.setText(0, docItemRef.title()) self.setIcon() self.setExpandable(docItemRef.numChildren()) docItemRef.viewData = self self.setOpen(docItemRef.open) def setIcon(self): """Set tree node icon""" if globalref.options.boolData('ShowTreeIcons'): icon = globalref.treeIcons.getIcon(self.docItemRef.\ nodeFormat.iconName) if icon: self.setPixmap(0, icon) def setOpen(self, open): """Called to open tree item, reads children if reqd""" currentItem = self.listView().currentTreeItem() if open and not self.childCount(): for child in self.docItemRef.childList: TreeViewItem(child.parent.viewData, child) QListViewItem.setOpen(self, open) # scroll if opening a formerly closed item if open and not self.docItemRef.open and self.childCount(): self.listView().ensureItemVisible(self.docItemRef.\ childList[-1].viewData) self.listView().ensureItemVisible(self) self.docItemRef.open = open if not open and currentItem and self.docItemRef in \ currentItem.ancestorList(): globalref.docRef.selection.change([self.docItemRef]) def setSelected(self, select): """Called when selection changes, updates core select list""" QListViewItem.setSelected(self, select) globalref.docRef.selection.addOrRemove(self.docItemRef, select) class TreeView(QListView): """Left pane view of tree structure""" maxOffset = 250 extCmdList = ('TreeSelectPrev', 'TreeSelectNext', 'TreeOpenItem', \ 'TreeCloseItem', 'TreePrevSibling', 'TreeNextSibling', \ 'TreeSelectParent', 'TreeTop', 'TreeBottom') intCmdList = ('TreePageUp', 'TreePageDown', 'TreeIncremSearch', \ 'TreeIncremNext', 'TreeIncremPrev') def __init__(self, mainWin, parent=None, name=None): QListView.__init__(self, parent, name) self.mainWin = mainWin self.renameEdit = None self.stopRename = False self.renameTimer = QTimer(self) self.connect(self.renameTimer, SIGNAL('timeout()'), self.editRename) self.oldSelItem = None self.setTreeStepSize(globalref.options.intData('IndentOffset', 0, \ TreeView.maxOffset)) self.setSelectionMode(QListView.Extended) self.setRootIsDecorated(True) self.setSorting(-1) self.header().hide() self.addColumn('Main') self.viewport().setMouseTracking(False) self.viewport().setAcceptDrops(True) self.setAcceptDrops(True) self.incremSearchMode = False self.incremSearchStr = '' self.openSelList = [] globalref.updateViewSelection = self.updateSelect globalref.updateViewTree = self.updateTree globalref.updateViewTreeItem = self.updateTreeItem self.connect(self, SIGNAL('currentChanged(QListViewItem*)'), \ self.setCurrent) self.cmdDict = {} for cmd in TreeView.extCmdList: # int() is obsolete for a QKeySequence, but [] not implemented # mask strips high bit set by QKeySequence accelKey = int(self.mainWin.accelKey(cmd)) & 0xfffffff if accelKey: cmdName = "t%s" % cmd[1:] self.cmdDict[accelKey] = lambda g, s, c=cmdName: \ getattr(g.docRef.selection, c) self.incremStopCmds = [] for cmd in TreeView.intCmdList: # int() is obsolete for a QKeySequence, but [] not implemented # mask strips high bit set by QKeySequence accelKey = int(self.mainWin.accelKey(cmd)) & 0xfffffff if accelKey: cmdName = "t%s" % cmd[1:] self.cmdDict[accelKey] = lambda g, s, c=cmdName: getattr(s, c) if cmdName in ('treeIncremNext', 'treeIncremPrev'): self.incremStopCmds.append(self.cmdDict[accelKey]) def updateTree(self): """Replace contents of TreeView from the doc""" if globalref.docRef.treeFormats.hasConditionals: globalref.docRef.root.setDescendantCondTypes() origX, origY = (self.contentsX(), self.contentsY()) # save selection, normal list overwritten with clear select = globalref.docRef.selection[:] self.clear() globalref.docRef.selection.replace([]) # needed in Qt2 item = TreeViewItem(self, globalref.docRef.root) self.blockSignals(True) for node in select: self.setSelected(node.viewData, True) self.blockSignals(False) self.setContentsPos(origX, origY) if select: self.setCurrentItem(select[-1].viewData) self.ensureItemVisible(select[-1].viewData) # currentItem should be set above, but isn't if it is root globalref.docRef.selection.currentItem = self.currentTreeItem() self.triggerUpdate() def updateSelect(self): """Update view selection""" select = globalref.docRef.selection[:] self.blockSignals(True) self.clearSelection() globalref.docRef.selection.replace([]) # needed in Qt2 for node in select: self.setSelected(node.viewData, True) self.blockSignals(False) self.setCurrentItem(select[-1].viewData) self.ensureItemVisible(select[-1].viewData) self.triggerUpdate() self.emit(SIGNAL('selectionChanged()'), ()) def updateTreeItem(self, item, updateCond=False): """Update the title and open status of item""" if item.viewData: if updateCond and globalref.docRef.treeFormats.hasConditionals: item.setConditionalType() item.viewData.setIcon() item.viewData.setText(0, item.title()) if item.open != item.viewData.isOpen(): item.viewData.setOpen(item.open) self.triggerUpdate() def setCurrent(self): """Set current item in selection, called from tree signal""" item = self.currentItem() if item: globalref.docRef.selection.currentItem = item.docItemRef def currentTreeItem(self): """Returns current treecore item""" item = self.currentItem() if item: return item.docItemRef return None def editRename(self): """Rename the selected tree entry""" item = self.currentItem() if not item: return self.ensureItemVisible(item) text = item.text(0) self.renameEdit = TmpEdit(text, self.viewport()) # rect = self.itemRect(item) # doesn't work: bug in itemRect() if scrolled yPos = item.itemPos() - self.contentsY() offset = self.treeStepSize() * item.depth() + self.itemMargin() \ - self.contentsX() if self.rootIsDecorated(): offset += self.treeStepSize() if offset < 0: offset = 0 # rect.setLeft(rect.left() + offset) rect = QRect(offset, yPos, self.visibleWidth() - offset, item.height()) self.renameEdit.setGeometry(rect) self.renameEdit.selectAll() self.renameEdit.show() self.renameEdit.setFocus() rect = self.renameEdit.rect() self.mainWin.disableAllControls() self.connect(self.renameEdit, PYSIGNAL('editDone'), self.endEdit) def endEdit(self): """Hide rename edit view and change tree""" if not self.renameEdit: return item = self.currentTreeItem() text = unicode(self.renameEdit.text()) if item and text and text != item.title(): item.setTitle(text, True) globalref.updateViewTreeItem(item, True) globalref.updateViewSelection() self.mainWin.enableAllControls() self.setFocus() self.renameEdit.close(True) self.renameEdit = None def firstVisibleItem(self): """Return item at top of viewport""" return self.itemAt(QPoint(0, self.firstChild().height() // 2)).\ docItemRef def lastVisibleItem(self): """Return item at bottom of viewport""" item = self.itemAt(QPoint(0, self.visibleHeight() - \ self.firstChild().height() // 2)) if not item: return globalref.docRef.root.lastDescendant(False) return item.docItemRef def treePageUp(self): """Move up one page""" item = self.firstVisibleItem() self.scrollBy(0, -self.visibleHeight()) globalref.docRef.selection.change([item]) def treePageDown(self): """Move down one page""" item = self.lastVisibleItem() self.scrollBy(0, self.visibleHeight()) globalref.docRef.selection.change([item]) def treeIncremSearch(self): """Begin iterative search""" self.incremSearchMode = True self.incremSearchStr = '' globalref.setStatusBar(_('Search for:')) def doIncremSearch(self): """Search for searchStr in all titles""" globalref.setStatusBar(_('Search for: %s') % \ self.incremSearchStr) if globalref.docRef.selection.findTitleText(self.incremSearchStr): globalref.setStatusBar(_('Search for: %s') % \ self.incremSearchStr) else: globalref.setStatusBar(_('Search for: %s (not found)') % \ self.incremSearchStr) def treeIncremNext(self): """Search for next occurance of increm string""" if self.incremSearchStr: if globalref.docRef.selection.findNextTitle(self.incremSearchStr, \ True): globalref.setStatusBar(_('Next: %s') % \ self.incremSearchStr) else: globalref.setStatusBar(_('Next: %s (not found)') % \ self.incremSearchStr) def treeIncremPrev(self): """Search for previous occurance of increm string""" if self.incremSearchStr: if globalref.docRef.selection.findNextTitle(self.incremSearchStr, \ False): globalref.setStatusBar(_('Previous: %s') % \ self.incremSearchStr) else: globalref.setStatusBar(_('Previous: %s (not found)') \ % self.incremSearchStr) def showTypeMenu(self): """Show popup menu for changing the item type""" self.ensureItemVisible(self.currentItem()) rect = self.itemRect(self.currentItem()) pt = self.mapToGlobal(QPoint(rect.center().x(), rect.bottom())) self.mainWin.typeSubMenu.popup(pt) def itemAtMouse(self, pt): """Return view item at pt""" clickedItem = self.itemAt(self.contentsToViewport(pt)) if clickedItem: offset = self.treeStepSize() * clickedItem.depth() + \ self.itemMargin() if self.rootIsDecorated(): offset += self.treeStepSize() if pt.x() <= offset: return None return clickedItem def contentsMousePressEvent(self, event): """Mouse press down event saves selected item for rename""" clickedItem = self.itemAtMouse(event.pos()) if event.button() == Qt.LeftButton and clickedItem and \ self.currentItem().isSelected(): self.oldSelItem = self.currentItem() else: self.oldSelItem = None if event.button() == Qt.RightButton and not clickedItem: return # skip unselecting right click if self.incremSearchMode: self.incremSearchMode = False globalref.setStatusBar('') QListView.contentsMousePressEvent(self, event) def contentsMouseReleaseEvent(self, event): """Mouse release event for rename and popup menus""" clickedItem = self.itemAtMouse(event.pos()) if clickedItem: if event.button() == Qt.RightButton and \ not self.renameTimer.isActive(): if clickedItem.docItemRef.numChildren(): popup = self.mainWin.parentPopup else: popup = self.mainWin.childPopup pt = self.mapToGlobal(self.contentsToViewport(event.pos())) popup.popup(pt) elif globalref.options.boolData('ClickRename') and \ event.state() == Qt.LeftButton and clickedItem == \ self.oldSelItem and not self.stopRename: self.renameTimer.start(1000, True) QListView.contentsMouseReleaseEvent(self, event) self.stopRename = False def contentsMouseDoubleClickEvent(self, event): """Mouse double click event, stops rename""" self.renameTimer.stop() self.stopRename = True if self.incremSearchMode: self.incremSearchMode = False globalref.setStatusBar('') QListView.contentsMouseDoubleClickEvent(self, event) def focusInEvent(self, event): """Stop rename on focus click""" if event.reason() == QFocusEvent.Mouse: self.stopRename = True QListView.focusInEvent(self, event) def focusOutEvent(self, event): """Stop incremental search on focus loss""" if self.incremSearchMode: self.incremSearchMode = False globalref.setStatusBar('') QListView.focusOutEvent(self, event) def keyPressEvent(self, event): """Bind keys to functions""" keyText = str(event.text()) combinedKey = event.key() if event.state() & Qt.ShiftButton: combinedKey += Qt.SHIFT if event.state() & Qt.ControlButton: combinedKey += Qt.CTRL if event.state() & Qt.AltButton: combinedKey += Qt.ALT # if event.state() & Qt.MetaButton: # combinedKey += Qt.Meta cmd = self.cmdDict.get(combinedKey, '') if self.incremSearchMode: if event.key() in (Qt.Key_Return, Qt.Key_Enter, Qt.Key_Escape): self.incremSearchMode = False globalref.setStatusBar('') elif event.key() == Qt.Key_Backspace and self.incremSearchStr: self.incremSearchStr = self.incremSearchStr[:-1] self.doIncremSearch() elif cmd in self.incremStopCmds: self.incremSearchMode = False globalref.setStatusBar('') cmd(globalref, self)() elif keyText and keyText in string.printable: self.incremSearchStr += keyText self.doIncremSearch() event.accept() return if cmd: cmd(globalref, self)() event.accept() elif keyText in string.lowercase: globalref.docRef.selection.letterSearch(keyText.upper(), True) event.accept() elif keyText in string.uppercase: globalref.docRef.selection.letterSearch(keyText, False) event.accept() elif (combinedKey == Qt.Key_Return or combinedKey == Qt.Key_Enter) and \ globalref.options.boolData('InsertOnEnter'): if self.currentTreeItem().parent: self.mainWin.editInAfter() else: self.mainWin.editAddChild() event.accept() # else: # QListView.keyPressEvent(self, event) def contentsMouseMoveEvent(self, event): """Move event for drag & drop""" if self.renameEdit: return copyFormat = treemainwin.TreeMainWin.copyFormat if self.itemAtMouse(event.pos()) and globalref.docRef.selection: if len(globalref.docRef.selection) > 1: globalref.docRef.treeFormats.addIfMissing(treemainwin.\ TreeMainWin.copyFormat) item = TreeItem(None, copyFormat) for node in globalref.docRef.selection: item.childList.append(copy.copy(node)) item.childList[-1].parent = item oldList = globalref.docRef.selection[:] else: item = globalref.docRef.selection[0] oldList = [item] oldCurrent = self.currentTreeItem() dragObj = QTextDrag(u'\n'.\ join(item.branchXml([copyFormat])), self) globalref.docRef.treeFormats.removeQuiet(treemainwin.TreeMainWin.\ copyFormat) # check for move & can't move to descendant (or to self) if dragObj.drag(QDragObject.DragDefault) \ and dragObj.target() == self.viewport() and \ self.currentTreeItem() and not \ filter(None, [node.hasDescendant(self.currentTreeItem()) \ for node in oldList]): for item in oldList: item.delete() globalref.updateViewAll() elif not self.currentTreeItem(): self.setCurrentItem(oldCurrent.viewData) def contentsDragEnterEvent(self, event): """Starting drag event""" event.accept(QTextDrag.canDecode(event) and \ globalref.options.boolData('DragTree')) def contentsDropEvent(self, event): """End drag & drop""" item = self.itemAtMouse(event.pos()) oldList = globalref.docRef.selection[:] text = QString('') copyFormat = treemainwin.TreeMainWin.copyFormat if globalref.options.boolData('DragTree') and \ QTextDrag.decode(event, text) and item and item.docItemRef not in \ globalref.docRef.selection: newItem = globalref.docRef.readXmlString(unicode(text), True) if newItem: if newItem.data: itemList = [newItem] else: itemList = newItem.childList parent = item.docItemRef undoParents = [parent] + filter(None, \ [old.parent for old in oldList]) globalref.docRef.undoStore.addChildListUndo(undoParents) for node in itemList: parent.addTree(node) parent.open = True globalref.docRef.treeFormats.removeQuiet(treemainwin.\ TreeMainWin.copyFormat) globalref.docRef.selection.replace(itemList) if qVersion()[0] >= '3': event.acceptAction() globalref.updateViewAll() else: print 'Error reading XML string' elif QUriDrag.canDecode(event): self.mainWin.dropEvent(event)