コード例 #1
0
ファイル: LinkChart.py プロジェクト: fenglb/ccpnmr2.4
class LinkChart(Frame):
    def __init__(self, parent, scrollX=True, scrollY=True, *args, **kw):

        self.parent = parent
        self.scrollX = scrollX
        self.scrollY = scrollY

        self.prototypes = []
        self.nodes = []
        self.node = None  # Selected
        self.links = []
        self.link = None  # Selected
        self.objDict = {}  # Map canvas to nodes, links
        self.cornerDict = {}
        self.editWidget = None
        self.mouseOver = None
        self.inMotion = None
        self.inMotion2 = None
        self.inMotion3 = None
        self.dragRegion = None
        self._resizeBusy = False

        self.selectedNodes = []
        self.selectedLinks = []
        self.highlightNodes = []
        self.highlightLinks = []

        kw['relief'] = 'flat'
        kw['borderwidth'] = 0

        Frame.__init__(self, parent, *args, **kw)

        self.canvas = canvas = Canvas(self, relief='flat', borderwidth=0)
        self.canvas.configure(xscrollincrement=2,
                              yscrollincrement=2,
                              bg=self.cget('bg'))
        self.canvas.grid(row=0, column=0, sticky='nsew')

        if scrollX:
            self.horizScrollbar = Tkinter.Scrollbar(self,
                                                    bg=self.cget('bg'),
                                                    command=self.canvas.xview,
                                                    orient=Tkinter.HORIZONTAL,
                                                    borderwidth=1)
            self.canvas.configure(xscrollcommand=self.horizScrollbar.set)

        if scrollY:
            self.vertScrollbar = Tkinter.Scrollbar(self,
                                                   bg=self.cget('bg'),
                                                   command=self.canvas.yview,
                                                   orient=Tkinter.VERTICAL,
                                                   borderwidth=1)
            self.canvas.configure(yscrollcommand=self.vertScrollbar.set)

        canvas.bind('<Button-1>', self._mouseSingleClick)
        if not isWindowsOS():
            canvas.bind('<Button-4>', self.scrollUp)
            canvas.bind('<Button-5>', self.scrollDown)
        else:
            canvas.bind('<MouseWheel>', self._windowsOsScroll)

        canvas.bind('<Double-1>', self._mouseDoubleClick)
        canvas.bind('<B1-Motion>', self._mouseDrag)
        canvas.bind('<ButtonRelease-1>', self._mouseRelease)
        canvas.bind('<Motion>', self._mouseEnter)
        canvas.bind('<Enter>', self._mouseEnter)
        canvas.bind('<Leave>', self._mouseLeave)
        canvas.bind('<KeyPress-Up>', self._moveUp)
        canvas.bind('<KeyPress-Down>', self._moveDown)
        canvas.bind('<KeyPress-Right>', self._moveRight)
        canvas.bind('<KeyPress-Left>', self._moveLeft)
        canvas.bind('<KeyPress-Delete>', self.deleteSelected)
        self.bind('<Configure>', self._resizeAfter)

        self.menu = Menu(self, tearoff=False)

        alignItems = [{
            'kind': 'command',
            'label': 'Y axis',
            'command': self._alignHorizontal
        }, {
            'kind': 'command',
            'label': 'X axis',
            'command': self._alignVertical
        }]

        distributeItems = [{
            'kind': 'command',
            'label': 'Horizontal',
            'command': self._distributeHorizontal
        }, {
            'kind': 'command',
            'label': 'Verical',
            'command': self._distributeVertical
        }]

        menuItems = [
            {
                'kind': 'cascade',
                'label': 'Align',
                'submenu': alignItems
            },  #0
            {
                'kind': 'cascade',
                'label': 'Distribute',
                'submenu': distributeItems
            },  #1
            {
                'kind': 'command',
                'label': 'Link Selected',
                'command': self.linkSelected
            },  #2
            {
                'kind': 'command',
                'label': 'Reset Links',
                'command': self._resetLinks
            },  #3
            {
                'kind': 'separator'
            },
            {
                'kind': 'command',
                'label': 'Delete Links',  #5
                'command': self.deleteLinks
            },
            {
                'kind': 'command',
                'label': 'Delete Items',  #6
                'command': self.deleteNodes
            },
            {
                'kind': 'separator'
            },
            {
                'kind': 'command',
                'label': 'Save PostScript File',
                'command': self.printCanvas
            },
        ]

        self.menuItems = menuItems
        self.menu.setMenuItems(menuItems)

        canvas.bind('<ButtonPress-3>', self._popupMenu)

        self._drawAfter()

    def _resizeAfter(self, event):

        if self._resizeBusy:
            return

        self._resizeBusy = True
        self._popdownMenu(event)
        self.after_idle(lambda: self._refreshScrollbars(event))

    def _refreshScrollbars(self, event):

        bbox = self.canvas.bbox('all')

        if not bbox:
            self._resizeBusy = False
            return

        cWidth = event.width
        cHeight = event.height

        vertScrollbar = self.vertScrollbar
        horizScrollbar = self.horizScrollbar

        xSpace = bbox[2] + bbox[0]  # Symmetric space
        ySpace = bbox[3] + bbox[1]

        horizHeight = int(horizScrollbar.winfo_height())
        vertWidth = int(vertScrollbar.winfo_width())

        if cHeight < ySpace + horizHeight:
            vertScrollbar.grid(row=0, column=1, sticky='ns')
            cWidth -= vertWidth
        else:
            vertScrollbar.grid_forget()

        if cWidth < xSpace + vertWidth:
            horizScrollbar.grid(row=1, column=0, sticky='ew')
            cHeight -= horizHeight
        else:
            horizScrollbar.grid_forget()

        x1 = 0
        y1 = 0
        x2 = max(cWidth, xSpace)
        y2 = max(cHeight, ySpace)

        self.canvas.configure(width=cWidth, height=cHeight)
        self.canvas.configure(scrollregion=(x1, y1, x2, y2))
        self.update_idletasks()
        self._resizeBusy = False

    def _popupMenu(self, event):

        self._finishEdit()
        self._configMenu()
        self.menu.popupMenu(event)

    def _popdownMenu(self, event):

        self._finishEdit()
        self.menu.unpost()

    def _configMenu(self):

        if self.selectedLinks:
            self.menu.entryconfig(5, state=ACTIVE)
            self.menu.entryconfig(3, state=ACTIVE)
        else:
            self.menu.entryconfig(5, state=INACTIVE)
            self.menu.entryconfig(3, state=INACTIVE)

        if self.selectedNodes:
            self.menu.entryconfig(2, state=ACTIVE)
            self.menu.entryconfig(6, state=ACTIVE)
        else:
            self.menu.entryconfig(2, state=INACTIVE)
            self.menu.entryconfig(6, state=INACTIVE)

        if self.selectedNodes or self.selectedLinks:
            self.menu.entryconfig(0, state=ACTIVE)
            self.menu.entryconfig(1, state=ACTIVE)
        else:
            self.menu.entryconfig(0, state=INACTIVE)
            self.menu.entryconfig(1, state=INACTIVE)

    def _resetLinks(self):

        for link in self.selectedLinks:
            link.resetCoords()

    def _alignSelected(self, vertical=False, horizontal=False):

        obj = self.node or self.link

        if not obj:
            return

        x0, y0 = obj.coords

        objs = self.selectedNodes + self.selectedLinks
        n = len(objs)

        if n < 2:
            return

        if horizontal:
            for obj in objs:
                obj.move(obj.coords[0], y0)

        if vertical:
            for obj in objs:
                obj.move(x0, obj.coords[1])

    def _distributeSelected(self, vertical=False, horizontal=False):

        xCoords = []
        yCoords = []

        objs = self.selectedNodes + self.selectedLinks
        n = len(objs)

        if n < 3:
            return

        for obj in objs:
            x, y = obj.coords
            xCoords.append((x, obj))
            yCoords.append((y, obj))

        if horizontal:
            xCoords.sort()
            xA = xCoords[0][0]
            dx = (xCoords[-1][0] - xA) / float(n - 1)

            for i, (x0, obj) in enumerate(xCoords):
                y = obj.coords[1]
                x = xA + (i * dx)
                obj.move(x, y)

        if vertical:
            yCoords.sort()
            yA = yCoords[0][0]
            dy = (yCoords[-1][0] - yA) / float(n - 1)

            for i, (y0, obj) in enumerate(yCoords):
                x = obj.coords[0]
                y = yA + (i * dy)
                obj.move(x, y)

    def _alignHorizontal(self):

        self._alignSelected(horizontal=True)

    def _alignVertical(self):

        self._alignSelected(vertical=True)

    def _distributeHorizontal(self):

        self._distributeSelected(horizontal=True)

    def _distributeVertical(self):

        self._distributeSelected(vertical=True)

    def linkSelected(self):

        nodes = self.selectedNodes
        n = len(nodes)
        if n > 1:

            for i in range(n - 1):
                nodeA = nodes[i]

                for j in range(i + 1, n):
                    nodeB = nodes[j]

                    for link in nodeA.links:
                        if nodeB in link.nodes:
                            #print link, link.nodes, nodeA, nodeB
                            break

                    else:
                        link = Link(self, nodeA, nodeB, text='*')
                        #print link

    def deleteNodes(self):

        if self.selectedNodes:
            msg = 'Really delete %d selected nodes?' % len(self.selectedNodes)
            if showOkCancel('Confirm', msg, parent=self):
                for node in self.selectedNodes[:]:
                    node.delete()
                self.selectedNodes = []
                self.node = None

    def deleteLinks(self):

        if self.selectedLinks:
            msg = 'Really delete %d selected links?' % len(self.selectedLinks)
            if showOkCancel('Confirm', msg, parent=self):
                for link in self.selectedLinks[:]:
                    link.delete()

                self.selectedLinks = []
                self.link = None

    def deleteSelected(self, event=None):

        objs = self.selectedLinks + self.selectedNodes
        if objs:
            n1 = len(self.selectedNodes)
            n2 = len(self.selectedLinks)
            msg = 'Really delete %d selected objects?\n' % (n1 + n2)
            msg += '(%d nodes, %d links)' % (n1, n2)
            if showOkCancel('Confirm', msg, parent=self):
                for obj in objs:
                    obj.delete()

                self.selectedLinks = []
                self.link = None
                self.selectedNodes = []
                self.node = None

    def printCanvas(self, *event):

        from memops.gui.FileSelect import FileType
        from memops.gui.FileSelectPopup import FileSelectPopup

        fileTypes = [FileType('PostScript', ['*.ps']), FileType('All', ['*'])]
        fileSelectPopup = FileSelectPopup(self,
                                          file_types=fileTypes,
                                          title='Print canvas to file',
                                          dismiss_text='Cancel',
                                          selected_file_must_exist=False)

        fileName = fileSelectPopup.getFile()

        self.postscript(colormode='color', file=fileName)

    def _clearSelection(self):

        for node in self.selectedNodes:
            node.isSelected = False

        for link in self.selectedLinks:
            link.isSelected = False

        self.selectedNodes = []
        self.selectedLinks = []
        self.link = None
        self.node = None

    def _clearHighlights(self):

        for node in self.highlightNodes:
            node.highlight = False

        for link in self.highlightLinks:
            link.highlight = False

        self.mouseOver = None
        self.highlightNodes = []
        self.highlightLinks = []

    def _mouseSingleClick(self, event):

        self._mouseClick(event)

    def _mouseDoubleClick(self, event):

        self._mouseClick(event, True)

    def _mouseClick(self, event, edit=False):

        #self.menu.popdownMenu(event)

        self._finishEdit()

        canvas = event.widget
        canvas.focus_force()

        x = canvas.canvasx(event.x)
        y = canvas.canvasy(event.y)
        items = canvas.find('overlapping', x - 1, y - 1, x + 1, y + 1)

        #item = self.find('closest',x,y)[0]
        if not items:
            self._clearSelection()
            self._clearHighlights()
            self.draw()
            return

        objs = [
            self.objDict.get(item) for item in items if self.objDict.get(item)
        ]

        for obj in objs:
            if isinstance(obj, Node):
                if not (event.state & 4 or event.state & 1):
                    self._clearSelection()

                obj.isSelected = True
                self.node = obj
                if obj not in self.selectedNodes:
                    self.selectedNodes.append(obj)

                break

        else:
            self.node = None

            for obj in objs:
                if isinstance(obj, Link):
                    if not (event.state & 4 or event.state & 1):
                        self._clearSelection()

                    obj.isSelected = True
                    self.link = obj

                    if obj not in self.selectedLinks:
                        self.selectedLinks.append(obj)

                    break

            else:
                self.link = None

        self._drawAfter()
        if not obj:
            return

        if edit and obj.editWidget:
            obj.edit()
            self.editWidget = obj.editWidget

    def _clearDragRegion(self, canvas):

        if self.dragRegion:
            canvas.delete(self.dragRegion[0])
            self.dragRegion = None

    def _mouseEnter(self, event):

        self.menu.popdownMenu(event)
        canvas = event.widget
        canvas.focus_force()

        x = canvas.canvasx(event.x)
        y = canvas.canvasy(event.y)
        items = canvas.find('overlapping', x - 1, y - 1, x + 1, y + 1)

        #item = self.find('closest',x,y)[0]
        if not items:
            self._clearHighlights()
            self.draw()
            return

        obj = self.objDict.get(items[0])

        self._clearHighlights()

        if obj is None:
            self.draw()
            return

        if obj is self.mouseOver:
            if self.inMotion2:
                for item in items:
                    if self.cornerDict.has_key(item):
                        self.inMotion2 = None
                        break

            return

        objs = [
            self.objDict.get(item) for item in items if self.objDict.get(item)
        ]

        for obj in objs:
            if isinstance(obj, Node):
                self.highlightNodes = [
                    obj,
                ]
                break

        else:
            for obj in objs:
                if isinstance(obj, Link):
                    self.highlightLinks = [
                        obj,
                    ]

        obj.highlight = True

        self.mouseOver = obj
        self.draw()

    def _mouseLeave(self, event):

        #self._mouseRelease(event)
        #self._finishEdit()
        self._clearHighlights()
        self.draw()

    def _mouseDrag(self, event):

        self.menu.popdownMenu(event)

        canvas = event.widget
        canvas.focus_force()
        x = canvas.canvasx(event.x)
        y = canvas.canvasy(event.y)

        #width = int(canvas.winfo_width())
        #height = int (canvas.winfo_height())
        #
        #if not (0 < x < width) or not (0 < y < height):
        #  self._mouseRelease(event)
        #  return

        inMotion = self.inMotion

        objs = self.selectedNodes + self.selectedLinks
        if objs:
            items = canvas.find('overlapping', x - 1, y - 1, x + 1, y + 1)

            if not inMotion:
                for item in items:
                    if self.inMotion2 or self.cornerDict.has_key(item):
                        obj = self.node

                        x1, y1 = obj.coords  # offsets
                        dx = abs(x - x1)
                        dy = abs(y - y1)

                        size = max(dx, dy) + 1

                        obj.width = size
                        obj.draw()

                        self.inMotion2 = True

                        return

            if inMotion:
                offsets = inMotion  # Get offsets

            else:
                offsets = []
                for obj in objs:
                    x1, y1 = obj.coords  # offsets
                    dx = x - x1  # Offset relative to centre
                    dy = y - y1

                    if isinstance(obj, Node) and obj.isPrototype:
                        obj = obj.copy()

                    offsets.append((dx, dy, obj))

                self.inMotion = offsets  # Store click offset and Node

            #print event, event.widget._root()

            for dx, dy, obj in offsets:
                x2 = x - dx
                y2 = y - dy

                obj.move(x2, y2)

            x0, y0 = event.x_root, event.y_root
            under = self.winfo_containing(x0, y0)

            if isinstance(under, Canvas):
                under = under.parent

            if isinstance(under, LinkChart) and (under is not self):
                for dx, dy, obj in offsets:
                    if not isinstance(obj, Node):
                        continue

                    original = obj.original
                    if original and original.isPrototype:
                        xT = x0 - under.winfo_rootx() - dx
                        yT = y0 - under.winfo_rooty() - dy

                        if original.spawn:
                            original.spawn.move(xT, yT)
                        else:
                            original.spawn = original.copy(under,
                                                           coords=(xT, yT))

            else:
                for dx, dy, obj in offsets:
                    if not isinstance(obj, Node):
                        continue

                    original = obj.original
                    if original and original.spawn:
                        original.spawn.delete()
                        original.spawn = None

        else:
            if self.inMotion3:
                x0, y0, = self.inMotion3

            else:
                #self._mouseRelease(event)
                x0 = x
                y0 = y
                self.inMotion3 = (x, y)

            if self.dragRegion:
                item = self.dragRegion[0]
                canvas.coords(item, x0, y0, x, y)
            else:
                item = canvas.create_rectangle(x0,
                                               y0,
                                               x,
                                               y,
                                               outline='#008000',
                                               fill='#00E000',
                                               stipple='gray25')

            self.dragRegion = (item, x0, y0, x, y)

    def _mouseRelease(self, event):

        canvas = event.widget

        if self.dragRegion:
            item, x1, y1, x2, y2 = self.dragRegion

            xCoords = [x1, x2]
            yCoords = [y1, y2]
            xCoords.sort()
            yCoords.sort()

            x1, x2 = xCoords
            y1, y2 = yCoords

            self.node = None
            self.selectedNodes = []
            for node in self.nodes:
                x0, y0 = node.coords

                if (x1 < x0 < x2) and (y1 < y0 < y2):
                    self.selectedNodes.append(node)
                    node.isSelected = True
                    self.node = node
                else:
                    node.isSelected = False

            self.link = None
            self.selectedLinks = []
            for link in self.links:
                x0, y0 = link.coords

                if (x1 < x0 < x2) and (y1 < y0 < y2):
                    self.selectedLinks.append(link)
                    link.isSelected = True
                    self.link = link
                else:
                    link.isSelected = False

            self._clearDragRegion(canvas)

            self.draw()

        if self.inMotion:
            for dx, dy, obj in self.inMotion:
                if isinstance(
                        obj,
                        Node) and obj.original and obj.original.isPrototype:
                    if obj.original.spawn:
                        obj.original.spawn.original = None
                        obj.original.spawn = None
                    obj.delete()

        self.inMotion = None
        self.inMotion2 = None
        self.inMotion3 = None

    def _finishEdit(self):

        if self.editWidget:
            self.focus_force()
            self.editWidget.commit()
            self.editWidget.hide()
            self.editWidget = None

    def _moveUp(self, event):

        self._moveSelected(event, 0, -1)

    def _moveDown(self, event):

        self._moveSelected(event, 0, 1)

    def _moveRight(self, event):

        self._moveSelected(event, 1, 0)

    def _moveLeft(self, event):

        self._moveSelected(event, -1, 0)

    def _moveSelected(self, event, dx, dy):

        if not (event.state & 4 or event.state & 1):
            dx *= MOVE_STEP
            dy *= MOVE_STEP

        for node in self.selectedNodes:
            x0, y0 = node.coords
            node.move(x0 + dx, y0 + dy)

        for link in self.selectedLinks:
            x0, y0 = link.coords
            link.move(x0 + dx, y0 + dy)

    def scrollUp(self, event=None):

        pass

    def scrollDown(self, event):

        pass

    def _windowsOsScroll(self, event=None):

        delta = event.delta

        if delta > 0:
            self.scrollUp(event)

        elif delta < 0:
            self.scrollDown(event)

    def _drawAfter(self):

        self.after_idle(self.draw)

    def draw(self):

        for link in self.links:
            link.draw()

        for node in self.nodes:
            node.draw()
