]> git.saurik.com Git - wxWidgets.git/commitdiff
Added wx.lib.combotreebox from Frank Niessink
authorRobin Dunn <robin@alldunn.com>
Mon, 21 Aug 2006 18:32:48 +0000 (18:32 +0000)
committerRobin Dunn <robin@alldunn.com>
Mon, 21 Aug 2006 18:32:48 +0000 (18:32 +0000)
git-svn-id: https://svn.wxwidgets.org/svn/wx/wxWidgets/trunk@40731 c3d73ce0-8a6f-49c7-b76d-6d57e0e08775

wxPython/demo/ComboTreeBox.py [new file with mode: 0644]
wxPython/demo/Main.py
wxPython/docs/CHANGES.txt
wxPython/wx/lib/combotreebox.py [new file with mode: 0644]

diff --git a/wxPython/demo/ComboTreeBox.py b/wxPython/demo/ComboTreeBox.py
new file mode 100644 (file)
index 0000000..ae91b1f
--- /dev/null
@@ -0,0 +1,69 @@
+import wx
+from wx.lib.combotreebox import ComboTreeBox
+
+
+#---------------------------------------------------------------------------
+
+
+class TestComboTreeBox(wx.Panel):
+    def __init__(self, parent, log):
+        super(TestComboTreeBox, self).__init__(parent)
+        self.log = log
+        panelSizer = wx.FlexGridSizer(2, 2)
+        panelSizer.AddGrowableCol(1)
+        for style, labelText in [(0, 'Default style:'), 
+                                 (wx.CB_READONLY, 'Read-only style:')]:
+            label = wx.StaticText(self, label=labelText)
+            panelSizer.Add(label, flag=wx.ALL|wx.ALIGN_CENTER_VERTICAL, 
+                           border=5)
+            comboBox = self._createComboTreeBox(style)
+            panelSizer.Add(comboBox, flag=wx.EXPAND|wx.ALL, border=5)
+        self.SetSizerAndFit(panelSizer)
+
+    def _createComboTreeBox(self, style):
+        comboBox = ComboTreeBox(self, style=style)
+        self._bindEventHandlers(comboBox)
+        for i in range(5):
+            child = comboBox.Append('Item %d'%i)
+            for j in range(5):
+                grandChild = comboBox.Append('Item %d.%d'%(i,j), child)
+                for k in range(5):
+                    comboBox.Append('Item %d.%d.%d'%(i,j, k), grandChild)
+        return comboBox
+        
+    def _bindEventHandlers(self, comboBox):
+        for eventType, handler in [(wx.EVT_COMBOBOX, self.OnItemSelected), 
+                                   (wx.EVT_TEXT, self.OnItemEntered)]:
+            comboBox.Bind(eventType, handler)
+
+    def OnItemSelected(self, event):
+        self.log.WriteText('You selected: %s\n'%event.GetString())
+        event.Skip()
+
+    def OnItemEntered(self, event):
+        self.log.WriteText('You entered: %s\n'%event.GetString())
+        event.Skip()
+
+
+#---------------------------------------------------------------------------
+
+
+def runTest(frame, nb, log):
+    win = TestComboTreeBox(nb, log)
+    return win
+
+
+#---------------------------------------------------------------------------
+
+
+overview = wx.lib.combotreebox.__doc__
+
+
+#---------------------------------------------------------------------------
+
+
+if __name__ == '__main__':
+    import sys, os
+    import run
+    run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:])
+
index 041678483a8f747668fe989452f122c80ac96797..e8791ef031fb4fbeb07582b7bbb59a253d740309 100644 (file)
@@ -51,6 +51,7 @@ _treeList = [
         'AUI_DockingWindowMgr',
         'AUI_Notebook',
         'CheckListCtrlMixin',
+        'ComboTreeBox',
         'Pickers',
         'PseudoDC',
         'RichTextCtrl',
@@ -140,6 +141,7 @@ _treeList = [
     ('Custom Controls', [
         'AnalogClock',
         'ColourSelect',
+        'ComboTreeBox',
         'Editor',
         'GenericButtons',
         'GenericDirCtrl',
index a670ac54d0fe926bc521564a8d8981a918533feb..991c9a76b83f9ddf966f01fa21036a0b32b5600d 100644 (file)
@@ -182,6 +182,16 @@ Added CanSetTransparent and SetTransparent methods to the
 wx.TopLevelWindow class, with implementations (so far) for wxMSW and
 wxMac. 
 
+SetDefaultItem() and GetDefaultItem() are now members of
+wx.TopLevelWindow, not wx.Panel.
+
+wxGTK: Stock items (icons) will be used for menu items with stock
+IDs. 
+
+Added wx.lib.combotreebox from Frank Niessink
+
+
+
 
 
 
diff --git a/wxPython/wx/lib/combotreebox.py b/wxPython/wx/lib/combotreebox.py
new file mode 100644 (file)
index 0000000..bdfd310
--- /dev/null
@@ -0,0 +1,820 @@
+""" 
+ComboTreeBox provides a ComboBox that pops up a tree instead of a list. 
+
+ComboTreeBox tries to provide the same interface as ComboBox as much as
+possible. However, whereas the ComboBox widget uses indices to access 
+items in the list of choices, ComboTreeBox uses TreeItemId's instead. If
+you add an item to the ComboTreeBox (using Append or Insert), the 
+TreeItemId associated with the added item is returned. You can then use
+that TreeItemId to add items as children of that first item. For
+example:
+
+>>> from wx.lib.combotreebox import ComboTreeBox
+>>> combo = ComboTreeBox(parent)
+>>> item1 = combo.Append('Item 1') # Add a root item
+>>> item1a = combo.Append('Item 1a', parent=item1) # Add a child to item1
+
+You can also add client data to each of the items like this:
+>>> item1 = combo.Append('Item 1', clientData=somePythonObject)
+>>> item1a = combo.Append('Item 1a', parent=item1, 
+...                       clientData=someOtherPythonObject)
+
+And later fetch the client data like this:
+>>> somePythonObject = combo.GetClientData(item1)
+
+To get the client data of the currently selected item (if any):
+>>> currentItem = combo.GetSelection()
+>>> if currentItem:
+>>>     somePythonObject = combo.GetClientData(currentItem)
+
+Supported styles are the same as for ComboBox, i.e. wx.CB_READONLY and
+wx.CB_SORT. Provide them as usual:
+>>> combo = ComboTreeBox(parent, style=wx.CB_READONLY|wx.CB_SORT)
+
+Supported platforms: wxMSW and wxMAC natively, wxGTK by means of a
+workaround.
+
+Author: Frank Niessink <frank@niessink.com>
+Copyright 2006, Frank Niessink
+License: wxWidgets license
+Version: 0.8
+Date: August 21, 2006
+"""
+
+import wx
+
+__all__ = ['ComboTreeBox'] # Export only the ComboTreeBox widget
+
+
+# ---------------------------------------------------------------------------
+
+
+class IterableTreeCtrl(wx.TreeCtrl):
+    """ 
+    TreeCtrl is the same as wx.TreeCtrl, with a few convenience methods 
+    added for easier navigation of items. """
+
+    def GetPreviousItem(self, item):
+        """ 
+        GetPreviousItem(self, TreeItemId item) -> TreeItemId
+
+        Returns the item that is on the line immediately above item 
+        (as is displayed when the tree is fully expanded). The returned 
+        item is invalid if item is the first item in the tree.
+        """
+        previousSibling = self.GetPrevSibling(item)
+        if previousSibling:
+            return self.GetLastChildRecursively(previousSibling)
+        else:
+            parent = self.GetItemParent(item)
+            if parent == self.GetRootItem() and \
+                (self.GetWindowStyle() & wx.TR_HIDE_ROOT):
+                # Return an invalid item, because the root item is hidden
+                return previousSibling
+            else:
+                return parent
+
+    def GetNextItem(self, item):
+        """
+        GetNextItem(self, TreeItemId item) -> TreeItemId
+
+        Returns the item that is on the line immediately below item
+        (as is displayed when the tree is fully expanded). The returned
+        item is invalid if item is the last item in the tree.
+        """
+        if self.ItemHasChildren(item):
+            firstChild, cookie = self.GetFirstChild(item)
+            return firstChild
+        else:
+            return self.GetNextSiblingRecursively(item)
+
+    def GetFirstItem(self):
+        """
+        GetFirstItem(self) -> TreeItemId
+
+        Returns the very first item in the tree. This is the root item
+        unless the root item is hidden. In that case the first child of
+        the root item is returned, if any. If the tree is empty, an
+        invalid tree item is returned.
+        """
+        rootItem = self.GetRootItem()
+        if rootItem and (self.GetWindowStyle() & wx.TR_HIDE_ROOT):
+            firstChild, cookie = self.GetFirstChild(rootItem)
+            return firstChild
+        else:
+            return rootItem
+
+    def GetLastChildRecursively(self, item):
+        """
+        GetLastChildRecursively(self, TreeItemId item) -> TreeItemId
+
+        Returns the last child of the last child ... of item. If item
+        has no children, item itself is returned. So the returned item
+        is always valid, assuming a valid item has been passed. 
+        """
+        lastChild = item
+        while self.ItemHasChildren(lastChild):
+            lastChild = self.GetLastChild(lastChild)
+        return lastChild
+
+    def GetNextSiblingRecursively(self, item):
+        """ 
+        GetNextSiblingRecursively(self, TreeItemId item) -> TreeItemId
+
+        Returns the next sibling of item if it has one. If item has no
+        next sibling the next sibling of the parent of item is returned. 
+        If the parent has no next sibling the next sibling of the parent 
+        of the parent is returned, etc. If none of the ancestors of item
+        has a next sibling, an invalid item is returned. 
+        """
+        if item == self.GetRootItem():
+            return wx.TreeItemId() # Return an invalid TreeItemId
+        nextSibling = self.GetNextSibling(item)
+        if nextSibling:
+            return nextSibling
+        else:
+            parent = self.GetItemParent(item)
+            return self.GetNextSiblingRecursively(parent)
+
+    def GetSelection(self):
+        """ Extend GetSelection to never return the root item if the
+            root item is hidden. """
+        selection = super(IterableTreeCtrl, self).GetSelection()
+        if selection == self.GetRootItem() and \
+            (self.GetWindowStyle() & wx.TR_HIDE_ROOT):
+            return wx.TreeItemId() # Return an invalid TreeItemId
+        else:
+            return selection
+
+
+# ---------------------------------------------------------------------------
+
+
+class BasePopupFrame(wx.MiniFrame):
+    """ 
+    BasePopupFrame is the base class for platform specific
+    versions of the PopupFrame. The PopupFrame is the frame that 
+    is popped up by ComboTreeBox. It contains the tree of items 
+    that the user can select one item from. Upon selection, or 
+    when focus is lost, the frame is hidden. """
+
+    def __init__(self, parent):
+        super(BasePopupFrame, self).__init__(parent,
+            style=wx.DEFAULT_FRAME_STYLE & wx.FRAME_FLOAT_ON_PARENT &
+                  ~(wx.RESIZE_BORDER | wx.CAPTION)) 
+        self._createInterior()
+        self._layoutInterior()
+        self._bindEventHandlers()
+
+    def _createInterior(self):
+        self._tree = IterableTreeCtrl(self, 
+            style=wx.TR_HIDE_ROOT|wx.TR_LINES_AT_ROOT|wx.TR_HAS_BUTTONS)
+        self._tree.AddRoot('Hidden root node')
+
+    def _layoutInterior(self):
+        frameSizer = wx.BoxSizer(wx.HORIZONTAL)
+        frameSizer.Add(self._tree, flag=wx.EXPAND, proportion=1)
+        self.SetSizerAndFit(frameSizer)
+
+    def _bindEventHandlers(self):
+        self._tree.Bind(wx.EVT_CHAR, self.OnChar)
+        self._tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnItemActivated)
+        self._tree.Bind(wx.EVT_LEFT_DOWN, self.OnMouseClick)
+
+    def _bindKillFocus(self):
+        self._tree.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
+
+    def _unbindKillFocus(self):
+        self._tree.Unbind(wx.EVT_KILL_FOCUS)
+
+    def OnKillFocus(self, event):
+        # We hide the frame rather than destroy it, so it can be 
+        # popped up again later:
+        self.Hide()
+        self.GetParent().NotifyNoItemSelected()
+        event.Skip()
+
+    def OnChar(self, keyEvent):
+        if self._keyShouldHidePopup(keyEvent):
+            self.Hide()
+            self.GetParent().NotifyNoItemSelected()
+        keyEvent.Skip()
+
+    def _keyShouldHidePopup(self, keyEvent):
+        return keyEvent.GetKeyCode() == wx.WXK_ESCAPE
+
+    def OnMouseClick(self, event):
+        item, flags = self._tree.HitTest(event.GetPosition())
+        if item and (flags & wx.TREE_HITTEST_ONITEMLABEL):
+            self._tree.SelectItem(item)
+            self.Hide()
+            self.GetParent().NotifyItemSelected(self._tree.GetItemText(item))
+        else:
+            event.Skip()
+
+    def OnItemActivated(self, event):
+        item = event.GetItem()
+        self.Hide()
+        self.GetParent().NotifyItemSelected(self._tree.GetItemText(item))
+
+    def Show(self):
+        self._bindKillFocus()
+        wx.CallAfter(self._tree.SetFocus)
+        super(BasePopupFrame, self).Show()
+
+    def Hide(self):
+        self._unbindKillFocus()
+        super(BasePopupFrame, self).Hide()
+
+    def GetTree(self):
+        return self._tree
+
+
+class MSWPopupFrame(BasePopupFrame):
+    def Show(self):
+        # Comply with the MS Windows Combobox behaviour: if the text in
+        # the text field is not in the tree, the first item in the tree
+        # is selected.
+        if not self._tree.GetSelection():
+            self._tree.SelectItem(self._tree.GetFirstItem())
+        super(MSWPopupFrame, self).Show()
+
+
+class MACPopupFrame(BasePopupFrame):
+    def _bindKillFocus(self):
+        # On wxMac, the kill focus event doesn't work, but the
+        # deactivate event does:
+        self.Bind(wx.EVT_ACTIVATE, self.OnKillFocus)
+
+    def _unbindKillFocus(self):
+        self.Unbind(wx.EVT_ACTIVATE)
+
+    def OnKillFocus(self, event):
+        if not event.GetActive(): # We received a deactivate event
+            self.Hide()
+            wx.CallAfter(self.GetParent().NotifyNoItemSelected)
+        event.Skip()
+
+
+class GTKPopupFrame(BasePopupFrame):
+    def _keyShouldHidePopup(self, keyEvent):
+        # On wxGTK, Alt-Up also closes the popup:
+        return super(GTKPopupFrame, self)._keyShouldHidePopup(keyEvent) or \
+            (keyEvent.AltDown() and keyEvent.GetKeyCode() == wx.WXK_UP)
+
+
+# ---------------------------------------------------------------------------
+
+
+class BaseComboTreeBox(object):
+    """ BaseComboTreeBox is the base class for platform specific
+        versions of the ComboTreeBox. """
+
+    def __init__(self, *args, **kwargs):
+        style = kwargs.pop('style', 0)
+        if style & wx.CB_READONLY:
+            style &= ~wx.CB_READONLY # We manage readonlyness ourselves
+            self._readOnly = True
+        else:
+            self._readOnly = False
+        if style & wx.CB_SORT:
+            style &= ~wx.CB_SORT # We manage sorting ourselves
+            self._sort = True
+        else:
+            self._sort = False
+        super(BaseComboTreeBox, self).__init__(style=style, *args, **kwargs)
+        self._createInterior()
+        self._layoutInterior()
+        self._bindEventHandlers()
+
+    # Methods to construct the widget.
+
+    def _createInterior(self):
+        self._popupFrame = self._createPopupFrame()
+        self._text = self._createTextCtrl()
+        self._button = self._createButton()
+        self._tree = self._popupFrame.GetTree()
+
+    def _createTextCtrl(self):
+        return self # By default, the text control is the control itself.
+
+    def _createButton(self):
+        return self # By default, the dropdown button is the control itself.
+
+    def _createPopupFrame(self):
+        # It is a subclass responsibility to provide the right PopupFrame, 
+        # depending on platform:
+        raise NotImplementedError 
+
+    def _layoutInterior(self):
+        pass # By default, there is no layout to be done.
+
+    def _bindEventHandlers(self):
+        for eventSource, eventType, eventHandler in self._eventsToBind():
+            eventSource.Bind(eventType, eventHandler)
+
+    def _eventsToBind(self):
+        """ 
+        _eventsToBind(self) -> [(eventSource, eventType, eventHandler), ...] 
+            
+        _eventsToBind returns a list of eventSource, eventType,
+        eventHandlers tuples that will be bound. This method can be 
+        extended to bind additional events. In that case, don't 
+        forget to call _eventsToBind on the super class. """
+        return [(self._text, wx.EVT_KEY_DOWN, self.OnKeyDown),
+                (self._text, wx.EVT_TEXT, self.OnText),
+                (self._button, wx.EVT_BUTTON, self.OnMouseClick)]
+
+    # Event handlers
+
+    def OnMouseClick(self, event):
+        self.Popup()
+        # Note that we don't call event.Skip() to prevent popping up the
+        # ComboBox's own box.
+
+    def OnKeyDown(self, keyEvent):
+        if self._keyShouldNavigate(keyEvent):
+            self._navigateUpOrDown(keyEvent)
+        elif self._keyShouldPopUpTree(keyEvent):
+            self.Popup()
+        else:
+            keyEvent.Skip()
+
+    def _keyShouldPopUpTree(self, keyEvent):
+        return (keyEvent.AltDown() or keyEvent.MetaDown()) and \
+                keyEvent.GetKeyCode() == wx.WXK_DOWN
+
+    def _keyShouldNavigate(self, keyEvent):
+        return keyEvent.GetKeyCode() in (wx.WXK_DOWN, wx.WXK_UP) and not \
+            self._keyShouldPopUpTree(keyEvent)
+
+    def _navigateUpOrDown(self, keyEvent):
+        item = self.GetSelection()
+        if item:
+            navigationMethods = {wx.WXK_DOWN: self._tree.GetNextItem, 
+                                 wx.WXK_UP: self._tree.GetPreviousItem}
+            getNextItem = navigationMethods[keyEvent.GetKeyCode()]
+            nextItem = getNextItem(item)
+        else:
+            nextItem = self._tree.GetFirstItem()
+        if nextItem:
+            self.SetSelection(nextItem)
+
+    def OnText(self, event):
+        event.Skip()
+        item = self.FindString(self._text.GetValue())
+        if item:
+            if self._tree.GetSelection() != item:
+                self._tree.SelectItem(item)
+        else:
+            self._tree.Unselect()
+
+    # Methods called by the PopupFrame, to let the ComboTreeBox know
+    # about what the user did.
+
+    def NotifyItemSelected(self, text):
+        """ Simulate selection of an item by the user. This is meant to 
+            be called by the PopupFrame when the user selects an item. """
+        self._text.SetValue(text)
+        self._postComboBoxSelectedEvent(text)
+        self.SetFocus()
+
+    def _postComboBoxSelectedEvent(self, text):
+        """ Simulate a selection event. """ 
+        event = wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED, 
+                                self.GetId())
+        event.SetString(text)
+        self.GetEventHandler().ProcessEvent(event)
+
+    def NotifyNoItemSelected(self):
+        """ This is called by the PopupFrame when the user closes the 
+            PopupFrame, without selecting an item.  """
+        self.SetFocus()
+
+    # Misc methods, not part of the ComboBox API.
+
+    def Popup(self):
+        """ 
+        Popup(self)
+
+        Pops up the frame with the tree.
+        """
+        comboBoxSize = self.GetSize()
+        x, y = self.GetParent().ClientToScreen(self.GetPosition())
+        y += comboBoxSize[1]
+        width = comboBoxSize[0]
+        height = 300
+        self._popupFrame.SetDimensions(x, y, width, height)
+        # On wxGTK, when the Combobox width has been increased a call 
+        # to SetMinSize is needed to force a resize of the popupFrame: 
+        self._popupFrame.SetMinSize((width, height)) 
+        self._popupFrame.Show()
+    def GetTree(self):
+        """
+        GetTree(self) -> wx.TreeCtrl
+
+        Returns the tree control that is popped up. 
+        """
+        return self._popupFrame.GetTree()
+
+    def FindClientData(self, clientData, parent=None):
+        """ 
+        FindClientData(self, PyObject clientData, TreeItemId parent=None) 
+            -> TreeItemId
+        
+        Finds the *first* item in the tree with client data equal to the
+        given clientData. If no such item exists, an invalid item is
+        returned. 
+        """
+        parent = parent or self._tree.GetRootItem()
+        child, cookie = self._tree.GetFirstChild(parent)
+        while child:
+            if self.GetClientData(child) == clientData:
+                return child
+            else:
+                result = self.FindClientData(clientData, child)
+                if result:
+                    return result
+            child, cookie = self._tree.GetNextChild(parent, cookie)
+        return child
+
+    def SetClientDataSelection(self, clientData):
+        """
+        SetClientDataSelection(self, PyObject clientData) -> bool
+
+        Selects the item with the provided clientData in the control. 
+        Returns True if the item belonging to the clientData has been 
+        selected, False if it wasn't found in the control.
+        """
+        item = self.FindClientData(clientData)
+        if item:
+            self._tree.SelectItem(item)
+            return True
+        else:
+            return False
+
+    # The following methods are all part of the ComboBox API (actually
+    # the ControlWithItems API) and have been adapted to take TreeItemIds 
+    # as parameter and return TreeItemIds, rather than indices.
+
+    def Append(self, itemText, parent=None, clientData=None):
+        """ 
+        Append(self, String itemText, TreeItemId parent=None, PyObject
+               clientData=None) -> TreeItemId
+
+        Adds the itemText to the control, associating the given clientData 
+        with the item if not None. If parent is None, itemText is added
+        as a root item, else itemText is added as a child item of
+        parent. The return value is the TreeItemId of the newly added
+        item. """    
+        if parent is None:
+            parent = self._tree.GetRootItem()
+        item = self._tree.AppendItem(parent, itemText, 
+                                     data=wx.TreeItemData(clientData))
+        if self._sort:
+            self._tree.SortChildren(parent)
+        return item
+
+    def Clear(self):
+        """
+        Clear(self)
+        
+        Removes all items from the control.
+        """
+        return self._tree.DeleteAllItems()
+        
+    def Delete(self, item):
+        """
+        Delete(self, TreeItemId item)
+
+        Deletes the item from the control. 
+        """
+        return self._tree.Delete(item)
+
+    def FindString(self, string, parent=None):
+        """ 
+        FindString(self, String string, TreeItemId parent=None) -> TreeItemId
+        
+        Finds the *first* item in the tree with a label equal to the
+        given string. If no such item exists, an invalid item is
+        returned. 
+        """
+        parent = parent or self._tree.GetRootItem()
+        child, cookie = self._tree.GetFirstChild(parent)
+        while child:
+            if self._tree.GetItemText(child) == string:
+                return child
+            else:
+                result = self.FindString(string, child)
+                if result:
+                    return result
+            child, cookie = self._tree.GetNextChild(parent, cookie)
+        return child
+
+    def GetSelection(self):
+        """
+        GetSelection(self) -> TreeItemId
+
+        Returns the TreeItemId of the selected item or an invalid item
+        if no item is selected.
+        """
+        selectedItem = self._tree.GetSelection()
+        if selectedItem and selectedItem != self._tree.GetRootItem():
+            return selectedItem
+        else:
+            return self.FindString(self.GetValue())
+
+    def GetString(self, item):
+        """
+        GetString(self, TreeItemId item) -> String
+
+        Returns the label of the given item.
+        """
+        if item:
+            return self._tree.GetItemText(item)
+        else:
+            return ''
+
+    def GetStringSelection(self):
+        """
+        GetStringSelection(self) -> String
+
+        Returns the label of the selected item or an empty string if no item 
+        is selected.
+        """
+        return self.GetValue()
+
+    def Insert(self, itemText, previous=None, parent=None, clientData=None):
+        """
+        Insert(self, String itemText, TreeItemId previous=None, TreeItemId
+               parent=None, PyObject clientData=None) -> TreeItemId
+
+        Insert an item into the control before the ``previous`` item 
+        and/or as child of the ``parent`` item. The itemText is associated 
+        with clientData when not None.
+        """
+        data = wx.TreeItemData(clientData)
+        if parent is None:
+            parent = self._tree.GetRootItem()
+        if previous is None:
+            item = self._tree.InsertItemBefore(parent, 0, itemText, data=data)
+        else:
+            item = self._tree.InsertItem(parent, previous, itemText, data=data)
+        if self._sort:
+            self._tree.SortChildren(parent)
+        return item
+
+    def IsEmpty(self):
+        """
+        IsEmpty(self) -> bool
+
+        Returns True if the control is empty or False if it has some items.
+        """
+        return self.GetCount() == 0
+
+    def GetCount(self):
+        """
+        GetCount(self) -> int
+
+        Returns the number of items in the control.
+        """
+        # Note: We don't need to substract 1 for the hidden root item, 
+        # because the TreeCtrl does that for us
+        return self._tree.GetCount() 
+
+    def SetSelection(self, item):
+        """ 
+        SetSelection(self, TreeItemId item) 
+
+        Sets the provided item to be the selected item.
+        """
+        self._tree.SelectItem(item)
+        self._text.SetValue(self._tree.GetItemText(item))
+        
+    Select = SetSelection
+
+    def SetString(self, item, string):
+        """
+        SetString(self, TreeItemId item, String string)
+
+        Sets the label for the provided item.
+        """
+        self._tree.SetItemText(item, string)
+        if self._sort:
+            self._tree.SortChildren(self._tree.GetItemParent(item))
+
+    def SetStringSelection(self, string):
+        """
+        SetStringSelection(self, String string) -> bool
+
+        Selects the item with the provided string in the control. 
+        Returns True if the provided string has been selected, False if
+        it wasn't found in the control.
+        """
+        item = self.FindString(string)
+        if item:
+            if self._text.GetValue() != string:
+                self._text.SetValue(string)
+            self._tree.SelectItem(item)
+            return True
+        else:
+            return False
+
+    def GetClientData(self, item):
+        """
+        GetClientData(self, TreeItemId item) -> PyObject
+
+        Returns the client data associated with the given item, if any.
+        """
+        return self._tree.GetItemPyData(item)
+
+    def SetClientData(self, item, clientData):
+        """
+        SetClientData(self, TreeItemId item, PyObject clientData)
+
+        Associate the given client data with the provided item.
+        """
+        self._tree.SetItemPyData(item, clientData)
+
+    def GetValue(self):
+        """
+        GetValue(self) -> String
+
+        Returns the current value in the combobox text field.
+        """
+        if self._text == self:
+            return super(BaseComboTreeBox, self).GetValue()
+        else:
+            return self._text.GetValue()
+
+    def SetValue(self, value):
+        """
+        SetValue(self, String value)
+
+        Sets the text for the combobox text field.
+
+        NB: For a combobox with wxCB_READONLY style the string must be
+        in the combobox choices list, otherwise the call to SetValue()
+        is ignored.
+        """
+        item = self._tree.GetSelection()
+        if not item or self._tree.GetItemText(item) != value:
+            item = self.FindString(value)
+        if self._readOnly and not item:
+            return
+        if self._text == self:
+            super(BaseComboTreeBox, self).SetValue(value)
+        else:
+            self._text.SetValue(value)
+        if item:
+            if self._tree.GetSelection() != item:
+                self._tree.SelectItem(item)
+        else:
+            self._tree.Unselect()
+
+
+class NativeComboTreeBox(BaseComboTreeBox, wx.ComboBox):
+    """ NativeComboTreeBox, and any subclass, uses the native ComboBox as 
+        basis, but prevent it from popping up its drop down list and
+        instead pops up a PopupFrame containing a tree of items. """
+
+    def _eventsToBind(self):
+        events = super(NativeComboTreeBox, self)._eventsToBind() 
+        # Bind all mouse click events to self.OnMouseClick so we can
+        # intercept those events and prevent the native Combobox from
+        # popping up its list of choices.
+        for eventType in (wx.EVT_LEFT_DOWN, wx.EVT_LEFT_DCLICK, 
+                          wx.EVT_MIDDLE_DOWN, wx.EVT_MIDDLE_DCLICK, 
+                          wx.EVT_RIGHT_DOWN, wx.EVT_RIGHT_DCLICK):
+            events.append((self._button, eventType, self.OnMouseClick))
+        if self._readOnly:
+            events.append((self, wx.EVT_CHAR, self.OnChar))
+        return events 
+
+    def OnChar(self, event):
+        # OnChar is only called when in read only mode. We don't call 
+        # event.Skip() on purpose, to prevent the characters from being 
+        # displayed in the text field.
+        pass
+
+
+class MSWComboTreeBox(NativeComboTreeBox):
+    """ MSWComboTreeBox adds one piece of functionality as compared to
+        NativeComboTreeBox: when the user browses through the tree, the
+        ComboTreeBox's text field is continuously updated to show the
+        currently selected item in the tree. If the user cancels
+        selecting a new item from the tree, e.g. by hitting escape, the
+        previous value (the one that was selected before the PopupFrame
+        was popped up) is restored. """
+
+    def _createPopupFrame(self):
+        return MSWPopupFrame(self)
+
+    def _eventsToBind(self):
+        events = super(MSWComboTreeBox, self)._eventsToBind()
+        events.append((self._tree, wx.EVT_TREE_SEL_CHANGED,
+            self.OnSelectionChangedInTree))
+        return events
+
+    def OnSelectionChangedInTree(self, event):
+        item = event.GetItem()
+        if item:
+            selectedValue = self._tree.GetItemText(item)
+            if self.GetValue() != selectedValue:
+                self.SetValue(selectedValue)
+        event.Skip()
+
+    def _keyShouldPopUpTree(self, keyEvent):
+        return super(MSWComboTreeBox, self)._keyShouldPopUpTree(keyEvent) or \
+            (keyEvent.GetKeyCode() == wx.WXK_F4) or \
+            ((keyEvent.AltDown() or keyEvent.MetaDown()) and \
+              keyEvent.GetKeyCode() == wx.WXK_UP)
+
+    def SetValue(self, value):
+        """ Extend SetValue to also select the text in the
+            ComboTreeBox's text field. """
+        super(MSWComboTreeBox, self).SetValue(value)
+        # We select the text in the ComboTreeBox's text field.
+        # There is a slight complication, however. When the control is 
+        # deleted, SetValue is called. But if we call SetMark at that 
+        # time, wxPython will crash. We can prevent this by comparing the 
+        # result of GetLastPosition and the length of the value. If they
+        # match, all is fine. If they don't match, we don't call SetMark.
+        if self._text.GetLastPosition() == len(value):
+            self._text.SetMark(0, self._text.GetLastPosition())
+
+    def Popup(self, *args, **kwargs):
+        """ Extend Popup to store a copy of the current value, so we can
+            restore it later (in NotifyNoItemSelected). This is necessary
+            because MSWComboTreeBox will change the value as the user
+            browses through the items in the popped up tree. """
+        self._previousValue = self.GetValue()
+        super(MSWComboTreeBox, self).Popup(*args, **kwargs)
+
+    def NotifyNoItemSelected(self, *args, **kwargs):
+        """ Restore the value copied previously, because the user has
+            not selected a new value. """
+        self.SetValue(self._previousValue)
+        super(MSWComboTreeBox, self).NotifyNoItemSelected(*args, **kwargs)
+
+
+class MACComboTreeBox(NativeComboTreeBox):
+    def _createPopupFrame(self):
+        return MACPopupFrame(self)
+
+    def _createButton(self):
+        return self.GetChildren()[0] # The choice button
+
+    def _keyShouldNavigate(self, keyEvent):
+        return False # No navigation with up and down on wxMac
+
+    def _keyShouldPopUpTree(self, keyEvent):
+        return super(MACComboTreeBox, self)._keyShouldPopUpTree(keyEvent) or \
+            keyEvent.GetKeyCode() == wx.WXK_DOWN
+
+
+class GTKComboTreeBox(BaseComboTreeBox, wx.Panel):
+    """ The ComboTreeBox widget for wxGTK. This is actually a work
+        around because on wxGTK, there doesn't seem to be a way to intercept 
+        mouse events sent to the Combobox. Intercepting those events is 
+        necessary to prevent the Combobox from popping up the list and pop up
+        the tree instead. So, until wxPython makes intercepting those events
+        possible we build a poor man's Combobox ourselves using a TextCtrl and
+        a BitmapButton.  """
+
+    def _createPopupFrame(self):
+        return GTKPopupFrame(self)
+
+    def _createTextCtrl(self):
+        if self._readOnly:
+            style = wx.TE_READONLY
+        else:
+            style = 0
+        return wx.TextCtrl(self, style=style)
+
+    def _createButton(self):
+        bitmap = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, client=wx.ART_BUTTON)
+        return wx.BitmapButton(self, bitmap=bitmap)
+
+    def _layoutInterior(self):
+        panelSizer = wx.BoxSizer(wx.HORIZONTAL)
+        panelSizer.Add(self._text, flag=wx.EXPAND, proportion=1)
+        panelSizer.Add(self._button)
+        self.SetSizerAndFit(panelSizer)
+
+
+# ---------------------------------------------------------------------------
+
+
+def ComboTreeBox(*args, **kwargs):
+    """ Factory function to create the right ComboTreeBox depending on
+        platform. You may force a specific class, e.g. for testing
+        purposes, by setting the keyword argument 'platform', e.g. 
+        'platform=GTK' or 'platform=MSW' or platform='MAC'. """
+
+    platform = kwargs.pop('platform', None) or wx.PlatformInfo[0][4:7]
+    ComboTreeBoxClassName = '%sComboTreeBox' % platform
+    ComboTreeBoxClass = globals()[ComboTreeBoxClassName]
+    return ComboTreeBoxClass(*args, **kwargs)
+