EventGhost/EventGhost

View on GitHub
eg/Classes/MainFrame/TreeCtrl.py

Summary

Maintainability
F
3 days
Test Coverage
# -*- coding: utf-8 -*-
#
# This file is part of EventGhost.
# Copyright © 2005-2020 EventGhost Project <http://www.eventghost.net/>
#
# EventGhost is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 2 of the License, or (at your option)
# any later version.
#
# EventGhost is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with EventGhost. If not, see <http://www.gnu.org/licenses/>.

import wx
from time import clock

# Local imports
import eg
from eg.Classes.TreeItem import (
    HINT_MOVE_AFTER,
    HINT_MOVE_BEFORE,
    HINT_MOVE_BEFORE_OR_AFTER,
    HINT_MOVE_EVERYWHERE,
    HINT_MOVE_INSIDE,
    HINT_NO_DROP,
)
from eg.WinApi.Dynamic import SendMessageTimeout

HITTEST_FLAG = (
    wx.TREE_HITTEST_ONITEMLABEL |
    wx.TREE_HITTEST_ONITEMICON |
    wx.TREE_HITTEST_ONITEMRIGHT
)

class DropSource(wx.DropSource):
    """
    This class represents a source for a drag and drop operation of the
    TreeCtrl.
    """
    @eg.AssertInMainThread
    def __init__(self, win, text):
        wx.DropSource.__init__(self, win)
        # create our own data format and use it in a
        # custom data object
        customData = wx.CustomDataObject("DragItem")
        customData.SetData(text)

        # Now make a data object for the text and also a composite
        # data object holding both of the others.
        textData = wx.TextDataObject(text.decode("UTF-8"))

        data = wx.DataObjectComposite()
        data.Add(textData)
        data.Add(customData)

        # We need to hold a reference to our data object, instead it could
        # be garbage collected
        self.data = data

        # And finally, create the drop source and begin the drag
        # and drop operation
        self.SetData(data)


