diff --git a/wxPython/demo/Main.py b/wxPython/demo/Main.py index c5ec3fa129..45f8255b31 100644 --- a/wxPython/demo/Main.py +++ b/wxPython/demo/Main.py @@ -70,6 +70,7 @@ _treeList = [ 'SearchCtrl', 'SizedControls', 'AUI_MDI', + 'TreeMixin', ]), # managed windows == things with a (optional) caption you can close @@ -214,6 +215,7 @@ _treeList = [ 'Throbber', 'Ticker', 'TimeCtrl', + 'TreeMixin', 'VListBox', ]), diff --git a/wxPython/demo/TreeMixin.py b/wxPython/demo/TreeMixin.py new file mode 100644 index 0000000000..556cf2ad14 --- /dev/null +++ b/wxPython/demo/TreeMixin.py @@ -0,0 +1,230 @@ +import wx, wx.lib.customtreectrl, wx.gizmos +import wx.lib.mixins.treemixin as treemixin + +class TreeModel(object): + def __init__(self, *args, **kwargs): + self.items = [] + self.itemCounter = 0 + super(TreeModel, self).__init__(*args, **kwargs) + + def GetItem(self, indices): + text, children = 'Hidden root', self.items + for index in indices: + text, children = children[index] + return text, children + + def GetText(self, indices): + return self.GetItem(indices)[0] + + def GetChildren(self, indices): + return self.GetItem(indices)[1] + + def GetChildrenCount(self, indices): + return len(self.GetChildren(indices)) + + def SetChildrenCount(self, indices, count): + children = self.GetChildren(indices) + while len(children) > count: + children.pop() + while len(children) < count: + children.append(('item %d'%self.itemCounter, [])) + self.itemCounter += 1 + + def MoveItem(self, itemToMoveIndices, newParentIndices): + itemToMove = self.GetItem(itemToMoveIndices) + newParentChildren = self.GetChildren(newParentIndices) + newParentChildren.append(itemToMove) + oldParentChildren = self.GetChildren(itemToMoveIndices[:-1]) + oldParentChildren.remove(itemToMove) + + +class DemoVirtualTreeMixin(treemixin.VirtualTree, treemixin.DragAndDrop): + def __init__(self, *args, **kwargs): + self.model = kwargs.pop('treemodel') + self.log = kwargs.pop('log') + super(DemoVirtualTreeMixin, self).__init__(*args, **kwargs) + self.CreateImageList() + + def CreateImageList(self): + size = (16, 16) + self.imageList = wx.ImageList(*size) + for art in wx.ART_FOLDER, wx.ART_FILE_OPEN, wx.ART_NORMAL_FILE: + self.imageList.Add(wx.ArtProvider.GetBitmap(art, wx.ART_OTHER, + size)) + self.AssignImageList(self.imageList) + + def OnGetItemText(self, indices): + return self.model.GetText(indices) + + def OnGetChildrenCount(self, indices): + return self.model.GetChildrenCount(indices) + + def OnGetItemFont(self, indices): + if self.model.GetChildrenCount(indices) > 0: + return wx.SMALL_FONT + else: + return super(DemoVirtualTreeMixin, self).OnGetItemFont(indices) + + def OnGetItemTextColour(self, indices): + if len(indices) % 2 == 0: + return wx.RED + elif len(indices) % 3 == 0: + return wx.BLUE + else: + return super(DemoVirtualTreeMixin, + self).OnGetItemTextColour(indices) + + def OnGetItemBackgroundColour(self, indices): + if indices[-1] == len(indices): + return wx.GREEN + else: + return super(DemoVirtualTreeMixin, + self).OnGetItemBackgroundColour(indices) + + def OnGetItemImage(self, indices, which): + if which in [wx.TreeItemIcon_Normal, wx.TreeItemIcon_Selected]: + if self.model.GetChildrenCount(indices): + return 0 + else: + return 2 + else: + return 1 + + def OnDrop(self, dropTarget, dragItem): + dropIndices = self.ItemIndices(dropTarget) + dropText = self.model.GetText(dropIndices) + dragIndices = self.ItemIndices(dragItem) + dragText = self.model.GetText(dragIndices) + self.log.write('drop %s %s on %s %s'%(dragText, dragIndices, + dropText, dropIndices)) + self.model.MoveItem(dragIndices, dropIndices) + self.GetParent().RefreshItems() + + +class VirtualTreeCtrl(DemoVirtualTreeMixin, wx.TreeCtrl): + pass + + +class VirtualTreeListCtrl(DemoVirtualTreeMixin, wx.gizmos.TreeListCtrl): + def __init__(self, *args, **kwargs): + kwargs['style'] = wx.TR_DEFAULT_STYLE | wx.TR_FULL_ROW_HIGHLIGHT + super(VirtualTreeListCtrl, self).__init__(*args, **kwargs) + self.AddColumn('Column 0') + self.AddColumn('Column 1') + for art in wx.ART_TIP, wx.ART_WARNING: + self.imageList.Add(wx.ArtProvider.GetBitmap(art, wx.ART_OTHER, + (16, 16))) + + def OnGetItemText(self, indices, column=0): + return '%s, column %d'%\ + (super(VirtualTreeListCtrl, self).OnGetItemText(indices), column) + + def OnGetItemImage(self, indices, which, column=0): + if column == 0: + return super(VirtualTreeListCtrl, self).OnGetItemImage(indices, + which) + elif self.OnGetChildrenCount(indices): + return 4 + else: + return 3 + + +class VirtualCustomTreeCtrl(DemoVirtualTreeMixin, + wx.lib.customtreectrl.CustomTreeCtrl): + def __init__(self, *args, **kwargs): + self.checked = {} + kwargs['ctstyle'] = wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | \ + wx.TR_HAS_BUTTONS | wx.TR_FULL_ROW_HIGHLIGHT + super(VirtualCustomTreeCtrl, self).__init__(*args, **kwargs) + self.Bind(wx.lib.customtreectrl.EVT_TREE_ITEM_CHECKED, + self.OnItemChecked) + + def OnGetItemType(self, indices): + if len(indices) == 1: + return 1 + elif len(indices) == 2: + return 2 + else: + return 0 + + def OnGetItemChecked(self, indices): + return self.checked.get(tuple(indices), False) + + def OnItemChecked(self, event): + item = event.GetItem() + indices = tuple(self.ItemIndices(item)) + if self.GetItemType(item) == 2: + # It's a radio item; reset other items on the same level + for index in range(self.GetChildrenCount(self.GetItemParent(item))): + self.checked[indices[:-1]+(index,)] = False + self.checked[indices] = True + + + +class TreeNotebook(wx.Notebook): + def __init__(self, *args, **kwargs): + treemodel = kwargs.pop('treemodel') + log = kwargs.pop('log') + super(TreeNotebook, self).__init__(*args, **kwargs) + self.trees = [] + for class_, title in [(VirtualTreeCtrl, 'TreeCtrl'), + (VirtualTreeListCtrl, 'TreeListCtrl'), + (VirtualCustomTreeCtrl, 'CustomTreeCtrl')]: + tree = class_(self, treemodel=treemodel, log=log) + self.trees.append(tree) + self.AddPage(tree, title) + self.RefreshItems() + + def RefreshItems(self): + for tree in self.trees: + tree.RefreshItems() + tree.UnselectAll() + + def GetSelectedItemIndices(self): + tree = self.trees[self.GetSelection()] + if tree.GetSelection(): + return tree.ItemIndices(tree.GetSelection()) + else: + return [] + + +class TestPanel(wx.Panel): + def __init__(self, parent, log): + self.log = log + super(TestPanel, self).__init__(parent) + self.treemodel = TreeModel() + self.notebook = TreeNotebook(self, treemodel=self.treemodel, log=log) + label = wx.StaticText(self, label='Number of children: ') + self.childrenCountCtrl = wx.SpinCtrl(self, value='0', max=10000) + button = wx.Button(self, label='Update children') + button.Bind(wx.EVT_BUTTON, self.onEnter) + hSizer = wx.BoxSizer(wx.HORIZONTAL) + options = dict(flag=wx.ALIGN_CENTER_VERTICAL|wx.ALL, border=2) + hSizer.Add(label, **options) + hSizer.Add(self.childrenCountCtrl, 2, **options) + hSizer.Add(button, **options) + sizer = wx.BoxSizer(wx.VERTICAL) + sizer.Add(self.notebook, 1, wx.EXPAND) + sizer.Add(hSizer, 0, wx.EXPAND) + self.SetSizer(sizer) + + def onEnter(self, event): + indices = self.notebook.GetSelectedItemIndices() + text = self.treemodel.GetText(indices) + oldChildrenCount = self.treemodel.GetChildrenCount(indices) + newChildrenCount = self.childrenCountCtrl.GetValue() + self.log.write('%s %s now has %d children (was %d)'%(text, indices, + newChildrenCount, oldChildrenCount)) + self.treemodel.SetChildrenCount(indices, newChildrenCount) + self.notebook.RefreshItems() + + +def runTest(frame, nb, log): + win = TestPanel(nb, log) + return win + + +if __name__ == '__main__': + import sys, os, run + run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:]) + diff --git a/wxPython/docs/CHANGES.txt b/wxPython/docs/CHANGES.txt index 14671a6844..123405ab4d 100644 --- a/wxPython/docs/CHANGES.txt +++ b/wxPython/docs/CHANGES.txt @@ -14,6 +14,8 @@ Refactored the inspection tool such that it can be used as a wx.App mix-in class as it was used before (with the wx.lib.mixins.inspect module) and also as a non mix-in tool (using wx.lib.inspect.InspectionTool). +Add wx.lib.mixins.treemixin from Frank Niessink. + diff --git a/wxPython/tests/TreeMixinTest.py b/wxPython/tests/TreeMixinTest.py new file mode 100644 index 0000000000..992c032aed --- /dev/null +++ b/wxPython/tests/TreeMixinTest.py @@ -0,0 +1,237 @@ +import wx, wx.gizmos, wx.lib.customtreectrl, unittest, treemixin + + +class VirtualTreeCtrl(treemixin.VirtualTree, wx.TreeCtrl): + def __init__(self, *args, **kwargs): + self.children = {} + super(VirtualTreeCtrl, self).__init__(*args, **kwargs) + + def OnGetItemText(self, indices): + return 'item %s'%'.'.join([str(index) for index in indices]) + + def OnGetChildrenCount(self, indices=None): + indices = indices or [] + return self.children.get(tuple(indices), 0) + + def SetChildrenCount(self, indices, childrenCount): + self.children[tuple(indices)] = childrenCount + + +class VirtualTreeCtrlTest_NoRootItems(unittest.TestCase): + def setUp(self): + self.frame = wx.Frame(None) + self.tree = VirtualTreeCtrl(self.frame) + self.tree.RefreshItems() + + def testNoRootItems(self): + self.assertEqual(0, self.tree.GetCount()) + + def testAddTwoRootItems(self): + self.tree.SetChildrenCount([], 2) + self.tree.RefreshItems() + self.assertEqual(2, self.tree.GetCount()) + + def testAddOneRootItemAndOneChild(self): + self.tree.SetChildrenCount([], 1) + self.tree.SetChildrenCount([0], 1) + self.tree.RefreshItems() + self.tree.ExpandAll() + self.assertEqual(2, self.tree.GetCount()) + + def testAddOneRootItemAndTwoChildren(self): + self.tree.SetChildrenCount([], 1) + self.tree.SetChildrenCount([0], 2) + self.tree.RefreshItems() + self.tree.ExpandAll() + self.assertEqual(3, self.tree.GetCount()) + + +class VirtualTreeCtrlTest_OneRoot(unittest.TestCase): + def setUp(self): + self.frame = wx.Frame(None) + self.tree = VirtualTreeCtrl(self.frame) + self.tree.SetChildrenCount([], 1) + self.tree.RefreshItems() + + def testOneRoot(self): + self.assertEqual(1, self.tree.GetCount()) + + def testDeleteRootItem(self): + self.tree.SetChildrenCount([], 0) + self.tree.RefreshItems() + self.assertEqual(0, self.tree.GetCount()) + + def testAddOneChild(self): + self.tree.SetChildrenCount([0], 1) + self.tree.RefreshItems() + self.tree.ExpandAll() + self.assertEqual(2, self.tree.GetCount()) + + def testAddTwoChildren(self): + self.tree.SetChildrenCount([0], 2) + self.tree.RefreshItems() + self.tree.ExpandAll() + self.assertEqual(3, self.tree.GetCount()) + + + +class TreeAPIHarmonizerTestCase(unittest.TestCase): + style = wx.TR_DEFAULT_STYLE + + def setUp(self): + self.frame = wx.Frame(None) + class HarmonizedTreeCtrl(treemixin.TreeAPIHarmonizer, self.TreeClass): + pass + self.tree = HarmonizedTreeCtrl(self.frame, style=self.style) + self.populateTree() + + def populateTree(self): + self.root = self.tree.AddRoot('Root') + self.item = self.tree.AppendItem(self.root, 'item') + + +class TreeAPIHarmonizerCommonTests(object): + def testSetItemImage(self): + self.tree.SetItemImage(self.item, -1, wx.TreeItemIcon_Normal) + self.assertEqual(-1, + self.tree.GetItemImage(self.item, wx.TreeItemIcon_Normal)) + + def testGetColumnCount(self): + self.assertEqual(0, self.tree.GetColumnCount()) + + def testGetItemType(self): + self.assertEqual(0, self.tree.GetItemType(self.item)) + + def testGetMainWindow(self): + self.assertEqual(self.tree, self.tree.GetMainWindow()) + + +class TreeAPIHarmonizerSingleSelectionTests(object): + def testUnselectAll(self): + self.tree.SelectItem(self.item) + self.tree.UnselectAll() + self.assertEqual([], self.tree.GetSelections()) + + def testGetSelections_NoSelection(self): + self.tree.UnselectAll() + self.assertEqual([], self.tree.GetSelections()) + + def testGetSelections_OneSelectedItem(self): + self.tree.UnselectAll() + self.tree.SelectItem(self.item) + self.assertEqual([self.item], self.tree.GetSelections()) + + +class TreeAPIHarmonizerMultipleSelectionTests(object): + style = wx.TR_DEFAULT_STYLE | wx.TR_MULTIPLE + + def testUnselectAll(self): + self.tree.SelectItem(self.item) + self.tree.UnselectAll() + self.assertEqual([], self.tree.GetSelections()) + + def testGetSelections_NoSelection(self): + self.tree.UnselectAll() + self.assertEqual([], self.tree.GetSelections()) + + def testGetSelections_OneSelectedItem(self): + self.tree.UnselectAll() + self.tree.SelectItem(self.item) + self.assertEqual([self.item], self.tree.GetSelections()) + + def testGetSelections_TwoSelectedItems(self): + self.tree.UnselectAll() + item2 = self.tree.AppendItem(self.root, 'item 2') + self.tree.SelectItem(self.item) + self.tree.SelectItem(item2) + self.assertEqual([self.item, item2], self.tree.GetSelections()) + + +class TreeAPIHarmonizerWithTreeCtrlTestCase(TreeAPIHarmonizerTestCase): + TreeClass = wx.TreeCtrl + +class TreeAPIHarmonizerWithTreeCtrl_SingleSelection( \ + TreeAPIHarmonizerCommonTests, TreeAPIHarmonizerSingleSelectionTests, + TreeAPIHarmonizerWithTreeCtrlTestCase): + pass + +class TreeAPIHarmonizerWithTreeCtrl_MultipleSelection( \ + TreeAPIHarmonizerCommonTests, TreeAPIHarmonizerMultipleSelectionTests, + TreeAPIHarmonizerWithTreeCtrlTestCase): + pass + + +class TreeAPIHarmonizerWithTreeListCtrlTestCase(TreeAPIHarmonizerTestCase): + TreeClass = wx.gizmos.TreeListCtrl + + def populateTree(self): + self.tree.AddColumn('Column') + super(TreeAPIHarmonizerWithTreeListCtrlTestCase, self).populateTree() + + +class TreeAPIHarmonizerWithTreeListCtrl_SingleSelection( \ + TreeAPIHarmonizerCommonTests, TreeAPIHarmonizerSingleSelectionTests, + TreeAPIHarmonizerWithTreeListCtrlTestCase): + + def testGetColumnCount(self): + self.assertEqual(1, self.tree.GetColumnCount()) + + def testGetMainWindow(self): + self.assertNotEqual(self.tree, self.tree.GetMainWindow()) + + +class TreeAPIHarmonizerWithTreeListCtrl_MultipleSelection( \ + TreeAPIHarmonizerCommonTests, TreeAPIHarmonizerMultipleSelectionTests, + TreeAPIHarmonizerWithTreeListCtrlTestCase): + + def testGetColumnCount(self): + self.assertEqual(1, self.tree.GetColumnCount()) + + def testGetMainWindow(self): + self.assertNotEqual(self.tree, self.tree.GetMainWindow()) + + def testGetSelections_TwoSelectedItems(self): + ''' Override TreeAPIHarmonizerMultipleSelectionTests.- + testGetSelections_TwoSelectedItems, because + TreeListCtrl.SelectItem needs an extra parameter. ''' + self.tree.UnselectAll() + item2 = self.tree.AppendItem(self.root, 'item 2') + self.tree.SelectItem(self.item) + self.tree.SelectItem(item2, unselect_others=False) + self.assertEqual([self.item, item2], self.tree.GetSelections()) + + +class TreeAPIHarmonizerWithCustomTreeCtrlTestCase(TreeAPIHarmonizerTestCase): + TreeClass = wx.lib.customtreectrl.CustomTreeCtrl + + +class TreeAPIHarmonizerCustomTreeCtrlTests(object): + def testGetCheckItemType(self): + item = self.tree.AppendItem(self.root, 'item', ct_type=1) + self.assertEqual(1, self.tree.GetItemType(item)) + + def testGetRadioItemType(self): + item = self.tree.AppendItem(self.root, 'item', ct_type=2) + self.assertEqual(2, self.tree.GetItemType(item)) + + +class TreeAPIHarmonizerWithCustomTreeCtrl_SingleSelection( \ + TreeAPIHarmonizerCommonTests, TreeAPIHarmonizerSingleSelectionTests, + TreeAPIHarmonizerCustomTreeCtrlTests, + TreeAPIHarmonizerWithCustomTreeCtrlTestCase): + pass + + +class TreeAPIHarmonizerWithCustomTreeCtrl_MultipleSelection( \ + TreeAPIHarmonizerCommonTests, TreeAPIHarmonizerMultipleSelectionTests, + TreeAPIHarmonizerCustomTreeCtrlTests, + TreeAPIHarmonizerWithCustomTreeCtrlTestCase): + pass + + + +if __name__ == '__main__': + app = wx.App(False) + unittest.main() + + diff --git a/wxPython/wx/lib/mixins/treemixin.py b/wxPython/wx/lib/mixins/treemixin.py new file mode 100644 index 0000000000..79dc6be3fc --- /dev/null +++ b/wxPython/wx/lib/mixins/treemixin.py @@ -0,0 +1,443 @@ +''' +treemixin.py + +This module provides two mixin classes that can be used with tree +controls: +- VirtualTree is a class that, when mixed in with a tree control, +makes the tree control virtual, similar to a ListCtrl in virtual mode. +A virtual tree control builds the tree itself by means of callbacks, +so the programmer is freed from the burden of building the tree herself. +- DragAndDrop is a mixin class that helps with dragging and dropping of +items. The graphical part of dragging and dropping tree items is done by +this mixin class. You only need to implement the OnDrop method that is +called when the drop happens. + +Both mixin classes work with wx.TreeCtrl, wx.gizmos.TreeListCtrl, +and wx.lib.customtree.CustomTreeCtrl. They can be used together or +separately. + +Both mixins force the wx.TR_HIDE_ROOT style. + +Frank Niessink +License: wxWidgets license +Version: 0.5 +Date: 11 February 2007 +''' + +import wx + +class TreeAPIHarmonizer(object): + ''' This class attempts to hide the differences in API between the + different tree controls currently supported. ''' + + def GetColumnCount(self): + # Only TreeListCtrl has columns, return 0 if we are mixed in + # with another tree control. + try: + return super(TreeAPIHarmonizer, self).GetColumnCount() + except AttributeError: + return 0 + + def GetItemType(self, item): + # Only CustomTreeCtrl has different item types, return the + # default item type if we are mixed in with another tree control. + try: + return super(TreeAPIHarmonizer, self).GetItemType(item) + except AttributeError: + return 0 + + def SetItemImage(self, item, imageIndex, which, column=0): + # The SetItemImage signature is different for TreeListCtrl and + # other tree controls. This adapter method hides the differences. + if self.GetColumnCount(): + args = (item, imageIndex, column, which) + else: + args = (item, imageIndex, which) + super(TreeAPIHarmonizer, self).SetItemImage(*args) + + def GetMainWindow(self): + # Only TreeListCtrl has a separate main window, return self if we are + # mixed in with another tree control. + try: + return super(TreeAPIHarmonizer, self).GetMainWindow() + except AttributeError: + return self + + def UnselectAll(self): + if self.GetWindowStyle() & wx.TR_MULTIPLE: + super(TreeAPIHarmonizer, self).UnselectAll() + else: + self.Unselect() + + def GetSelections(self): + if self.GetWindowStyle() & wx.TR_MULTIPLE: + return super(TreeAPIHarmonizer, self).GetSelections() + else: + selection = self.GetSelection() + if selection: + return [selection] + else: + return [] + + def HitTest(self, *args, **kwargs): + # Only TreeListCtrl has columns, return 0 for the column if we are + # mixed in with another tree control. + hitTestResult = super(TreeAPIHarmonizer, self).HitTest(*args, **kwargs) + try: + item, flags, column = hitTestResult + except ValueError: + (item, flags), column = hitTestResult, 0 + return item, flags, column + + def GetItemChildren(self, item, recursively=False): + ''' Return the children of item as a list. This method is not + part of the API of any tree control, but merely convenient to + have available. ''' + children = [] + child, cookie = self.GetFirstChild(item) + while child: + children.append(child) + if recursively: + children.extend(self.GetItemChildren(child, True)) + child, cookie = self.GetNextChild(item, cookie) + return children + + +class VirtualTree(TreeAPIHarmonizer): + ''' This is a mixin class that can be used to allow for virtual tree + controls. It can be mixed in with wx.TreeCtrl, wx.gizmos.TreeListCtrl, + wx.lib.customtree.CustomTreeCtrl. + + To use it derive a new class from this class and one of the tree + controls, e.g.: + class MyTree(VirtualTree, wx.TreeCtrl): + ... + + You *must* implement OnGetChildrenCount and OnGetItemText so that + this class knows how many items to draw and what text to use. To + also draw images, change colours, etc., override one or more of the + other OnGetXXX methods. + + About specifying item indices: the VirtualTree uses various item + callbacks (such as OnGetItemText) to retrieve information needed + to display the items. To specify what item a callback needs + information about, the callback passes a list of indices. The first + index in the list is the index of the root item. If no more indices + are in the list (i.e. the length of the list of indices is exactly + 1), the callback should return the information about that root item. + If the list contains two indices, the first index is the index of + the root item, and the second index is the index of the child of + that root item. For example, if OnGetItemText is called with + indices=[0,2], the callback should return information about that the + third child of the first root item. OnGetChildrenCount may also be + called with indices == [] to get the number of root items in the + tree. + ''' + + def __init__(self, *args, **kwargs): + kwargs['style'] = kwargs.get('style', wx.TR_DEFAULT_STYLE) | \ + wx.TR_HIDE_ROOT + super(VirtualTree, self).__init__(*args, **kwargs) + self.Bind(wx.EVT_TREE_ITEM_EXPANDING, self.OnItemExpanding) + self.Bind(wx.EVT_TREE_ITEM_COLLAPSED, self.OnItemCollapsed) + + def OnGetChildrenCount(self, indices): + ''' This function must be overloaded in the derived class. It + should return the number of child items of the item with the + provided indices. If indices == [] it should return the number + of root items. ''' + raise NotImplementedError + + def OnGetItemText(self, indices, column=0): + ''' This function must be overloaded in the derived class. It + should return the string containing the text of the specified + item. ''' + raise NotImplementedError + + def OnGetItemFont(self, indices): + ''' This function may be overloaded in the derived class. It + should return the wx.Font to be used for the specified item. ''' + return wx.NullFont + + def OnGetItemTextColour(self, indices): + ''' This function may be overloaded in the derived class. It + should return the wx.Colour to be used as text colour for the + specified item. ''' + return wx.NullColour + + def OnGetItemBackgroundColour(self, indices): + ''' This function may be overloaded in the derived class. It + should return the wx.Colour to be used as background colour for + the specified item. ''' + return wx.NullColour + + def OnGetItemImage(self, indices, which=wx.TreeItemIcon_Normal, column=0): + ''' This function may be overloaded in the derived class. It + should return the index of the image to be used. ''' + return -1 + + def OnGetItemType(self, indices): + ''' This function may be overloaded in the derived class, but + that only makes sense when this class is mixed in with a tree + control that supports checkable items, e.g. CustomTreeCtrl. + This method should return whether the item is to be normal (0, + the default), a checkbox (1) or a radiobutton (2). + Note that OnGetItemChecked should return whether the item is + actually checked. ''' + return 0 + + def OnGetItemChecked(self, indices): + ''' This function may be overloaded in the derived class, but + that only makes sense when this class is mixed in with a tree + control that supports checkable items, e.g. CustomTreeCtrl. + This method should return whether the item is to be checked. + Note that OnGetItemType should return 1 (checkbox) or 2 + (radiobutton) for this item. ''' + return False + + def RefreshItems(self): + ''' Redraws all visible items. ''' + rootItem = self.GetRootItem() + if not rootItem: + rootItem = self.AddRoot('Hidden root') + self.RefreshChildrenRecursively(rootItem, [], + self.OnGetChildrenCount([])) + + def RefreshChildrenRecursively(self, item, indices, childrenCount): + ''' Refresh the children of item, reusing as much of the + existing items in the tree as possible. ''' + existingChildren = self.GetItemChildren(item) + for childIndex in range(childrenCount): + childIndices = indices + [childIndex] + if childIndex < len(existingChildren): + child = existingChildren[childIndex] + else: + child = self.AppendItem(item, '') + self.RefreshRecursively(child, childIndices) + # Delete left-over items: + for existingChild in existingChildren[childrenCount:]: + self.Delete(existingChild) + + def RefreshRecursively(self, item, indices): + childrenCount = self.OnGetChildrenCount(indices) + self.RefreshItem(item, indices, bool(childrenCount)) + # We need to refresh the children when the item is expanded and + # when the item has no children, because in the latter case we + # might have to delete old children from the tree: + if self.IsExpanded(item) or not childrenCount: + self.RefreshChildrenRecursively(item, indices, childrenCount) + self.SetItemHasChildren(item, bool(childrenCount)) + + def RefreshItem(self, item, indices, hasChildren): + ''' Refresh one item. ''' + item = self.RefreshItemType(item, indices) + self.RefreshItemText(item, indices) + self.RefreshColumns(item, indices) + self.RefreshFont(item, indices) + self.RefreshTextColour(item, indices) + self.RefreshBackgroundColour(item, indices) + self.RefreshItemImage(item, indices, hasChildren) + self.RefreshCheckedState(item, indices) + + def RefreshItemType(self, item, indices): + try: + currentType = self.GetItemType(item) + except AttributeError: + return item + type = self.OnGetItemType(indices) + if type != currentType: + # There's no way to change the type so we create a new item + # and delete the old one. + oldItem = item + item = self.InsertItem(self.GetItemParent(oldItem), item, '', + ct_type=type) + self.Delete(oldItem) + return item + + def RefreshItemText(self, item, indices): + itemText = self.OnGetItemText(indices) + if self.GetItemText(item) != itemText: + self.SetItemText(item, itemText) + + def RefreshColumns(self, item, indices): + for columnIndex in range(1, self.GetColumnCount()): + itemText = self.OnGetItemText(indices, columnIndex) + if self.GetItemText(item, columnIndex) != itemText: + self.SetItemText(item, itemText, columnIndex) + + def RefreshFont(self, item, indices): + font = self.OnGetItemFont(indices) + if self.GetItemFont(item) != font: + self.SetItemFont(item, font) + + def RefreshTextColour(self, item, indices): + colour = self.OnGetItemTextColour(indices) + if self.GetItemTextColour(item) != colour: + self.SetItemTextColour(item, colour) + + def RefreshBackgroundColour(self, item, indices): + # This one only seems to work for wx.TreeCtrl and not for + # wx.gizmos.TreeListCtrl and wx.lib.customtreectrl.CustomTreeCtrl... + colour = self.OnGetItemBackgroundColour(indices) + if self.GetItemBackgroundColour(item) != colour: + self.SetItemBackgroundColour(item, colour) + + def RefreshItemImage(self, item, indices, hasChildren): + regularIcons = [wx.TreeItemIcon_Normal, wx.TreeItemIcon_Selected] + expandedIcons = [wx.TreeItemIcon_Expanded, + wx.TreeItemIcon_SelectedExpanded] + # Refresh images in first column: + for icon in regularIcons: + imageIndex = self.OnGetItemImage(indices, icon) + if self.GetItemImage(item, icon) != imageIndex: + self.SetItemImage(item, imageIndex, icon) + for icon in expandedIcons: + if hasChildren: + imageIndex = self.OnGetItemImage(indices, icon) + else: + imageIndex = -1 + if self.GetItemImage(item, icon) != imageIndex or imageIndex == -1: + self.SetItemImage(item, imageIndex, icon) + # Refresh images in remaining columns, if any: + for columnIndex in range(1, self.GetColumnCount()): + for icon in regularIcons: + imageIndex = self.OnGetItemImage(indices, icon, columnIndex) + if self.GetItemImage(item, columnIndex, icon) != imageIndex: + self.SetItemImage(item, imageIndex, icon, columnIndex) + + def RefreshCheckedState(self, item, indices): + try: + isChecked = self.IsItemChecked(item) + except AttributeError: + return # Checking not supported + shouldBeChecked = self.OnGetItemChecked(indices) + if isChecked != shouldBeChecked: + self.CheckItem(item, shouldBeChecked) + + def OnItemExpanding(self, event): + indices = self.ItemIndices(event.GetItem()) + childrenCount = self.OnGetChildrenCount(indices) + self.RefreshChildrenRecursively(event.GetItem(), indices, childrenCount) + event.Skip() + + def OnItemCollapsed(self, event): + parent = self.GetItemParent(event.GetItem()) + if not parent: + parent = self.GetRootItem() + indices = self.ItemIndices(parent) + childrenCount = self.OnGetChildrenCount(indices) + self.RefreshChildrenRecursively(parent, indices, childrenCount) + event.Skip() + + def ItemIndices(self, item): + ''' Construct index list for item. ''' + parent = self.GetItemParent(item) + if parent: + parentIndices = self.ItemIndices(parent) + ownIndex = self.GetItemChildren(parent).index(item) + return parentIndices + [ownIndex] + else: + return [] + + +class DragAndDrop(TreeAPIHarmonizer): + ''' This is a mixin class that can be used to easily implement + dragging and dropping of tree items. It can be mixed in with + wx.TreeCtrl, wx.gizmos.TreeListCtrl, or wx.lib.customtree.CustomTreeCtrl. + + To use it derive a new class from this class and one of the tree + controls, e.g.: + class MyTree(DragAndDrop, wx.TreeCtrl): + ... + + You *must* implement OnDrop. OnDrop is called when the user has + dropped an item on top of another item. It's up to you to decide how + to handle the drop. If you are using this mixin together with the + VirtualTree mixin, it makes sense to rearrange your underlying data + and then call RefreshItems to let de virtual tree refresh itself. + ''' + + def __init__(self, *args, **kwargs): + kwargs['style'] = kwargs.get('style', wx.TR_DEFAULT_STYLE) | \ + wx.TR_HIDE_ROOT + super(DragAndDrop, self).__init__(*args, **kwargs) + self.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnBeginDrag) + + def OnDrop(self, dropItem, dragItem): + ''' This function must be overloaded in the derived class. + dragItem is the item being dragged by the user. dropItem is the + item dragItem is dropped upon. If the user doesn't drop dragItem + on another item, dropItem equals the (hidden) root item of the + tree control. ''' + raise NotImplementedError + + def OnBeginDrag(self, event): + # We allow only one item to be dragged at a time, to keep it simple + self._dragItem = self.GetSelections()[0] + if self._dragItem: + self.StartDragging() + event.Allow() + else: + event.Veto() + + def OnEndDrag(self, event): + self.StopDragging() + dropTarget = event.GetItem() + if not dropTarget: + dropTarget = self.GetRootItem() + if self.IsValidDropTarget(dropTarget): + self.UnselectAll() + if dropTarget != self.GetRootItem(): + self.SelectItem(dropTarget) + self.OnDrop(dropTarget, self._dragItem) + + def OnDragging(self, event): + if not event.Dragging(): + self.StopDragging() + return + item, flags, column = self.HitTest(wx.Point(event.GetX(), event.GetY())) + if not item: + item = self.GetRootItem() + if self.IsValidDropTarget(item): + self.SetCursorToDragging() + else: + self.SetCursorToDroppingImpossible() + if flags & wx.TREE_HITTEST_ONITEMBUTTON: + self.Expand(item) + selections = self.GetSelections() + if selections != [item]: + self.UnselectAll() + if item != self.GetRootItem(): + self.SelectItem(item) + event.Skip() + + def StartDragging(self): + self.GetMainWindow().Bind(wx.EVT_MOTION, self.OnDragging) + self.Bind(wx.EVT_TREE_END_DRAG, self.OnEndDrag) + self.SetCursorToDragging() + + def StopDragging(self): + self.GetMainWindow().Unbind(wx.EVT_MOTION) + self.Unbind(wx.EVT_TREE_END_DRAG) + self.ResetCursor() + self.UnselectAll() + self.SelectItem(self._dragItem) + + def SetCursorToDragging(self): + self.GetMainWindow().SetCursor(wx.StockCursor(wx.CURSOR_HAND)) + + def SetCursorToDroppingImpossible(self): + self.GetMainWindow().SetCursor(wx.StockCursor(wx.CURSOR_NO_ENTRY)) + + def ResetCursor(self): + self.GetMainWindow().SetCursor(wx.NullCursor) + + def IsValidDropTarget(self, dropTarget): + if dropTarget: + allChildren = self.GetItemChildren(self._dragItem, recursively=True) + parent = self.GetItemParent(self._dragItem) + return dropTarget not in [self._dragItem, parent] + allChildren + else: + return True + +