# -*- coding: iso-8859-1 -*- # This software and supporting documentation are distributed by # Institut Federatif de Recherche 49 # CEA/NeuroSpin, Batiment 145, # 91191 Gif-sur-Yvette cedex # France # # This software is governed by the CeCILL license version 2 under # French law and abiding by the rules of distribution of free software. # You can use, modify and/or redistribute the software under the # terms of the CeCILL license version 2 as circulated by CEA, CNRS # and INRIA at the following URL "http://www.cecill.info". # # As a counterpart to the access to the source code and rights to copy, # modify and redistribute granted by the license, users are provided only # with a limited warranty and the software's author, the holder of the # economic rights, and the successive licensors have only limited # liability. # # In this respect, the user's attention is drawn to the risks associated # with loading, using, modifying and/or developing or reproducing the # software by the user in light of its specific status of free software, # that may mean that it is complicated to manipulate, and that also # therefore means that it is reserved for developers and experienced # professionals having in-depth computer knowledge. Users are therefore # encouraged to load and test the software's suitability as regards their # requirements in conditions enabling the security of their systems and/or # data to be ensured and, more generally, to use and operate it in the # same conditions as regards security. # # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license version 2 and that you accept its terms. ''' This module provides widgets L{EditableTreeWidget} L{TreeListWidget} and L{ObservableListWidget} for L{EditableTree}, L{ObservableSortedDictionary of EditableTree} and L{ObservableList} model objects. These widgets register callback methods to update when the model changes. They provide user interaction to modify the underlying model (drag&drop, contextual menu...) @author: Dominique Geffroy @organization: U{NeuroSpin<http://www.neurospin.org>} and U{IFR 49<http://www.ifr49.org>} @license: U{CeCILL version 2<http://www.cecill.info/licences/Licence_CeCILL_V2-en.html>} ''' __docformat__ = "epytext en" import os from StringIO import StringIO from PyQt4.QtGui import QTreeWidget, QTreeWidgetItem, QListWidget, QListWidgetItem, QPixmap, QDrag, QMenu, QPainter, QPen, QCursor, QSizePolicy, QIcon, qApp, QKeyEvent, QApplication from PyQt4.QtCore import Qt, QEvent, QMimeData, QObject, QPoint, QRect, QSize, QTimer, SIGNAL import copy from soma.notification import ObservableList, EditableTree from soma.minf.api import defaultReducer, createMinfWriter, iterateMinf, minfFormat from soma.wip.application.api import findIconFile from soma.qt4gui.api import defaultIconSize #---------------------------------------------------------------------------- class EditableTreeWidget(QTreeWidget): """A widget to represent a tree represented by an EditableTree object. The tree can be modifiable by manipulating items in the widget : - add new item - copy an item in a branch - move an item in a branch - delete an item It is possible to copy items from one tree widget to another. This component is created with a EditableTree object which is the model. The graphic component has a reference on the model and methods to control events. It registers a callback method on the model notifier. On drag&drop of an item in another, the model is updated and notify all its listeners, so the view is updated via updateContent method. The widgets treats some events : - drag&drop events : to copy or move items - key events : del to remove an item, shift to set move mode - context menu request event: show a popup menu to create new item - rename event on modifiable items. All items icon are resized to have the same size. Inner classes : - Item - Branch - Leaf @type MARGIN: int @cvar MARGIN: margin width in pixel around items @type model: EditableTree @ivar model: The tree which this widget is the representation @type controller: EditableTreeController @ivar controller: the controller is called to proceed model's changes on events. @type draggedItems: list of EditableTree.Item @ivar draggedItems: list of currently dragged items. Items can come from current tree or from another tree (so it is a copy of the other tree's item). @type draggedItemsParent: list of EditableTree.Item @ivar draggedItemsParent: list of currently dragged items' parents (if they are in current tree). Usefull for delete and move functionalities. @type dropOn: EditableTree or Item @ivar dropOn: draggedItem is currently on this container @type dropAfter: Item @ivar dropAfter: draggedItem is currently after this item @type dropBefore: Item @ivar dropBefore: draggedItem is currently before this item @type popupMenu: QPopupMenu @ivar popupMenu: contextual menu shown on mouse right click @type contextMenuTarget: EdtiableTree.Item or EditableTree @ivar contextMenuTarget: item on which a context menu is opened """ # margin in pixel around the items # it is used for the highlighting of the dropzones MARGIN=3 def __init__(self, treeModel, parent=None, iconSize=defaultIconSize): """ @type treeModel: EditableTree @param treeModel: the tree which current widget represents. @type parent:QWidget @param parent: parent of this widget @type iconSize: couple of integers or None @param iconSize: force items icon resizing. """ QTreeWidget.__init__( self, parent) self.setColumnCount(1) self.setColumnWidth(0, self.width()) self.setRootIsDecorated(True) #self.setItemMargin(self.MARGIN) #self.setSortingEnabled(False) # disable sort self.setSizePolicy( QSizePolicy( QSizePolicy.Preferred, QSizePolicy.Preferred ) ) self.setIconSize(QSize(*iconSize)) self.iconDim=iconSize # enable several items selection using ctrl and shift keys. self.setSelectionMode(QTreeWidget.ExtendedSelection) self.setAcceptDrops(True) # draggedItems attribute stores the items which are currently dragged self.draggedItems=[] self.draggedItemParent=[] # and their parents in current tree self.dragStartPosition=0 # current drop zones (used in drawContentsOffset to highlight current dropzone) self.dropOn=None # dragged item will be dropped in this item's children self.dropAfter=None # dragged item will be dropped after this item (sibling) self.dropBefore=None # dragged item will be dropped before this item # Popup Menu self.popupMenu = QMenu(self) self.popupMenu.addAction( "New", self.menuNewItemEvent ) self.contextMenuTarget=None self.setContextMenuPolicy(Qt.CustomContextMenu) # keep a reference to the model for control event and register a listener to be aware of changes self.setModel(treeModel) self.connect(self, SIGNAL("itemChanged( QTreeWidgetItem *, int )"), self.itemRenamed) self.connect(self, SIGNAL( 'customContextMenuRequested ( const QPoint & )'), self.openContextMenu) def setModel(self, m): """Construct the children of the tree reading the model given. A listener is added to the model, so when the model change, the method self.updateContents is automatically called. """ self.clear() self.model=m self.controller=EditableTreeController(m) if m!=None: self.setWindowTitle(m.name) self.setHeaderLabel(m.name) self.setAcceptDrops(m.modifiable) self.model.addListener(self.updateContent) self.model.onAttributeChange("name", self.updateName) # create child items with data in the tree model lastChild=None for item in self.model.values(): if item.isLeaf(): # append item to keep the same order as in the model lastChild=EditableTreeWidget.Leaf(self, item, lastChild, self.iconDim) else: lastChild=EditableTreeWidget.Branch(self, item, lastChild, self.iconDim) def itemRenamed(self, item, col): if item is not None and hasattr(item, "okRename"): item.okRename(col) def getChild(self, n): """Return the nth child item If n<=0 return the first child, if n>=nb children, return the last child @rtype: EditableTreeWidget.Item """ if n<=0: n=0 else: nbItems=self.topLevelItemCount() if n>=nbItems: n=nbItems-1 return self.topLevelItem(n) def getLastChild(self): """Return the last child item. @rtype: EditableTreeWidget.Item """ return self.topLevelItem(self.topLevelItemCount()-1) def toContentsPoint(self, point): """Translates coordinates in the frame in coordinates in the content of the list (without header). @rtype: QPoint""" #viewportPoint=self.contentsToViewport(point) to get the item at position we need the position in the entire list not in the visible content (viewport) return point # - QPoint(0, self.header().height()) # headerItem().sizeHint().height() ? # already implemented in QTreeWidget #def selectedItems(self): #""" #Gets items that are currently selected in the listview (as we are in extended selection mode). #@rtype: list of EditableTreeWidget.Item #@return: items currently selected #""" #items=[] #it = QListViewItemIterator(self, QListViewItemIterator.Selected) #while it.current() : #items.append( it.current() ) #it+=1 #return items #------ Drag&Drop ------ def mousePressEvent(self, event): if (event.button() == Qt.LeftButton): self.dragStartPosition = QPoint(event.pos()) QTreeWidget.mousePressEvent(self, event) def mouseMoveEvent(self, event): """ The QDrag object is shown during the drag. This method is called when the mouse moves, this can be the beginning of a drag if the left button is clicked and the distance between the current position and the dragStartPosition is sufficient. draggedItems attribute is set with selected item's model. It constructs a minf (xml) representation of drag objects which will be provided to the target drop zone. """ if (not (event.buttons() & Qt.LeftButton)): return if ((event.pos() - self.dragStartPosition).manhattanLength() < QApplication.startDragDistance()): return items=self.selectedItems() # keep a reference to the current dragged item d=None if items != []: self.draggedItems=[] self.draggedItemsParent=[] for item in items: self.draggedItems.append(item.model) self.draggedItemsParent.append(item.container().model) #create a string containing the minf representation of items itemMinfBuf=StringIO() # find minf format to use for item's class firstItem=items[0].model reducer = defaultReducer( firstItem ) # supposing that all items needs the same reducer itemMinf="" if reducer != None: writer = createMinfWriter( itemMinfBuf, "XML", reducer ) for item in items: writer.write( item.model ) itemMinf=itemMinfBuf.getvalue() writer.close() #else: itemMinf=item.name # create a QTextDrag object with mime type text/xml containing the minf string d=QDrag(self) icon = findIconFile( firstItem.icon ) if icon is not None: # adding an icon which will be visible on drag move, it will be the first item's icon d.setPixmap(QPixmap(icon)) mimeData = QMimeData() mimeData.setText(itemMinf) d.setMimeData(mimeData); dropAction = d.exec_(); #QTreeWidget.mouseMoveEvent(self, event) def dragEnterEvent(self, event): """This method is called when a drag enter in the widget. The source of the drag can be this widget or antoher. The event contains the encoding of the drag object. If the event's source is the current window, the event is always accepted. Else, try to decode text event as minf representation of an item and set self.draggedItem. """ # if source of event is this widget, draggedItem is already set. if event.source()==self: event.accept() else: #must decode the event. If it is minf format, read it and initialize draggedItem self.draggedItems=[] self.draggedItemsParent=[] if event.mimeData().hasText(): textEvent=event.mimeData().text() textEventBuf=StringIO(textEvent) # check if it is the expected minf format format, reduction=minfFormat(textEventBuf) if format=="XML": textEventBuf.seek(0) for value in iterateMinf( textEventBuf ): if isinstance(value, EditableTree.Item): event.accept() self.draggedItems.append(value) else: event.ignore() textEventBuf.close() else: event.ignore() else: event.ignore() def dragLeaveEvent(self, event): """The drag leave the widget, there's no more dropzones to highlight.""" self.dropOn=None self.dropAfter=None self.dropBefore=None self.viewport().update() def dragMoveEvent(self, event): """When the drag moves, the current dropzone can change. According to cursor position (on, before, after an item), attributes that store current dropzone are updated (dropBefore, dropAfter, dropOn). Method update is called in order to refresh painting with dropzone highlighting. """ # draggedItem must be set, otherwise it isn't a correct drag dropzonesChanged=False if self.draggedItems==[]: event.ignore(self.contentsRect()) dropzonesChanged=self.setDropzones(None, None, None) else: cursorPos=self.toContentsPoint(event.pos()) currentItem=self.itemAt(cursorPos) #item under the mouse cursor if currentItem!=None: #cursor on an item currentItemContainer=currentItem.container() rect = self.visualItemRect(currentItem) if currentItem.model not in self.draggedItems: #the item under the mouse isn't one of dragged items # if the cursor is in the top margin of item, insert before item if (cursorPos.y() < rect.top()+self.MARGIN): if currentItemContainer.acceptDrops(): # container must accept drop # in theory, it is possible to pass to accept method a rectangle for which the event is accepted, # in order to speed up the process event but this doesn't work : # when using this hint, dropzones highlighting is not always updated... #event.accept(QRect(rect.left(), rect.top(), rect.width(), self.MARGIN)) event.accept() dropzonesChanged=self.setDropzones(currentItemContainer, currentItem, None) else: #event.ignore(QRect(rect.left(), rect.top(), rect.width(), self.MARGIN)) event.ignore() dropzonesChanged=self.setDropzones(None, None, None) #if the cursor is in the bottom margin of the item -> drop after this item elif (cursorPos.y()>rect.bottom()-self.MARGIN): if currentItemContainer.acceptDrops(): #event.accept(QRect(rect.left(), rect.bottom()-self.MARGIN, rect.width(), self.MARGIN)) event.accept() dropzonesChanged=self.setDropzones(currentItemContainer, None, currentItem) else: #event.ignore(QRect(rect.left(), rect.bottom()-self.MARGIN, rect.width(), self.MARGIN)) event.ignore() dropzonesChanged=self.setDropzones(None, None, None) # else if the cursor is on an item which accept drop elif currentItem.acceptDrops(): #event.accept(QRect(rect.left(), rect.top()+self.MARGIN, rect.width(), rect.height()-2*self.MARGIN)) event.accept() dropzonesChanged=self.setDropzones(currentItem, None, None) else: #event.ignore(QRect(rect.left(), rect.top()+self.MARGIN, rect.width(), rect.height()-2*self.MARGIN)) event.ignore() dropzonesChanged=self.setDropzones(None, None, None) else: # item dragged on itself : ignore the event #event.ignore(rect) event.ignore() dropzonesChanged=self.setDropzones(None, None, None) elif self.acceptDrops(): #if the cursor is not on an item, the dropzone is the listview (at the end of the list) event.accept() dropzonesChanged=self.setDropzones(self, None, self.getLastChild()) else: event.ignore() dropzonesChanged=self.setDropzones(None, None, None) if dropzonesChanged: self.viewport().update() def setDropzones(self, dropOn, dropBefore, dropAfter): """Sets dropOn, dropBefore and dropAfter attributes with parameter values. Return true if the dropzones have changed.""" change=(self.dropOn != dropOn) or (self.dropBefore != dropBefore) or (self.dropAfter != dropAfter) if change: self.dropOn=dropOn self.dropBefore=dropBefore self.dropAfter=dropAfter return change def dropEvent( self, event): """When an item is dropped in another, a deep copy of the item is added in the dropzone item""" # current dropzone is stored in dropOn if self.dropOn!=None: dropAfterModel=None dropBeforeModel=None if self.dropAfter!=None: dropAfterModel=self.dropAfter.model if self.dropBefore!=None: dropBeforeModel=self.dropBefore.model self.controller.drop(event.source(), self, self.draggedItems, self.draggedItemsParent, self.dropOn.model, dropAfterModel, dropBeforeModel, ((event.keyboardModifiers() & Qt.ControlModifier) == Qt.ControlModifier) ) self.draggedItems = [] self.draggedItemsParent=[] self.dropOn=None self.dropAfter=None self.dropBefore=None self.viewport().update() def deleteDraggedItems(self): """Called when the item have been moved in another tree widget (drop+moveMode)""" i=0 for draggedItem in self.draggedItems: self.controller.delete(draggedItem, self.draggedItemsParent[i]) i+=1 self.draggedItems = [] self.draggedItemsParent=[] #------ Key events ------ def keyPressEvent(self, event): """ This method is called when a key is pressed during a few seconds or when a key is released If the user keep the key pressed, the event occurs however; same as pressing several times the key. It doesn't seems to be parametrable... If the key pressed is delete, the selected items are deleted. """ if (event.key() == Qt.Key_Delete): # delete current selected item items = self.selectedItems() for item in items: self.controller.delete(item.model, item.container().model) else: event.ignore() # the event could be handled by a parent component #------ context menu events ------ def openContextMenu(self, point): """On right click on the mouse, a context menu opens. With this menu, it is possible to create a new branch. """ self.contextMenuTarget=None cursorPos=self.toContentsPoint(point) currentItem=self.itemAt(cursorPos) #item under the mouse cursor accept=False if currentItem is not None: if currentItem.model.modifiable and not currentItem.model.isLeaf(): self.contextMenuTarget=currentItem.model accept=True else: if self.model is not None and self.model.modifiable: self.contextMenuTarget=self.model accept=True #items=self.selectedItems() #if len(items)==1: #if items[0].model.modifiable: #event.accept() #else: event.ignore() #elif self.model!=None and self.model.modifiable: #event.accept() #else: event.ignore() if accept: self.popupMenu.exec_(QCursor.pos()) def menuNewItemEvent(self): """ Called when user selects New in context menu in order to create a new item in the tree. """ #items=self.selectedItems() #if len(items) == 1: #target=items[0].model #else: target=self.model #self.controller.newItem(target) self.controller.newItem(self.contextMenuTarget) #------ Update on model changes ------ def updateContent(self, action, items, position=None): """This method is called when the model notifies a change : The action has been done at position, with items. The view should update its content to reflect the changes. """ if action==ObservableList.INSERT_ACTION: self.insert(items, position) elif action==ObservableList.REMOVE_ACTION: self.remove(items, position) elif action==ObservableList.MODIFY_ACTION: # some model items have been modified, # widget items are replaced by new items with new model items # from position, all items must be replaced with new value # ->remove and then insert new i=position+1 for modelItem in items: self.takeTopLevelItem(i) i+=1 self.insert(items, position) #else: print action, "unknown action" def updateName(self, newName): self.setWindowTitle(newName) self.setHeaderLabel(newName) def insert(self, items, position=None): """Insertion of items at position in the model -> create view items for all items and insert them at position in self. Inserted item becomes the selected item.""" #insert at position = insert before item at position = insert after item at position-1 for item in items: if item.isLeaf(): # append item to keep the same order as in the model itemBefore=EditableTreeWidget.Leaf(None, item, None, self.iconDim) else: itemBefore=EditableTreeWidget.Branch(None, item, None, self.iconDim) self.insertTopLevelItem(position, itemBefore) if item.unamed and self.hasFocus(): self.editItem(itemBefore, 0) item.unamed=False #print "current item : ", self.currentItem() #print "set current item : ", itemBefore, itemBefore.text(0) self.setCurrentItem(itemBefore, 0) itemBefore.setSelected(True) def remove(self, items, position): """Removes items in the list from position. If the list is empty, one item is removed. If position is undefined, removes items whose model is in the list""" if position is None: position=0 if len(items)==0: self.takeTopLevelItem(position) else: for modelItem in items: found=False i=0 while not found and i<self.topLevelItemCount(): item=self.topLevelItem(i) if item.model is modelItem: self.takeTopLevelItem(i) found=True i+=1 #------ Refresh painting ------ def paintEvent(self, event): """This method is called to paint the tree. To refresh the view, call self.update. It is redefined in order to highlight drop zone during the drag of an item Three types of dropzone are defined : - before an item (cursor is in the top of the item's rectangle) -> draw a line on top - after an item (cursor is in the bottom of the item's rectangle) -> draw a line on bottom - on an item -> draw a rectangle around item """ QTreeWidget.paintEvent(self, event) painter=QPainter() painter.begin(self.viewport()) if self.dropBefore!=None: # draw a line in the top of the item rect=self.visualItemRect(self.dropBefore) # offset of the item = depth * offset in relation to parent #offset=self.indentation() painter.setBrush(Qt.black) # the line has the same offset as the item painter.drawRect(rect.left(), rect.top(), rect.width(), self.MARGIN) elif self.dropAfter!=None: # draw a line in the bottom of the item and its visible content rect=self.visualItemRect(self.dropAfter) # this rect only contains the item, not its content # inc the height to include item's content #rect.setHeight( (min( self.dropAfter.totalHeight(), self.viewport().height() - rect.y() ) ) ) # stay in the viewport to keep the line visible #offset=self.indentation() painter.setBrush(Qt.black) #print "draw drop after", self.dropAfter.getText(), rect.left(), rect.top(), rect.width(), rect.height() painter.drawRect(rect.left(), rect.bottom()-self.MARGIN+1, rect.width(), self.MARGIN) elif self.dropOn!=None: #draw the rectangle containing the item if self.dropOn!=self: rect=self.visualItemRect(self.dropOn) else: #ajout dans le listview alors qu'il est vide rect=QRect(0, 0, self.columnWidth(0), self.MARGIN) #print "draw drop on", self.dropOn.getText(), rect.left(), rect.top(), rect.width(), rect.height() painter.setPen(QPen(Qt.black, self.MARGIN, Qt.SolidLine)) painter.drawRect(rect) painter.end() #---------------------------------------------------------------------------- class Item(QTreeWidgetItem): """Item is the base class for elements of EditableTreeWidget. """ def __init__( self, parent, treeItemModel, after=None, iconSize=defaultIconSize): """ @type parent: tree or branch @param parent: container of the item @type treeItemModel: EditableTree.Item @param treeItemModel: model which this widget is the representation @type after: item @param after: the item after which current item must be added in the parent """ QTreeWidgetItem.__init__( self, parent, [treeItemModel.name] ) if treeItemModel.icon: iconPath = findIconFile( treeItemModel.icon ) if iconPath is not None: self.setIcon(0, QIcon(iconPath)) if not treeItemModel.copyEnabled: self.setFlags(self.flags() & ~Qt.ItemIsDragEnabled) if not treeItemModel.modifiable: self.setFlags(self.flags() & ~Qt.ItemIsDropEnabled) # rename is enabled only if the item is modifiable if treeItemModel.modifiable: self.setFlags(self.flags() | Qt.ItemIsEditable) treeItemModel.onAttributeChange("name", self.updateName) treeItemModel.onAttributeChange("visible", self.updateVisibiliy) self.setHidden(not treeItemModel.visible) self.setDisabled(not treeItemModel.enabled) self.model=treeItemModel def acceptDrops(self): return ((self.flags() & Qt.ItemIsDropEnabled) == Qt.ItemIsDropEnabled) def getText(self): return self.text(0) def container(self): """Return the parent item or the listview that contains the element if it is a top level element @rtype: EditableTreeWidget.Branch or EditableTreeWidget""" parent = self.parent() if parent is None: parent = self.treeWidget() return parent def okRename(self, col): """It is the called associated to the signal QTreeWidget.itemChanged(item, col). It is called when user renames the item. The name must be changed in the model.""" if getattr(self, "model", None) is not None: newText=unicode(self.getText()) if self.model.name != newText: if self.model.name == self.model.tooltip: self.model.tooltip=newText self.model.name=newText def updateName(self, newName): """ Called when the model notifies that its name attribute value has changed. """ self.setText(0, newName) def updateVisibiliy(self, newValue): """ Called when the model notifies that its visible attribute value has changed. """ #pass self.setHidden(not newValue) #self.setText(0, self.getText()+"-hid") #print "change visibility to ", newValue, " for ", self.getText() #---------------------------------------------------------------------------- class Branch( Item ): """Item that represents a tree branch. It can have children.""" def __init__( self, parent, treeItemModel, after=None, iconSize=defaultIconSize): EditableTreeWidget.Item.__init__(self, parent, treeItemModel, after, iconSize) self.iconDim=iconSize treeItemModel.addListener(self.updateContent) #create child items lastChild=None for item in treeItemModel.values(): if item.isLeaf(): lastChild=EditableTreeWidget.Leaf(self, item, lastChild, iconSize) else: lastChild=EditableTreeWidget.Branch(self, item, lastChild, iconSize) def getChild(self, n): """Return the nth child item If n<=0 return the first child, if n>=nb children, return the last child @rtype: EditableTreeWidget.Item """ if n<0: n=0 elif n>=self.childCount(): n=self.childCount()-1 return self.child(n) #------ Update on model changes ------ def updateContent(self, action, items, position=None): """This method is called when the model notifies a change : The action has been done at position, with items. The view should update its content to reflect the changes""" if action==ObservableList.INSERT_ACTION: self.insert(items, position) elif action==ObservableList.REMOVE_ACTION: self.remove(items, position) elif action==ObservableList.MODIFY_ACTION: # some model items have been modified, # widget items are replaced by new items with new model items # from position, all items must be replaced with new value #remove and then insert new for modelItem in items: self.takeChild(position) # when an item is removed, position becomes the index of the next item self.insert(items, position) #else: print "unknown action" #print str(self.listView()) def insert(self, items, position): """Insertion of items at position in the model -> create view items for all items and insert them at position in self insert at position = insert before item at position = insert after item at position-1""" i=position for item in items: if item.isLeaf(): # append item to keep the same order as in the model newItem=EditableTreeWidget.Leaf(None, item, None, self.iconDim) else: newItem=EditableTreeWidget.Branch(None, item, None, self.iconDim) self.insertChild(i, newItem) i+=1 if item.unamed and self.treeWidget().hasFocus(): self.editItem(newItem, 0) item.unamed=False self.setExpanded(True) #print "current item : ", self.treeWidget().currentItem() #print "set current item : ", newItem, newItem.text(0) self.treeWidget().setCurrentItem(newItem, 0) newItem.setSelected(True) def remove(self, items, position): """Remove items in the list from position if the list is empty, remove one item if position is undefined, remove items whose model is in the list""" if position is None: position=0 if len(items)==0: self.takeChild(position) else: for modelItem in items: found=False i=position while not found and i<self.childCount(): item=self.child(i) if item.model is modelItem: self.takeChild(i) found=True else: # if we remove an item, the index stay the same, no need to increment it i+=1 #---------------------------------------------------------------------------- class Leaf( Item ): """Item that represents a tree leaf. It doesn't have children.""" def __init__( self, parent, treeItemModel, after=None, iconSize=defaultIconSize): EditableTreeWidget.Item.__init__(self, parent, treeItemModel, after, iconSize) #self.setExpandable(False) #self.setDropEnabled(False) self.setFlags( self.flags() & ~Qt.ItemIsDropEnabled ) # This method could be used to change text color of items : #def paintCell(self, painter, colorGroup, column, width, alignment ): #cg=qt.QColorGroup(colorGroup) #c=qt.QColor(cg.text()) #cg.setColor( qt.QColorGroup.Text, Qt.red) #QListViewItem.paintCell(self, painter, cg, column, width, alignment ) #cg.setColor( qt.QColorGroup.Text, c ) #---------------------------------------------------------------------------- class EditableTreeController: """The controller is called to make changes on the model when events occur.""" def __init__(self, m): self.model=m def drop(self, sourceTreeWidget, targetTreeWidget, draggedItems, draggedItemsParent, dropOn, dropAfter, dropBefore, copyMode): """ Called when an item is dropped. It is copied or moved in target model. The copy of item will be modifiable even if source item is not. @type sourceTreeWidget: EditableTreeWidget @param sourceTreeWidget: the widget which dragged item comes from @type targetTreeWidget: EditableTreeWidget @param targetTreeWidget: the widget in which dragged item is dropped @type draggedItems: list of EditableTree.Item @param draggedItems: items to move or copy @type draggedItemsParent: list of EditableTree.Branch or EditableTree @param draggedItemsParent: container of dragged items @type dropOn: EditableTree.Branch or EditableTree @param dropOn: container in which item is dropped @type dropAfter: EditableTree.Item @param dropAfter: item after which dragged item is dropped @type dropBefore: EditableTree.Item @param dropBefore: item before which dragged item is dropped """ insertedItem=None insertIndex=None if sourceTreeWidget==targetTreeWidget: # inner drop i=0 for draggedItem in draggedItems: if copyMode: # if ctrl key is pressed, copy item, else move insertedItem, insertIndex=self.copy(draggedItem, dropOn, dropAfter, dropBefore, insertIndex) else: insertedItem, insertIndex=self.move(draggedItem, draggedItemsParent[i], dropOn, dropAfter, dropBefore, insertIndex) i+=1 if insertedItem!=None: dropAfter=insertedItem dropBefore=None if insertIndex is not None: insertIndex+=1 else: # drop from one widget to another #print "drop on ", targetTreeWidget if not copyMode: sourceTreeWidget.deleteDraggedItems() for draggedItem in draggedItems: draggedItem.setAllModificationsEnabled(True) insertedItem, insertIndex=self.insert(draggedItem, dropOn, dropAfter, dropBefore, insertIndex) if insertedItem is not None: dropAfter=insertedItem dropBefore=None if insertIndex is not None: insertIndex+=1 def move(self, item, source, target, after, before, index): """Move an element of the tree from source to target.""" insertedItem=None insertIndex=index if not target.isDescendant(item): # can't move an item in its descendant # the item is deleted from its parent if source == target: # insertion index can change if the item is moved inside its container index=None self.delete(item, source) if item.copyEnabled and target.modifiable and not target.has_key(item.id): # if the item was not deletable, it is already in the target and can't be moved. itemCopy = copy.deepcopy(item) # add this new item in the target item insertedItem, insertIndex=self.insert(itemCopy, target, after, before, index) return (insertedItem, insertIndex) def copy(self, item, target, after, before, index): """Recursive copy of an item in another item. A copy is completly modifiable, even if the source is not.""" # create a deepcopy of the dragged item # notifiers are not copied, view items will be removed so they shouldn't be notified of changes in the new item insertedItem=None insertIndex=index if item.copyEnabled and target.modifiable: itemCopy = copy.deepcopy(item) # a copy is completely modifiable, even if the source is not itemCopy.setAllModificationsEnabled(True) # add this new item in the target item insertedItem, insertIndex=self.insert(itemCopy, target, after, before, index) #print self.model return (insertedItem, insertIndex) def insert(self, item, target, after, before, index): """Insert item in target, eventually after or before an existing child.""" insertedItem=item insertIndex=index if not target.has_key(item.id): # can't copy an item that is already in the target if index is not None: target.insert(index, item.id, item) else: if before!=None: insertIndex=target.index(before.id) target.insert(insertIndex, item.id, item) # inserer le suivant en i+1 elif after!=None: # insert the item after item "after" insertIndex=target.index(after.id)+1 target.insert(insertIndex, item.id, item) # inserer le suivant en i+2 else: # if no position, append the item #print "model append", self.model.onChangeNotifier._listeners target[item.id]=item # rien a preciser on ajoute a la fin else: newItem=target[item.id] if newItem is not after and newItem is not before: # else no move to do, it is already in the right place. insertedItem, insertIndex=self.move(newItem, target, target, after, before, None) else: insertedItem=newItem insertIndex=target.index(newItem.id) return (insertedItem, insertIndex) def delete(self, item, source): """Delete an item in the tree Delete item from source""" if item.delEnabled: del source[item.id] #print self.model def newItem(self, target): """Creates a new child in target""" if target.modifiable: #it is possible to add new item in the target # create a new branch item of the model # EditableTree class is not used directly because the model could be a derived class # class Branch should be able to be called without parameters newItem=self.model.__class__.Branch() target[newItem.id]=newItem #---------------------------------------------------------------------------- class ObservableListWidget(QTreeWidget): """A widget to represent an ObservableList. The list is modifiable by manipulating items in the widget : - add new item - delete an item - rename an item The widget is created with an ObservableList as model. The graphic component has a reference on the model. It registers a callback method on the model notifier. On modification events, the model is updated and notify all its listeners, so the view is updated via updateContent method. Action events are not catched in this class. To enable actions, you may attach a popup menu to the signal contextMenuRequested. The widget shows tooltips items. All items icon are resized to have the same size. Inner classes : - Item Inherits qt component QTreeWidget. @type model: ObservableList with an attribute name @ivar model: the list which this widget is a graphic representation. @type tooltipsViewer: ListViewToolTip @ivar tooltipsViewer: this object reimplements QToolTip in order to show tooltip's item when mouse is on an item """ def __init__(self, model, parent=None, iconSize=defaultIconSize): """ @type model: ObservableList @param model: the list which this widget is a graphic representation. @type parent:QWidget @param parent: parent of this widget @type iconSize: couple of integers or None @param iconSize: force items icon resizing. """ QTreeWidget.__init__( self, parent) self.setColumnCount(1) self.setHeaderLabels([""]) self.iconDim=iconSize self.setIconSize(QSize(*iconSize)) self.connect(self, SIGNAL("currentItemChanged ( QTreeWidgetItem *, QTreeWidgetItem * )", self.currentItemRenamed)) self.setContextMenuPolicy(Qt.CustomContextMenu) # keep a reference to the model for control event and register a listener to be aware of changes self.setModel(model) def setModel(self, m): """Construct items of the list reading the model given. A listener is added to the model, so when the model change, the method self.updateContents is automatically called. """ self.clear() self.model=m if m is not None: self.setHeaderLabels( [m.name] ) self.model.addListener(self.updateContent) # create child items with data in the tree model lastChild=None for item in self.model: lastChild=ObservableListWidget.Item(self, item, lastChild, self.iconDim) # append item to keep the same order as in the model def currentItemRenamed(self, current, previous): if self.currentItem() is not None: self.currentItem().okRename() def getChild(self, n): """Return the nth child item If n<=0 return the first child, if n>=nb children, return the last child @rtype: ObservableListWidget.Item""" if n<0: n=0 elif n>=self.topLevelItemCount(): n=self.topLevelItemCount()-1 return self.topLevelItem(n) def getLastChild(self): """Return the last child item. @rtype: ObservableListWidget.Item """ return self.topLevelItem( self.topLevelItemCount() - 1) #------ Update on model changes ------ def updateContent(self, action, items, position=None): """This method is called when the model notifies a change : The action has been done at position, with items. The view should update its content to reflect the changes. """ if action==ObservableList.INSERT_ACTION: self.insert(items, position) elif action==ObservableList.REMOVE_ACTION: self.remove(items, position) elif action==ObservableList.MODIFY_ACTION: # some model items have been modified, # widget items are replaced by new items with new model items # from position, all items must be replaced with new value # ->remove and then insert new if position is None: position=0 for modelItem in items: self.takeTopLevelItem(position) self.insert(items, position) #else: print action, "unknown action" def insert(self, items, position=None): """insertion of items at position in the model -> create view items for all items and insert them at position in self. Inserted item becomes the selected item.""" #insert at position = insert before item at position = insert after item at position-1 i=position for item in items: widgetItem=ObservableListWidget.Item(None, item, None, self.iconDim) self.insertTopLevelItem( i, item ) i+=1 if item.unamed: self.editItem(widgetItem) item.unamed=False self.setCurrentItem(widgetItem) def remove(self, items, position): """Removes items in the list from position. If the list is empty, one item is removed. If position is undefined, removes items whose model is in the list""" if position is None: position =0 if len(items)==0: self.takeTopLevelItem(position) else: for modelItem in items: found=False i=position while not found and i<=self.topLevelItemcount(): item=self.topLevelItem(i) if item.model is modelItem: self.takeTopLevelItem(i) found=True else: i+=1 #---------------------------------------------------------------------------- class Item(QTreeWidgetItem): """Item is the base class for elements of ObservableListWidget. Treats renaming events if the item is modifiable. """ def __init__( self, parent, model, after=None, iconSize=defaultIconSize): """ @type parent: ObservableListWidget @param parent: container of the item @type model: any object that contains attributes name, icon, tooltip and modifiable, and which is Observable. @param model: model which this item is the representation @type after: item @param after: the item after which current item must be added in the parent """ QTreeWidgetItem.__init__( self, parent ) if model.icon: iconPath = findIconFile( model.icon ) # QIcon if iconPath is not None: self.setIcon(0, QIcon(iconPath)) self.setText(0, model.name) self.setToolTip(0, model.tooltip) # rename is enabled only if the item is modifiable if model.modifiable: self.setFlags(self.flags() | Qt.ItemIsEditable) self.model=model self.model.onAttributeChange("name", self.updateName) def getText(self): return self.text(0) def okRename(self): """This method is called when user renames the item. The name must be changed in the model.""" if getattr(self, "model", None) is not None: newText=unicode(self.text(0)) if self.model.name != newText: if self.model.name==self.model.tooltip: self.model.tooltip=newText self.model.name=newText #------ Update on model changes ------ def updateName(self, newName): """This method is called when the model notifies that its name attribute has changed : The view should update its content to reflect the changes""" self.setText(0, newName) self.setToolTip(0, self.model.tooltip) #else: print "unknown action" #---------------------------------------------------------------------------- class TreeListWidget(QTreeWidget): """ This widget represents a list of EditableTree. The given model is an ObservableSortedDictionary (trees are referenced by their id). The widget listens for modifications done on the model : - add new item - delete an item - rename an item It registers a callback method on the model notifier. On modification events, the model is updated and notify all its listeners, so the view is updated via updateContent method. Action events are not catched in this class. To enable actions, you may attach a popup menu to the signal contextMenuRequested. Items accept drop if dropped elements are EditableTree.Item. Dropped element are added in target EditableTree. The widget shows tooltips items. All items icon are resized to have the same size. Inner classes : - Item Inherits qt component QTreeWidget. @type model: ObservableSortedDictionary @ivar model: the EditableTree map which this widget is a graphic representation. @type tooltipsViewer: ListViewToolTip @ivar tooltipsViewer: this object reimplements QToolTip in order to show tooltip's item when mouse is on an item @type draggedItems: list of EditableTree.Item @ivar draggedItems: list of elements that are currently dragged on the widget @type dropOn: TreeListWidget.Item @ivar dropOn: current drop zone (mouse is over this item with dragged items and the drop zone item accepts drop) """ def __init__(self, model, parent=None, iconSize=defaultIconSize): """ @type model: ObservableSortedDictionary @ivar model: the EditableTree map which this widget is a graphic representation. @type parent:QWidget @param parent: parent of this widget @type iconSize: couple of integers or None @param iconSize: force items icon resizing. """ QTreeWidget.__init__( self, parent) self.setColumnCount(1) self.iconDim=iconSize self.setIconSize(QSize(*iconSize)) self.setRootIsDecorated(False) # it is not really a tree but a list, keep a QTreeWidget to keep columns # enable display of tooltips on items # the listview accept drops to enable adding items in trees by dropping items on the item representing the tree self.setAcceptDrops(True) self.draggedItems=[] self.dropOn=None self.connect(self, SIGNAL("currentItemChanged ( QTreeWidgetItem *, QTreeWidgetItem * )"), self.currentItemRenamed ) self.setContextMenuPolicy(Qt.CustomContextMenu) # keep a reference to the model for control event and register a listener to be aware of changes self.setModel(model) def setModel(self, m): """ Construct items of the list reading the model given. A listener is added to the model, so when the model change, the method self.updateContents is automatically called. @type m: ObservableSortedDictionary @param m: tree map to show in this widget """ self.clear() self.model=m if m is not None: self.setHeaderLabels([m.name]) self.model.addListener(self.updateContent) # create child items with data in the tree model lastChild=None for item in self.model.values(): lastChild=self.Item(self, item, lastChild, self.iconDim) # append item to keep the same order as in the model def currentItemRenamed(self, current, previous): if self.currentItem() is not None: self.currentItem().okRename() def getChild(self, n): """ Return the nth child item If n<=0 return the first child, if n>=nb children, return the last child @rtype: TreeListWidget.Item """ if n<0: n=0 elif n>=self.topLevelItemCount(): n=self.topLevelItemCount() - 1 return self.topLevelItem(n) def getLastChild(self): """ Return the last child item. @rtype: TreeListWidget.Item """ return self.topLevelItem(self.topLevelItemCount()-1) #------ Drag&drop control ------ def dragEnterEvent(self, event): """ This method is called when a drag enters in the widget. The event contains the encoding of the dragged objects. The method tries to decode text event as minf representation of EditableTree.Item and sets self.draggedItems. """ # accept drag if it contains instances of EditableTree.Item self.draggedItems=[] if event.mimeData().hasText(): textEventBuf=StringIO(event.mimeData().text()) # check if it is the expected minf format format, reduction=minfFormat(textEventBuf) if format=="XML": textEventBuf.seek(0) for value in iterateMinf( textEventBuf ): if isinstance(value, EditableTree.Item): event.accept() value.setAllModificationsEnabled(True) # this is a copy of items that comes from another tree, so enable modifications on the copied items self.draggedItems.append(value) else: event.ignore() textEventBuf.close() else: event.ignore() else: event.ignore() def toContentsPoint(self, point): """ Translates coordinates in the frame to coordinates in the content of the list (without header). @rtype: QPoint """ #viewportPoint=self.contentsToViewport(point) to get the item at position we need the position in the entire list not in the visible content (viewport) return point #- QPoint(0, self.header().height()) def dragMoveEvent(self, event): """ When the drag moves, the current dropzone can change. According to cursor position, dropOn attribute is updated. To have a valid dropzone, mouse must be over a modifiable item. When the dragMoveEvent is ignored, a forbidden cursor is shown. """ # draggedItem must be set, otherwise it isn't a correct drag if self.draggedItems==[]: event.ignore(self.contentsRect()) self.dropOn=None else: cursorPos=self.toContentsPoint(event.pos()) currentItem=self.itemAt(cursorPos) #item under the mouse cursor self.dropOn=currentItem if currentItem is not None and currentItem.model.modifiable and event.source().model is not currentItem.model: # not accept dragged items that comes from current tree event.accept() else: event.ignore() def dropEvent( self, event): """ When items are dropped, they are added to the dropzone model (an EditableTree) """ # current dropzone is stored in dropOn if self.dropOn!=None: for item in self.draggedItems: self.dropOn.model.add(item) self.draggedItems = [] self.dropOn=None #------ Update on model changes ------ def updateContent(self, action, items, position=None): """ This method is called when the model notifies a change : The action has been done at position, with items. The view should update its content to reflect the changes. """ if action==ObservableList.INSERT_ACTION: self.insert(items, position) elif action==ObservableList.REMOVE_ACTION: self.remove(items, position) elif action==ObservableList.MODIFY_ACTION: # some model items have been modified, # widget items are replaced by new items with new model items # from position, all items must be replaced with new value # ->remove and then insert new for modelItem in items: self.takeTopLevelItemItem(position) self.insert(items, position) #else: print action, "unknown action" def insert(self, items, position=None): """insertion of items at position in the model -> create view items for all items and insert them at position in self. Inserted item becomes the selected item.""" #insert at position = insert before item at position = insert after item at position-1 i=position for item in items: newItem=self.Item(None, item, None, self.iconDim) self.insertTopLevelItem(i, newItem) i+=1 if item.unamed: self.editItem(newItem, 0) item.unamed=False self.setCurrentItem(newItem) def remove(self, items, position): """Removes items in the list from position. If the list is empty, one item is removed. If position is undefined, removes items whose model is in the list""" if position is None: position =0 if len(items)==0: self.takeTopLevelItem(position) else: for modelItem in items: found=False i=position while not found and i<self.count(): item=self.topLevelItem(i) if item.model is modelItem: self.takeTopLevelItem(i) found=True else: i+=1 #---------------------------------------------------------------------------- class Item(QTreeWidgetItem): """ Item is the base class for elements of TreeListWidget. Treats renaming events if the item is modifiable. """ def __init__( self, parent, model, after=None, iconSize=defaultIconSize): """ @type parent: TreeListWidget @param parent: container of the item @type model: any object that contains attributes name, icon, tooltip and modifiable, and which is Observable. @param model: model which this item is the representation @type after: item @param after: the item after which current item must be added in the parent """ QTreeWidgetItem.__init__( self, parent ) if model.icon: iconPath = findIconFile( model.icon ) if iconPath is not None: self.setIcon(0, QIcon(iconPath)) self.setText(0, model.name) self.setToolTip(0, model.tooltip) # rename is enabled only if the item is modifiable if model.modifiable: self.setFlags(self.flags() | Qt.ItemIsEditable) if not model.visible: self.setHidden(True) self.model=model self.model.onAttributeChange("name", self.updateName) self.model.onAttributeChange("visible", self.updateVisibility) def getText(self): return self.text(0) def okRename(self): """ This method is called when user renames the item. The name must be changed in the model. """ if getattr(self, "model", None) is not None: newText=unicode(self.text(0)) if self.model.name != newText: if self.model.name==self.model.tooltip: self.model.tooltip=newText self.model.name=newText # the model will notify the change and updateName will be called #------ Update on model changes ------ def updateName(self, newName): """ This method is called when the model notifies that its name attribute has changed : The view should update its content to reflect the changes. """ self.setText(0, newName) self.setToolTip(0, self.model.tooltip) #else: print "unknown action" def updateVisibility(self, newValue): """ Called when the model notifies that its visible attribute value has changed. """ #pass self.setHidden(not newValue)