class DropTarget(wx.PyDropTarget):
    """
    This class represents a target for a drag and drop operation of the
    TreeCtrl.
    """
    @eg.AssertInMainThread
    def __init__(self, treeCtrl):
        wx.PyDropTarget.__init__(self)
        self.treeCtrl = treeCtrl
        self.srcNode = eg.EventItem
        # specify the type of data we will accept
        textData = wx.TextDataObject()
        self.customData = wx.CustomDataObject(wx.CustomDataFormat("DragItem"))
        self.customData.SetData("")
        compositeData = wx.DataObjectComposite()
        compositeData.Add(textData)
        compositeData.Add(self.customData)
        self.SetDataObject(compositeData)
        self.lastHighlighted = None
        self.isExternalDrag = True
        self.whereToDrop = None
        self.lastDropTime = clock()
        self.lastTargetItemId = None
        timerId = wx.NewId()
        self.autoScrollTimer = wx.Timer(self.treeCtrl, timerId)
        self.treeCtrl.Bind(wx.EVT_TIMER, self.OnDragTimerEvent, id=timerId)

    @eg.AssertInMainThread
    def OnData(self, dummyX, dummyY, dragResult):
        """
        Overrides wx.DropTarget.OnData

        Called after OnDrop returns true. By default this will usually GetData
        and will return the suggested default.
        """
        self.OnLeave()
        # Called when OnDrop returns True.
        tree = self.treeCtrl
        #tree.ClearInsertMark()
        if self.isExternalDrag and self.whereToDrop is not None:
            # We need to get the data and do something with it.
            if self.GetData():
                # copy the data from the drag source to our data object
                #if tree.GetSelection().IsOk():
                #    tree.SelectItem(tree.GetSelection(), False)
                if self.lastHighlighted is not None:
                    tree.SetItemDropHighlight(self.lastHighlighted, False)
                if self.customData.GetDataSize() > 0:
                    label = self.customData.GetData()
                    self.customData.SetData("")
                    parent, pos = self.whereToDrop
                    eg.UndoHandler.NewEvent(tree.document).Do(
                        parent,
                        pos,
                        label
                    )
        # what is returned signals the source what to do
        # with the original data (move, copy, etc.)  In this
        # case we just return the suggested value given to us.
        return dragResult

    @eg.AssertInMainThread
    def OnDragOver(self, x, y, dummyDragResult):
        """
        Called when the mouse is being dragged over the drop target.
        """
        tree = self.treeCtrl
        self.whereToDrop = None

        # remove the last drop highlight if any
        if self.lastHighlighted is not None:
            tree.SetItemDropHighlight(self.lastHighlighted, False)
            self.lastHighlighted = None

        dstItemId, flags = tree.HitTest((x, y))

        if not (flags & HITTEST_FLAG):
            return wx.DragNone

        dstNode = tree.GetPyData(dstItemId)
        srcNode = self.srcNode

        # check if dstNode would get a parent of the srcNode
        dstParentNode = dstNode
        while dstParentNode is not None:
            if dstParentNode == srcNode:
                # they would, so avoid dropping the srcNode inside itself.
                return wx.DragNone
            dstParentNode = dstParentNode.parent

        insertionHint = dstNode.DropTest(srcNode)

        # expand a container, if the mouse is hold over it for some time
        if dstItemId == self.lastTargetItemId:
            if self.lastDropTime + 0.6 < clock():
                if (
                    dstNode.__class__ == dstNode.document.FolderItem or
                    insertionHint & HINT_MOVE_INSIDE
                ):
                    if not tree.IsExpanded(dstItemId):
                        tree.Expand(dstItemId)
        else:
            self.lastDropTime = clock()
            self.lastTargetItemId = dstItemId

        if insertionHint == HINT_NO_DROP:
            tree.ClearInsertMark()
            return wx.DragNone

        if insertionHint == HINT_MOVE_BEFORE_OR_AFTER:
            targetRect = tree.GetBoundingRect(dstItemId)
            if y > targetRect.y + targetRect.height / 2:
                insertionHint = HINT_MOVE_AFTER
            else:
                insertionHint = HINT_MOVE_BEFORE
        elif insertionHint == HINT_MOVE_EVERYWHERE:
            targetRect = tree.GetBoundingRect(dstItemId)
            if y < targetRect.y + targetRect.height / 4:
                insertionHint = HINT_MOVE_BEFORE
            elif y > targetRect.y + (targetRect.height / 4) * 3:
                insertionHint = HINT_MOVE_AFTER
            else:
                insertionHint = HINT_MOVE_INSIDE

        if insertionHint == HINT_MOVE_INSIDE:
            self.whereToDrop = self.OnWouldMoveInside(tree, dstNode, dstItemId)
        elif insertionHint == HINT_MOVE_BEFORE:
            self.whereToDrop = self.OnWouldMoveBefore(tree, dstNode, dstItemId)
        elif insertionHint == HINT_MOVE_AFTER:
            self.whereToDrop = self.OnWouldMoveAfter(tree, dstNode, dstItemId)
        return wx.DragMove

    @eg.AssertInMainThread
    def OnDragTimerEvent(self, dummyEvent):
        """
        Handles wx.EVT_TIMER, while a drag operation is in progress. It is
        responsible for the automatic scrolling if the mouse gets on the
        upper or lower bounds of the control.
        """
        tree = self.treeCtrl
        x, y = wx.GetMousePosition()
        treeRect = tree.GetScreenRect()
        if treeRect.x <= x <= treeRect.GetRight():
            if y < treeRect.y + 20:
                tree.ScrollLines(-1)
            elif y > treeRect.GetBottom() - 20:
                tree.ScrollLines(1)

    def OnEnter(self, dummyX, dummyY, dragResult):
        """
        Called when the mouse enters the drop target.
        """
        self.autoScrollTimer.Start(50)
        return dragResult

    @eg.AssertInMainThread
    def OnLeave(self):
        """
        Called when the mouse leaves the drop target.
        """
        self.treeCtrl.ClearInsertMark()
        self.autoScrollTimer.Stop()

    def OnWouldMoveAfter(self, tree, dstNode, dstItemId):
        """
        Handles the situation, that the dragged item would be inserted behind
        the currently highlighted item.
        """
        parent = dstNode.parent
        pos = parent.GetChildIndex(dstNode)
        for i in xrange(pos + 1, len(parent.childs)):
            child = parent.childs[i]
            if child.DropTest(self.srcNode) == HINT_MOVE_AFTER:
                dstItemId = tree.visibleNodes[child]
                pos += 1
        tree.SetInsertMark(dstItemId, 1)
        return (parent, pos + 1)

    def OnWouldMoveBefore(self, tree, dstNode, dstItemId):
        """
        Handles the situation, that the dragged item would be inserted before
        the currently highlighted item.
        """
        parent = dstNode.parent
        pos = parent.GetChildIndex(dstNode)
        for i in xrange(pos - 1, -1, -1):
            child = parent.childs[i]
            if child.DropTest(self.srcNode) == HINT_MOVE_BEFORE:
                dstItemId = tree.visibleNodes[child]
                pos -= 1
        tree.SetInsertMark(dstItemId, 0)
        return (parent, pos)

    def OnWouldMoveInside(self, tree, dstNode, dstItemId):
        """
        Handles the situation, that the dragged item would be inserted inside
        the currently highlighted item.
        """
        tree.SetItemDropHighlight(dstItemId, True)
        self.lastHighlighted = dstItemId
        whereToDrop = (dstNode, 0)
        for i in xrange(len(dstNode.childs) - 1, -1, -1):
            child = dstNode.childs[i]
            if child.DropTest(self.srcNode) & HINT_MOVE_AFTER:
                tree.SetInsertMark(tree.visibleNodes.get(child, 0), 1)
                whereToDrop = (dstNode, i + 1)
                break
        else:
            tree.ClearInsertMark()
        return whereToDrop