コード例 #2
0
class Tree(Frame):
    def __init__(self,
                 parent,
                 iconSize='medium',
                 multiSelect=False,
                 font='Helvetica 8',
                 doubleCallback=None,
                 *args,
                 **kw):

        Frame.__init__(self, parent, *args, **kw)

        self.multiSelect = multiSelect
        self.icons = {}
        self.openDict = {}
        self.scrollbarWidth = 15
        self.canvasList = []
        self.canvasDict = {}
        self.canvasLines = []
        self._wait = False
        self.vOffset = 0
        self.nodes = []  # Just a list of nodes
        self.nodeDict = {}  # To fetch nodes via objects
        self.font = font
        self.doubleCallback = doubleCallback

        #bg = self.cget('background')

        iconItems = [{
            'kind': 'command',
            'label': 'Small',
            'command': self.smallIcons
        }, {
            'kind': 'command',
            'label': 'Medium',
            'command': self.medIcons
        }, {
            'kind': 'command',
            'label': 'Large',
            'command': self.largeIcons
        }]

        self.menu = Menu(self, tearoff=False)
        menu_items = [
            {
                'kind': 'cascade',
                'label': 'Icon size',
                'submenu': iconItems
            },
        ]

        self.menu.setMenuItems(menu_items)

        self.rowHeight = None
        self.visibleRows = 80

        self.setIconSize(iconSize, redraw=False)

        self.canvas = Canvas(self,
                             relief='flat',
                             borderwidth=0,
                             background=BG_COLOR,
                             xscrollcommand=self._setHScrollbar,
                             *args,
                             **kw)

        self.canvas.pack()

        self.xScrollbar = Scrollbar(self,
                                    orient='horizontal',
                                    width=self.scrollbarWidth,
                                    borderwidth=1,
                                    callback=self._moveHScrollbar,
                                    background=BG_COLOR)

        self.yScrollbar = Scrollbar(self,
                                    orient='vertical',
                                    width=self.scrollbarWidth,
                                    borderwidth=1,
                                    callback=self._setVScrollbar,
                                    background=BG_COLOR)

        self.canvas.bind('<Button-1>', self._mouseClick)
        self.canvas.bind('<Double-1>', self._mouseDoubleClick)
        if isWindowsOS:
            self.canvas.bind('<MouseWheel>', self._windowsOsScroll)
        else:
            self.canvas.bind('<Button-4>', self._mouseUp)
            self.canvas.bind('<Button-5>', self._mouseDown)

        self.canvas.bind('<p>', self._printCanvas)
        self.canvas.bind('<KeyPress-Prior>', self._pageUp)
        self.canvas.bind('<KeyPress-Next>', self._pageDown)
        self.canvas.bind('<KeyPress-Up>', self._keyUp)
        self.canvas.bind('<KeyPress-Down>', self._keyDown)
        self.canvas.bind('<KeyPress-Home>', self._keyHome)
        self.canvas.bind('<KeyPress-End>', self._keyEnd)
        self.canvas.bind('<Enter>', self._enter)
        self.canvas.bind('<ButtonPress-3>', self._popupMenu)

        self.bind('<Configure>', self._changeSizeAfter)

    def _popupMenu(self, event):

        self.menu.popupMenu(event)

    def smallIcons(self):

        self.setIconSize('small')

    def medIcons(self):

        self.setIconSize('medium')

    def largeIcons(self):

        self.setIconSize('large')

    def setIconSize(self, iconSize, redraw=True):

        size = ICON_SIZE_DICT.get(iconSize, 22)
        if size != self.rowHeight:
            self.rowHeight = size + 1
            self.font = 'Helvetica %d' % (size / 2)

            ccpnDir = getTopDirectory()
            iconDir = '%dx%d' % (size, size)
            imageDir = path.join(ccpnDir, 'python', 'memops', 'gui',
                                 'graphics', iconDir)

            files = [f for f in listdir(imageDir) if f.endswith('.gif')]

            for file in files:
                imageFile = path.join(imageDir, file)
                self.icons[file[:-4]] = PhotoImage(file=imageFile)

            if redraw:
                self._changeSize()

        return size

    def _setHScrollbar(self, start, end):

        self.xScrollbar.set(float(start), float(end))

    def _moveHScrollbar(self, start, end):

        self.canvas.xview('moveto', start)

    def _enter(self, event):

        self.menu.popdownMenu()
        self.canvas.focus_force()

    def _printCanvas(self, *event):

        from memops.gui.FileSelect import FileType
        from memops.gui.FileSelectPopup import FileSelectPopup

        fileTypes = [FileType('PostScript', ['*.ps']), FileType('All', ['*'])]
        fileSelectPopup = FileSelectPopup(self,
                                          file_types=fileTypes,
                                          title='Print canvas to file',
                                          dismiss_text='Cancel',
                                          selected_file_must_exist=False)

        fileName = fileSelectPopup.getFile()

        self.canvas.postscript(colormode='color', file=fileName)

    def _windowsOsScroll(self, event):

        delta = event.delta
        if delta > 0:
            self._mouseUp(event)
        elif delta < 0:
            self._mouseDown(event)

    def _keyHome(self, event):

        self.vOffset = 0
        self._drawAfter()

    def _keyEnd(self, event):

        numNodes = len(self.nodes)
        visNodes = self.visibleRows
        self.vOffset = max(0, numNodes - visNodes)
        self._drawAfter()

    def _scroll(self, delta=1):

        height = int(self.winfo_height())
        numNodes = len(self.nodes)
        visNodes = self.visibleRows

        if numNodes <= visNodes:
            return

        vOffset = self.vOffset + delta
        vOffset = max(0, vOffset)
        vOffset = min(numNodes - visNodes, vOffset)

        if vOffset != self.vOffset:
            self.vOffset = vOffset
            self._drawAfter()

    def _pageUp(self, event):

        self._scroll(-1 * self.visibleRows)

    def _pageDown(self, event):

        self._scroll(self.visibleRows)

    def _mouseUp(self, event):

        self._scroll(-8)

    def _mouseDown(self, event):

        self._scroll(8)

    def _keyUp(self, event):

        self._scroll(-1)

    def _keyDown(self, event):

        self._scroll(1)

    def _setVScrollbar(self, start, end):

        self.cancelEdits()

        height = int(self.winfo_height())

        gaps = start + 1 - end

        nRows = float(len(self.nodes))

        if gaps == 0 or nRows == 0.0:
            self.yScrollbar.set(0, 1)
            self.vOffset = 0
        else:

            # Have to do this so that the bar keeps up with the mouse
            overlap = int(nRows - self.visibleRows)
            size = min(1.0, self.visibleRows / nRows)
            self.yScrollbar.set(min(gaps, max(0, start)),
                                max(size, min(1.0, start + size)))
            self.vOffset = min(
                max(0, int('%1.1d' % (start / gaps * (overlap + 1)))), overlap)

            #vOffset = max(0, start*nRows)
            #vOffset = min(nRows-self.visibleRows, vOffset)
            #self.vOffset = int(vOffset)

        self._drawAfter()

    def _updateScrollBars(self):

        allRows = len(self.nodes)
        visRows = self.visibleRows

        if allRows < visRows:
            self.vOffset = 0
            self.yScrollbar.set(0, 1)
            self.yScrollbar.place_forget()

        else:
            size = self.scrollbarWidth
            width = int(self.winfo_width()) - 1
            height = int(self.winfo_height()) - 1
            start = float(self.vOffset) / allRows
            end = float(self.vOffset + visRows) / allRows

            self.yScrollbar.set(start, end)
            self.yScrollbar.place(x=width - size + 1,
                                  y=1,
                                  width=size,
                                  height=height)

        self.update_idletasks()

    def _mouseClick(self, event):

        self.cancelEdits()

        x = self.canvas.canvasx(event.x)
        y = self.canvas.canvasy(event.y)
        obj = self.canvas.find('closest', x, y)[0]
        node, typ = self.canvasDict.get(obj, (None, None))

        if node:
            if typ is TOGGLE:
                node.toggle()

            else:
                if self.multiSelect and (event.state & 4 or event.state & 1):
                    if event.state & 4:
                        node.isSelected = not node.isSelected

                    elif event.state & 1:
                        indices = [
                            self.nodes.index(node),
                        ]
                        for i, node2 in enumerate(self.nodes):
                            if node2.isSelected:
                                indices.append(i)

                        for node2 in self.nodes[min(indices):max(indices) + 1]:
                            node2.isSelected = True

                else:
                    for node2 in self.nodes:
                        if node2.isSelected:
                            node2.isSelected = False

                    node.isSelected = True

            self._drawAfter()

    def _mouseDoubleClick(self, event):

        self.cancelEdits()

        x = self.canvas.canvasx(event.x)
        y = self.canvas.canvasy(event.y)
        obj = self.canvas.find('closest', x, y)[0]
        node, typ = self.canvasDict.get(obj, (None, None))

        if node:
            if typ is TOGGLE:
                #node.toggle()
                return

            elif self.doubleCallback:
                self.doubleCallback(node)

        self._drawAfter()

    def _drawAfter(self):

        if self._wait:
            return

        self._wait = True
        self.after_idle(self._draw)

    def _draw(self):

        self._updateScrollBars()

        delta = self.rowHeight
        canvas = self.canvas
        cImage = canvas.create_image
        cList = self.canvasList
        cDict = self.canvasDict
        cLines = self.canvasLines
        cConfig = canvas.itemconfigure
        cDel = canvas.delete
        cCoords = canvas.coords
        cText = canvas.create_text
        cRect = canvas.create_rectangle
        cBbox = canvas.bbox
        cLine = canvas.create_line
        cLift = canvas.lift
        cLower = canvas.lower
        cPoly = canvas.create_polygon
        nodes = self.nodes
        nodeDict = self.nodeDict
        visibleRows = self.visibleRows
        icons = self.icons
        font = self.font
        vOffset = self.vOffset
        bg = BG_COLOR  # self.cget('bg')

        roots = [n for n in nodes if not n.parent]

        if not roots:
            raise Exception('No rooted nodes')

        for item in cLines:
            cDel(item)

        stack = [(node, 0) for node in roots]
        nodes = []
        while stack:
            node, level = stack.pop(0)
            nodes.append((node, level))

            if node.isOpen:
                level2 = level + 1
                stack = [(c, level2) for c in node.children] + stack

        yDict = {}
        pLevel = 0
        pNode = None
        pad = 2 + (delta / 2)
        bSpace = 4
        nodes = nodes[vOffset:]
        for row, data in enumerate(nodes):

            node, level = data

            y = pad + row * delta
            x = pad + level * delta
            x1 = x + delta
            x2 = x1 + delta - bSpace

            yDict[node] = y, x1

            yP, xP = yDict.get(node.parent, (0, x))

            if yP is not None:
                line = cLine(xP, yP, xP, y, fill=LINE_COLOR, width=1)
                cLower(line)
                cLines.append(line)

            if row > visibleRows:
                break

            iconImage = icons[node.icon]

            if node.isSelected:
                bgColor = SELECT_BG_COLOR
                fgColor = SELECT_FG_COLOR
            else:
                bgColor = bg
                fgColor = bg

            if row < len(cList):
                line, rect, tog1, tog2, box, icon, text = cList[row]
                cConfig(rect, fill=bgColor, outline=fgColor)
                cConfig(icon, image=iconImage)
                cConfig(text, text=node.label or '', font=font)
                cCoords(icon, x1, y)
                cCoords(text, x2, y)
                cCoords(line, x, y, x1, y)

            else:
                line = cLine(x, y, x1, y, fill=LINE_COLOR)
                rect = cRect(x, y, x2, y, fill=bgColor, outline=fgColor)
                box = cRect(-2, -2, -2, -2, fill=NODE_BG_COLOR)
                tog1 = cLine(-2, -2, -2, -2)
                tog2 = cLine(-2, -2, -2, -2)
                icon = cImage(x1, y, image=iconImage)
                text = cText(x2,
                             y,
                             text=node.label or '',
                             justify='left',
                             anchor='w',
                             font=font)
                cList.append((line, rect, tog1, tog2, box, icon, text))

            if node.callback:
                cCoords(box, x - 4, y - 4, x + 4, y + 4)

                if node.isOpen:
                    cCoords(tog1, -2, -2, -2, -2)
                    cCoords(tog2, x - 2, y, x + 3, y)

                else:
                    cCoords(tog1, x, y - 2, x, y + 3)
                    cCoords(tog2, x - 2, y, x + 3, y)

            else:
                cCoords(tog1, -2, -2, -2, -2)
                cCoords(tog2, -2, -2, -2, -2)
                cCoords(box, -2, -2, -2, -2)

            bbox = cBbox(text)
            if bbox:
                cCoords(rect, bbox[0] - 2, bbox[1], bbox[2] + 2, bbox[3])

            cLift(box)
            cLift(tog1)
            cLift(tog2)

            cDict[tog1] = (node, TOGGLE)
            cDict[tog2] = (node, TOGGLE)
            cDict[box] = (node, TOGGLE)
            cDict[text] = (node, TEXT)
            cDict[icon] = (node, ICON)

        nRows = min(visibleRows, len(nodes))
        if len(cList) > nRows:
            for row in range(nRows, len(cList)):
                for item in cList[row]:
                    if cDict.get(item):
                        del cDict[item]
                    cDel(item)

            self.canvasList = cList[:nRows]

        self._wait = False

    def setFont(self, font):

        self.font = font
        self._drawAfter()

    def _changeSizeAfter(self, event):

        self.after_idle(lambda: self._changeSize(event))

    def _changeSize(self, event=None):

        self.cancelEdits()

        if event:
            width = event.width
            height = event.height
        else:
            width = int(self.winfo_width())
            height = int(self.winfo_height())

        self.canvas.config(width=width, height=height)
        self.visibleRows = height / self.rowHeight

        bbox = self.canvas.bbox('all')

        if bbox:  # None at startup: nothing drawn
            canvasWidth = bbox[2] - bbox[0]
            if width < canvasWidth:
                size = self.scrollbarWidth
                end = width / float(canvasWidth)
                self.canvas.config(scrollregion="0 0 %s %s" %
                                   (canvasWidth, height))

                if self.visibleRows < len(self.nodes):
                    self.xScrollbar.place(x=0,
                                          y=height - size,
                                          width=width - size,
                                          height=size)
                else:
                    self.xScrollbar.place(x=0,
                                          y=height - size,
                                          width=width,
                                          height=size)

                self.xScrollbar.set(0, end)
            else:
                self.xScrollbar.place_forget()
        else:
            self.xScrollbar.place_forget()

        self._drawAfter()

    # Public

    def cancelEdits(self):

        self.menu.popdownMenu()

    def getSelected(self):

        return [node.object for node in self.nodes if node.isSelected]

    def select(self, object, expand=True):

        node = self.nodeDict.get(object)
        if node is not None:
            node.isSelected = True

            if expand:
                parent = node.parent

                while parent:
                    parent.isOpen = True
                    parent = parent.parent

            self._drawAfter()

    def selectAll(self, expand=True):

        for node in self.nodes:
            node.isSelected = True
            if expand:
                node.isOpen = expand

    def selectDeep(self, object, expand=True):

        node = self.nodeDict.get(object)
        if node is not None:
            stack = [
                node,
            ]
            nodes = []

            while stack:
                node2 = stack.pop()
                stack.extend(node2.children)
                nodes.append(node2)

            for node2 in nodes:
                if expand:
                    node2.isOpen = True
                node2.isSelected = True

            self._drawAfter()

    def setSelected(self, objects, expand=True):

        select = self.select
        for object in objects:
            select(object, expand)

    def expand(self, object):

        node = self.nodeDict.get(object)
        if node is not None:
            node.expand()
            self._drawAfter()

    def expandAll(self):

        for node in self.nodes:
            node.expand()

        self._drawAfter()

    def expandDeep(self, object):

        node = self.nodeDict.get(object)
        if node is not None:
            stack = [
                node,
            ]
            nodes = []

            while stack:
                node2 = stack.pop()
                stack.extend(node2.children)
                nodes.append(node2)

            for node2 in nodes:
                node2.iexpand()

            self._drawAfter()

    def contract(self, object):

        node = self.nodeDict.get(object)
        if node is not None:
            node.collapse()
            self._drawAfter()

    def contractAll(self):

        for node in self.nodes:
            node.collapse()

        self._drawAfter()

    def contractDeep(self, object):

        node = self.nodeDict.get(object)
        if node is not None:
            stack = [
                node,
            ]
            nodes = []

            while stack:
                node2 = stack.pop()
                stack.extend(node2.children)
                nodes.append(node2)

            for node2 in nodes:
                node2.collapse()

            self._drawAfter()

    def update(self,
               parents,
               objects,
               labels=None,
               icons=None,
               callbacks=None,
               editWidgets=None):

        n = len(parents)

        for test in (objects, callbacks, labels, icons, editWidgets):
            if test is not None:
                assert len(test) == n

        self.nodes = nodes = []
        self.nodeDict = nodeDict = {}
        self.openDict = {}

        if not labels:
            labels = [None] * n

        if not icons:
            icons = [None] * n

        if not callbacks:
            callbacks = [None] * n

        if not editWidgets:
            editWidgets = [[] for x in xrange(n)]

        for i, parent in enumerate(parents):
            object = objects[i]

            node = Node(self, parent, object, labels[i], icons[i],
                        callbacks[i], False, False, editWidgets[i])

        # JMCI FIXME: need to understand how the redraw works

        self._draw()

    def add(self,
            parent,
            object,
            label=None,
            icon='media-playback-stop',
            callback=None,
            isOpen=False,
            isSelected=False,
            editWidgets=None):

        nodeDict = self.nodeDict
        if nodeDict.get(object):
            nodeDict[object].delete()

            #raise Exception('Node already present for object %s' % object)

        node = Node(self, parent, object, label, icon, callback, isOpen,
                    isSelected, editWidgets)

    def remove(self, object):

        node = self.nodeDict.get(object)
        if node is not None:
            if self.openDict.get(object):
                del self.openDict[object]

            node.delete()

            self._drawAfter()