class EditControlProxy(object):
    def __init__(self, parent):
        self.parent = parent
        self.realControl = None

    def CanCut(self):
        return self.realControl.CanCut()

    def CanCopy(self):
        return self.realControl.CanCopy()

    #def CanPython(self):
    #    return self.realControl.CanPython()

    def CanPaste(self):
        return self.realControl.CanPaste()

    def CanDelete(self):
        start, end = self.realControl.GetSelection()
        return (start != end)

    def ClearControl(self):
        self.realControl = None

    def OnCmdCut(self):
        self.realControl.Cut()

    def OnCmdCopy(self):
        self.realControl.Copy()

    def OnCmdPython(self):
        eg.PrintNotice("DEBUG: TreeCtrl: OnCmdPython")
        #self.realControl.Copy()

    def OnCmdPaste(self):
        self.realControl.Paste()

    #def OnCmdClear(self):
    def OnCmdDelete(self):  # Pako
        start, end = self.realControl.GetSelection()
        if end - start == 0:
            end += 1
        self.realControl.Remove(start, end)
        return

    def SetControl(self):
        self.realControl = self.parent.GetEditControl()
        eg.Notify("FocusChange", self)


class TreeCtrl(wx.TreeCtrl):
    @eg.AssertInMainThread
    def __init__(self, parent, document, size=wx.DefaultSize):
        self.document = document
        self.root = None
        self.editLabelId = None
        self.insertionMark = None
        self.editControl = EditControlProxy(self)
        style = (
            wx.TR_HAS_BUTTONS |
            wx.TR_EDIT_LABELS |
            wx.TR_ROW_LINES |
            wx.CLIP_CHILDREN
        )
        wx.TreeCtrl.__init__(self, parent, size=size, style=style)
        self.SetImageList(eg.Icons.gImageList)
        self.hwnd = self.GetHandle()
        self.normalfont = self.GetFont()
        self.italicfont = self.GetFont()
        self.italicfont.SetStyle(wx.FONTSTYLE_ITALIC)
        self.Bind(wx.EVT_SET_FOCUS, self.OnGetFocusEvent)
        self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocusEvent)
        self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.OnItemExpandingEvent)
        self.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self.OnItemCollapsingEvent)
        self.Bind(wx.EVT_TREE_BEGIN_LABEL_EDIT, self.OnBeginLabelEditEvent)
        self.Bind(wx.EVT_TREE_END_LABEL_EDIT, self.OnEndLabelEditEvent)
        self.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnItemActivateEvent)
        self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDoubleClickEvent)
        self.Bind(wx.EVT_TREE_ITEM_RIGHT_CLICK, self.OnRightClickEvent)
        self.Bind(wx.EVT_TREE_ITEM_MENU, self.OnItemMenuEvent)
        self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnBeginDragEvent)
        self.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnSelectionChangedEvent)
        self.visibleNodes = {}
        self.expandedNodes = document.expandedNodes
        self.dropTarget = DropTarget(self)
        self.SetDropTarget(self.dropTarget)
        eg.Bind("NodeAdded", self.OnNodeAdded)
        eg.Bind("NodeDeleted", self.OnNodeDeleted)
        eg.Bind("NodeChanged", self.OnNodeChanged)
        eg.Bind("NodeSelected", self.OnNodeSelected)
        eg.Bind("DocumentNewRoot", self.OnNewRoot)
        if document.root:
            self.OnNewRoot(document.root)

    @eg.AssertInMainThread
    def ClearInsertMark(self):
        SendMessageTimeout(self.hwnd, 4378, 0, long(0), 1, 100, None)
        self.insertionMark = None

    @eg.AssertInMainThread
    def CollapseAllChildren(self, item_id):
        """
        Collapses all children of the selected item.
        """
        if not item_id.IsOk():
            return
        if not self.ItemHasChildren(item_id):
            return

        def collaps(item):
            child, cookie = self.GetFirstChild(item)
            while child.IsOk():
                if self.ItemHasChildren(child):
                    collaps(child)
                if self.IsExpanded(child):
                    self.Collapse(child)
                child, cookie = self.GetNextChild(item, cookie)
        self.Freeze()
        try:
            collaps(item_id)
        finally:
            if not self.IsVisible(item_id):
                self.EnsureVisible(item_id)
            self.Thaw()

    @eg.AssertInMainThread
    def CollapseAll(self):
        """
        Collapses all items in the tree.
        """
        self.Freeze()
        mainNodes = {self.root: self.GetRootItem()}
        for child in self.root.childs:
            itemId = self.visibleNodes[child]
            self.CollapseAndReset(itemId)
            mainNodes[child] = itemId
        self.visibleNodes = mainNodes
        self.expandedNodes.clear()
        self.expandedNodes.add(self.root)
        if not self.IsVisible(self.GetSelection()):
            self.EnsureVisible(self.GetSelection())
        self.Thaw()

    @eg.AssertInMainThread
    def CreateRoot(self, node):
        itemId = self.AddRoot(
            node.GetLabel(),
            node.imageIndex,
            -1,
            wx.TreeItemData(node)
        )
        self.visibleNodes[node] = itemId
        self.SetItemHasChildren(itemId, True)
        self.Expand(itemId)
        return itemId

    @eg.AssertInMainThread
    def CreateTreeItem(self, node, parentId):
        itemId = self.AppendItem(
            parentId,
            node.GetLabel(),
            node.imageIndex,
            -1,
            wx.TreeItemData(node)
        )
        node.SetAttributes(self, itemId)
        self.visibleNodes[node] = itemId
        if node.childs:
            self.SetItemHasChildren(itemId, True)
        if node in self.expandedNodes:
            self.Expand(itemId)
        return itemId

    @eg.AssertInMainThread
    def CreateTreeItemAt(self, node, parentId, parentNode, pos):
        if pos == -1 or pos >= len(parentNode.childs):
            return self.CreateTreeItem(node, parentId)
        else:
            itemId = self.InsertItemBefore(
                parentId,
                pos,
                node.GetLabel(),
                node.imageIndex,
                -1,
                wx.TreeItemData(node)
            )
            node.SetAttributes(self, itemId)
            self.visibleNodes[node] = itemId
            if node.childs:
                self.SetItemHasChildren(itemId, True)
            if node in self.expandedNodes:
                self.Expand(itemId)
            return itemId

    @eg.AssertInMainThread
    @eg.LogIt
    def Destroy(self):
        self.document.firstVisibleItem = self.GetFirstVisibleNode()
        eg.Unbind("NodeAdded", self.OnNodeAdded)
        eg.Unbind("NodeDeleted", self.OnNodeDeleted)
        eg.Unbind("NodeChanged", self.OnNodeChanged)
        eg.Unbind("NodeSelected", self.OnNodeSelected)
        eg.Unbind("DocumentNewRoot", self.OnNewRoot)

    @eg.AssertInMainThread
    def EditNodeLabel(self, node):
        self.SetFocus()
        self.EditLabel(self.visibleNodes[node])

    @eg.AssertInMainThread
    def ExpandAllChildren(self, item_id):
        """
        Expands all items in the tree.
        """
        if not item_id.IsOk():
            return
        self.Freeze()

        def _expand(item):
            child, cookie = self.GetFirstChild(item)
            while child.IsOk():
                if not self.IsExpanded(child):
                    self.Expand(child)
                _expand(child)
                child, cookie = self.GetNextChild(child, cookie)

        if not self.IsExpanded(item_id):
            self.Expand(item_id)

        try:
            _expand(item_id)
        finally:
            if not self.IsVisible(item_id):
                self.EnsureVisible(item_id)
            self.Thaw()

    @eg.AssertInMainThread
    def ExpandAll(self):
        """
        Expands all items in the tree.
        """
        self.ExpandAllChildren(self.GetRootItem())
        if not self.IsVisible(self.GetSelection()):
            self.EnsureVisible(self.GetSelection())

    @eg.AssertInMainThread
    def GetEditCmdState(self):
        node = self.GetSelectedNode()
        if node is None:
            return (False, False, False, False, False)
        return (
            node.CanCut(),
            node.CanCopy(),
            node.CanPython(),
            node.CanPaste(),
            node.CanDelete()
        )

    @eg.AssertInMainThread
    def GetFirstVisibleNode(self):
        """
        Returns the first currently visible node.
        """
        itemId = self.GetFirstVisibleItem()
        if not itemId.IsOk():
            return None
        return self.GetPyData(itemId)

    @eg.AssertInMainThread
    def GetSelectedNode(self):
        """
        Returns the currently selected node.
        """
        itemId = self.GetSelection()
        if not itemId.IsOk():
            return None
        return self.GetPyData(itemId)

    @eg.AssertInMainThread
    def SetInsertMark(self, treeItem, after):
        if treeItem:
            lParam = long(treeItem.m_pItem)
            if self.insertionMark == (lParam, after):
                return
            # TVM_SETINSERTMARK = 4378
            SendMessageTimeout(self.hwnd, 4378, after, lParam, 1, 100, None)
            self.insertionMark = (lParam, after)
        else:
            self.ClearInsertMark()

    @eg.AssertInMainThread
    def TraverseDelete(self, itemId):
        childId, cookie = self.GetFirstChild(itemId)
        while childId.IsOk():
            self.TraverseDelete(childId)
            node = self.GetPyData(childId)
            del self.visibleNodes[node]
            childId, cookie = self.GetNextChild(childId, cookie)

    #-------------------------------------------------------------------------
    # wx.Event Handlers
    #-------------------------------------------------------------------------

    @eg.AssertInMainThread
    @eg.LogItWithReturn
    def OnBeginDragEvent(self, event):
        """
        Handles wx.EVT_TREE_BEGIN_DRAG
        """
        srcItemId = event.GetItem()
        srcNode = self.GetPyData(srcItemId)
        if not srcNode.isMoveable:
            return
        self.SelectItem(srcItemId)
        dropTarget = self.dropTarget
        dropTarget.srcNode = srcNode
        dropTarget.isExternalDrag = False

        DropSource(self, srcNode.GetXmlString()).DoDragDrop(wx.Drag_AllowMove)

        dropTarget.srcNode = eg.EventItem
        dropTarget.isExternalDrag = True
        self.ClearInsertMark()
        if dropTarget.lastHighlighted is not None:
            self.SetItemDropHighlight(dropTarget.lastHighlighted, False)

        if dropTarget.whereToDrop is not None:
            parentNode, pos = dropTarget.whereToDrop
            eg.UndoHandler.MoveTo(self.document).Do(srcNode, parentNode, pos)

    @eg.AssertInMainThread
    @eg.LogIt
    def OnBeginLabelEditEvent(self, event):
        """
        Handles wx.EVT_TREE_BEGIN_LABEL_EDIT
        """
        node = self.GetPyData(event.GetItem())
        if not node.isRenameable or self.FindFocus() is not self:
            event.Veto()
            return
        self.editLabelId = event.GetItem()
        # we have to delay the notification, because the listener wants to
        # get the EditControl and it doesn't yet exist
        wx.CallAfter(self.editControl.SetControl)
        event.Skip()

    @eg.AssertInMainThread
    @eg.LogIt
    def OnEndLabelEditEvent(self, event):
        """
        Handles wx.EVT_TREE_END_LABEL_EDIT
        """
        self.editLabelId = None
        eg.Notify("FocusChange", self)
        itemId = event.GetItem()
        node = self.GetPyData(itemId)
        newLabel = event.GetLabel()
        if not event.IsEditCancelled() and node.GetLabel() != newLabel:
            eg.UndoHandler.Rename(self.document).Do(node, newLabel)
        event.Skip()

    @eg.AssertInMainThread
    def OnGetFocusEvent(self, event):
        """
        Handles wx.EVT_SET_FOCUS
        """
        eg.Notify("FocusChange", self)
        event.Skip(True)

    @eg.AssertInMainThread
    @eg.LogIt
    def OnItemActivateEvent(self, event):
        """
        Handles wx.EVT_TREE_ITEM_ACTIVATED
        """
        itemId = event.GetItem()
        if itemId.IsOk():
            node = self.GetPyData(itemId)
            if node.isConfigurable:
                wx.CallAfter(self.document.OnCmdConfigure, node)
        event.Skip()

    @eg.AssertInMainThread
    @eg.LogIt
    def OnItemCollapsingEvent(self, event):
        """
        Handles wx.EVT_TREE_ITEM_COLLAPSING
        """
        itemId = event.GetItem()
        if itemId == self.GetRootItem():
            event.Veto()
            return
        self.TraverseDelete(itemId)
        self.Unbind(wx.EVT_TREE_ITEM_COLLAPSING)
        self.Collapse(itemId)
        self.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self.OnItemCollapsingEvent)
        self.DeleteChildren(itemId)
        self.SetItemHasChildren(itemId)
        node = self.GetPyData(itemId)
        self.expandedNodes.discard(node)

    @eg.AssertInMainThread
    def OnItemExpandingEvent(self, event):
        """
        Handles wx.EVT_TREE_ITEM_EXPANDING
        """
        itemId = event.GetItem()
        self.ClearInsertMark()
        if not self.IsExpanded(itemId):
            node = self.GetPyData(itemId)
            self.expandedNodes.add(node)
            for subNode in node.childs:
                self.CreateTreeItem(subNode, itemId)

    @eg.AssertInMainThread
    def OnItemMenuEvent(self, event):
        """
        Handles wx.EVT_TREE_ITEM_MENU
        """
        self.SetFocus()
        frame = self.document.frame
        frame.SetupEditMenu(frame.popupMenu)
        self.PopupMenu(frame.popupMenu, event.GetPoint())
        event.Skip()

    @eg.AssertInMainThread
    def OnKillFocusEvent(self, event):
        """
        Handles wx.EVT_KILL_FOCUS
        """
        if self.editLabelId is None:
            eg.Notify("FocusChange", None)
        event.Skip(True)

    @eg.AssertInMainThread
    @eg.LogItWithReturn
    def OnLeftDoubleClickEvent(self, event):
        """
        Handles wx.EVT_LEFT_DCLICK
        """
        itemId = self.HitTest(event.GetPosition())[0]
        if itemId.IsOk():
            node = self.GetPyData(itemId)
            if node.isConfigurable:
                while wx.GetMouseState().LeftIsDown():
                    wx.GetApp().Yield()
                wx.CallLater(1, self.document.OnCmdConfigure, node)
        event.Skip()

    @eg.AssertInMainThread
    def OnRightClickEvent(self, event):
        """
        Handles wx.EVT_TREE_ITEM_RIGHT_CLICK
        """
        treeId = event.GetItem()
        self.SelectItem(treeId)

    @eg.AssertInMainThread
    def OnSelectionChangedEvent(self, event):
        """
        Handles wx.EVT_TREE_SEL_CHANGED
        """
        node = self.GetPyData(event.GetItem())
        self.document.selection = node
        eg.Notify("SelectionChange", node)
        event.Skip()

    #-------------------------------------------------------------------------
    # eg.Notify Handlers
    #-------------------------------------------------------------------------

    @eg.AssertInMainThread
    def OnNewRoot(self, root):
        """
        Handles eg.Notify("DocumentNewRoot")
        """
        self.Freeze()
        try:
            if self.root:
                self.DeleteAllItems()
                self.visibleNodes.clear()
            self.root = root
            rootId = self.CreateRoot(root)
            self.Expand(rootId)
            selectedNode = self.document.selection
            if selectedNode in self.visibleNodes:
                itemId = self.visibleNodes[selectedNode]
                self.SelectItem(itemId)
            firstVisibleItem = self.document.firstVisibleItem
            if firstVisibleItem:
                if firstVisibleItem in self.visibleNodes:
                    itemId = self.visibleNodes[firstVisibleItem]
                    self.ScrollTo(itemId)
        finally:
            self.Thaw()

    @eg.AssertInMainThread
    @eg.LogIt
    def OnNodeAdded(self, (node, pos)):
        """
        Handles eg.Notify("NodeAdded")
        """
        parentNode = node.parent
        if parentNode in self.visibleNodes:
            parentId = self.visibleNodes[parentNode]
            if len(parentNode.childs):
                self.SetItemHasChildren(parentId)
            if self.IsExpanded(parentId):
                self.CreateTreeItemAt(
                    node,
                    parentId,
                    parentNode,
                    pos
                )
            elif parentNode in self.expandedNodes:
                self.Expand(parentId)

    @eg.AssertInMainThread
    def OnNodeChanged(self, node):
        """
        Handles eg.Notify("NodeChanged")
        """
        if node not in self.visibleNodes:
            return
        itemId = self.visibleNodes[node]
        self.SetItemText(itemId, node.GetLabel())
        self.SetItemImage(itemId, node.imageIndex)
        self.SetItemHasChildren(itemId, bool(node.childs))
        node.SetAttributes(self, itemId)
        if node in self.expandedNodes:
            if not self.IsExpanded(itemId):
                self.Expand(itemId)
        else:
            if self.IsExpanded(itemId):
                self.Collapse(itemId)

    @eg.AssertInMainThread
    @eg.LogIt
    def OnNodeDeleted(self, (node, parent)):
        """
        Handles eg.Notify("NodeDeleted")
        """
        if parent in self.visibleNodes:
            if node in self.visibleNodes:
                itemId = self.visibleNodes.pop(node)
                self.Delete(itemId)
            if len(parent.childs) == 0:
                self.SetItemHasChildren(self.visibleNodes[parent], False)
                self.expandedNodes.discard(parent)
        self.expandedNodes.discard(node)
        selected = self.GetSelection()
        self.UnselectAll()
        self.SelectItem(selected)

    @eg.AssertInMainThread
    def OnNodeSelected(self, node):
        """
        Handles eg.Notify("NodeSelected")
        """
        if node not in self.visibleNodes:
            path = node.GetPath()
            if path is None:
                return
            parent = self.root
            for pos in path[:-1]:
                childNode = parent.childs[pos]
                if (
                    childNode not in self.visibleNodes and
                    parent not in self.expandedNodes
                ):
                        self.Expand(self.visibleNodes[parent])
                parent = childNode
            if parent not in self. expandedNodes:
                self.Expand(self.visibleNodes[parent])
        self.SelectItem(self.visibleNodes[node])