From 9b90e0a53d4b8f2d002f2dd75928f058fcf42d43 Mon Sep 17 00:00:00 2001 From: cbeytas Date: Sun, 23 Jan 2022 20:18:20 +0000 Subject: [PATCH 1/4] Major speedups and bug fixes to CustomTreeCtrl Fixes bugs #2006, #1938, #1395 --- wx/lib/agw/customtreectrl.py | 2318 ++++++++++++++++++++++++---------- 1 file changed, 1644 insertions(+), 674 deletions(-) diff --git a/wx/lib/agw/customtreectrl.py b/wx/lib/agw/customtreectrl.py index 0fbb15c46..9fb4daf54 100644 --- a/wx/lib/agw/customtreectrl.py +++ b/wx/lib/agw/customtreectrl.py @@ -219,6 +219,21 @@ def __init__(self, parent): after the ``EVT_TREE_SEL_CHANGED`` event). +Single and Multiple Selection +============= + +If the ``TR_MULTIPLE`` style is not set (default) the tree will be in single +selection mode where there is always one item selected. When an item is +selected a ``EVT_TREE_SEL_CHANGING`` event is sent which can be vetoed to +prevent the selection from occuring. A ``EVT_TREE_SEL_CHANGED`` event is sent +out when the selection has successfully changed. If the selection gets cleared +by the user or programatically, the next Idle handler will select the root item +ensuring there is a selected item at all times. + +If the ``TR_MULTIPLE`` style is set the tree can have any number of selected +items and can also have no selection. + + Drag and Drop ============= @@ -243,6 +258,36 @@ def __init__(self, parent): a more responsive drag. +Item Windows +============= +Windows and controls can be added to any tree item. This is done with the +:meth:`~CustomTreeCtrl.SetItemWindow`. Note that parent of the window/control +must be set to the CustomTreeCtrl. The tree then owns the window and will show +or hide it with the item it belongs to. The window can be removed with +:meth:`~CustomTreeCtrl.DeleteItemWindow` upon which the window is destroyed. + +Enabling or disabling the item with :meth:`~CustomTreeCtrl.EnableItem` will +also enable or disable its associated window. Alternatively the window itself +can be enabled or disabled with :meth:`~CustomTreeCtrl.SetItemWindowEnabled`. + +By default item windows are shown immediately after the item text. If the +``TR_ALIGN_WINDOWS`` style is set, all windows for a level of the tree will +be aligned to the longest text in that level. The ``TR_ALIGN_WINDOWS_RIGHT`` +style will instead right-alight all windows added to the tree. + +Keyboard focus in the tree can be difficult to manage once windows are added. +Most platforms will shift focus to an item window any time the tree gets focus. +This can prevent keyboard control of the tree, or cause the tree to jump +uncontrollably to an item with a window when it regains focus. This is due to +the tree being based off `wx.ScrolledWindow`. In the future this may be changed +to `wx.ScrolledCanvas` instead. + +:note: On Windows platforms (Windows 7 or newer using Aero theme) it is not + recommended to add more than about 1000 windows to the tree. This can cause + dramatic slowdowns with the Desktop Window Manager process (dwm.exe) even if + the windows are hidden. + + Supported Platforms =================== @@ -349,24 +394,24 @@ def __init__(self, parent): Latest Revision: Helio Guilherme @ 09 Aug 2018, 21.35 GMT -Version 2.7 +Version 2.8 """ # Version Info -__version__ = "2.7" +__version__ = "2.8" import wx from wx.lib.expando import ExpandoTextCtrl # Python 2/3 compatibility helper import six - +import time # ---------------------------------------------------------------------------- # Constants # ---------------------------------------------------------------------------- -_NO_IMAGE = -1 +_NO_IMAGE = -1 # This should be wx.NO_IMAGE ? _PIXELS_PER_UNIT = 10 # Start editing the current item after half a second (if the mouse hasn't @@ -381,6 +426,7 @@ def __init__(self, parent): # ---------------------------------------------------------------------------- # Enum for different images associated with a treectrl item +# NOTE: We should use wx.TreeItemIcon_XXX values here instead? TreeItemIcon_Normal = 0 # not selected, not expanded """ The tree item is not selected and not expanded. """ TreeItemIcon_Selected = 1 # selected, not expanded @@ -625,11 +671,11 @@ def DrawTreeItemButton(win, dc, rect, flags): dc.DrawRectangle(rect) # black lines - xMiddle = rect.x + rect.width//2 - yMiddle = rect.y + rect.height//2 + xMiddle = rect.x + rect.width // 2 + yMiddle = rect.y + rect.height // 2 # half of the length of the horz lines in "-" and "+" - halfWidth = rect.width//2 - 2 + halfWidth = rect.width // 2 - 2 dc.SetPen(wx.BLACK_PEN) dc.DrawLine(xMiddle - halfWidth, yMiddle, xMiddle + halfWidth + 1, yMiddle) @@ -637,7 +683,7 @@ def DrawTreeItemButton(win, dc, rect, flags): if not flags & _CONTROL_EXPANDED: # turn "-" into "+" - halfHeight = rect.height//2 - 2 + halfHeight = rect.height // 2 - 2 dc.DrawLine(xMiddle, yMiddle - halfHeight, xMiddle, yMiddle + halfHeight + 1) @@ -710,6 +756,71 @@ def ChopText(dc, text, max_size): ret = text[0:last_good_length] + "..." return ret +# ---------------------------------------------------------------------------- + +def EnsureText(text): + """ + Make sure the given text is valid, converting if necessary. + + This conversion is done as a courtesy for the user, otherwise bad + text in the tree will throw a UnicodeDecodeError in calls to + dc.GetMultiLineTextExtent() and the tree will not draw correctly + and behave bizarrely. This is a frustrating behavior and not + clear what causes the issue when it happens. + + :param `text`: The object to turn into valid text. + + :return: The text, unmodified if it was valid, converted otherwise. + + .. versionadded:: 2.8 + """ + if not isinstance(text, six.text_type): + if isinstance(text, six.binary_type): + # Make sure bytes are valid utf-8 (wxPython de-facto encoding). + try: + text.decode("utf-8", errors="strict") + except ValueError: + # Bad utf8 string. Fix it so it is compatible with the tree. + text = text.decode("utf-8", errors="replace") + else: + # Convert `None` and other types to text. + text = six.text_type(text) + return text + +# ---------------------------------------------------------------------------- + +def BisectChildren(children, y): + """ + Find index of last child whose Y position is before the given ``y``. + + Performs a binary search yielding quick results even for very large + lists. The implementation is derived from the `bisect` module. It is + used to speed up Paint, HitTest, and Refresh operations by reducing + their work to ``children[n:len(children)]`` items, where ``n`` is + the returned index of this search. + + :param list `children`: a Python list containing :class:`GenericTreeItem` + objects to search. + :param integer `y`: the logical Y coordinate to look for. This is + usually the start of the visible client area + (e.g. self.CalcUnscrolledPosition(0, 0)[1]). + + :return: The integer index before the first item in ``children`` + whose Y position is on or after ``y``. This will be 0 if children + is empty. + + .. versionadded:: 2.8 + """ + # Binary search for first child that is on or after the given `y`. + lo, hi, = 0, len(children) + while lo < hi: + mid = (lo + hi) // 2 + if children[mid].GetY() < y: + lo = mid + 1 + else: + hi = mid + # Always return the previous index (it may be partially visible). + return lo - 1 if lo > 0 else 0 #--------------------------------------------------------------------------- # DragImage Implementation @@ -780,13 +891,13 @@ def __init__(self, treeCtrl, item): if image_w: ximagepos = wcheck - yimagepos = ((total_h > image_h) and [(total_h-image_h)//2] or [0])[0] + yimagepos = ((total_h > image_h) and [(total_h - image_h) // 2] or [0])[0] if checkimage is not None: xcheckpos = 2 - ycheckpos = ((total_h > image_h) and [(total_h-image_h)//2] or [0])[0] + 2 + ycheckpos = ((total_h > image_h) and [(total_h - image_h) // 2] or [0])[0] + 2 - extraH = ((total_h > height) and [(total_h - height)//2] or [0])[0] + extraH = ((total_h > height) and [(total_h - height) // 2] or [0])[0] xtextpos = wcheck + image_w ytextpos = extraH @@ -795,9 +906,9 @@ def __init__(self, treeCtrl, item): total_h = max(total_h, height) if total_h < 30: - total_h += 2 # at least 2 pixels + total_h += 2 # at least 2 pixels else: - total_h += total_h//10 # otherwise 10% extra spacing + total_h += total_h // 10 # otherwise 10% extra spacing total_w = image_w + wcheck + width @@ -851,12 +962,12 @@ def CreateBitmap(self): if self._itemcheck: memory.DrawBitmap(self._itemcheck, self._xcheckpos, self._ycheckpos, True) - textrect = wx.Rect(self._xtextpos, self._ytextpos+self._extraH, self._textwidth, self._textheight) + textrect = wx.Rect(self._xtextpos, self._ytextpos + self._extraH, self._textwidth, self._textheight) memory.DrawLabel(self._text, textrect) memory.SelectObject(wx.NullBitmap) - # Gtk and Windows unfortunately don't do so well with transparent + # Gtk and Windows unfortunatly don't do so well with transparent # drawing so this hack corrects the image to have a transparent # background. if wx.Platform != '__WXMAC__': @@ -885,7 +996,7 @@ class TreeItemAttr(object): :note: This class is inspired by the wxWidgets generic implementation of :class:`TreeItemAttr`. """ - def __init__(self, colText=wx.NullColour, colBack=wx.NullColour, colBorder=wx.NullColour,font=wx.NullFont): + def __init__(self, colText=wx.NullColour, colBack=wx.NullColour, colBorder=wx.NullColour, font=wx.NullFont): """ Default class constructor. For internal use: do not call it in your code! @@ -1417,7 +1528,7 @@ def __init__(self, owner, item=None): xSize, ySize = w + 25, h else: expandoStyle |= wx.SUNKEN_BORDER - xSize, ySize = w + 25, h+2 + xSize, ySize = w + 25, h + 2 ExpandoTextCtrl.__init__(self, self._owner, wx.ID_ANY, self._startValue, wx.Point(x - 4, y), wx.Size(xSize, ySize), @@ -1493,7 +1604,11 @@ def OnChar(self, event): wx.CallAfter(self.Finish) elif keycode == wx.WXK_ESCAPE: - self.StopEditing() + # This calls self.Finish() which calls _owner.ResetEditControl() + # which calls Destroy() on this EditCtrl. We cannot destroy this + # edit control while handing an event for it otherwise wxWidgets + # v3.1+ on GTK2/3 will seg fault. So we must use wx.CallAfter. + wx.CallAfter(self.StopEditing) else: event.Skip() @@ -1589,9 +1704,11 @@ def __init__(self, owner): def Notify(self): - """ The timer has expired, clear the `_findPrefix` attribute in :class:`CustomTreeCtrl`. """ + """ The timer has expired, clear the `_findPrefix` attribute + and the `_findFail` flag in :class:`CustomTreeCtrl`. """ self._owner._findPrefix = "" + self._owner._findFail = False # ----------------------------------------------------------------------------- @@ -1660,13 +1777,15 @@ def __init__(self, parent, text="", ct_type=0, wnd=None, image=-1, selImage=-1, """ - # since there can be very many of these, we save size by choosing + # since there can be very many of these, we save size by chosing # the smallest representation for the elements and by ordering # the members to avoid padding. self._text = text # label to be rendered for item + self._extents = None # The (width, height) of text in current font. self._data = data # user-provided data + self._dirty = True # Flag indicates if CalculateSize required. - self._children = [] # list of children + self._children = [] # list of children self._parent = parent # parent of this item self._attr = None # attributes??? @@ -1681,22 +1800,21 @@ def __init__(self, parent, text="", ct_type=0, wnd=None, image=-1, selImage=-1, self._images[TreeItemIcon_Expanded] = _NO_IMAGE self._images[TreeItemIcon_SelectedExpanded] = _NO_IMAGE - self._checkedimages = [None, None, None, None, None] self._leftimage = _NO_IMAGE - self._x = 0 # (virtual) offset from top - self._y = 0 # (virtual) offset from left + self._x = 0 # (virtual) offset from left + self._y = 0 # (virtual) offset from top self._width = 0 # width of this item self._height = 0 # height of this item self._isCollapsed = True - self._hasHilight = False # same as focused + self._hasHilight = False # True when item is selected. self._hasPlus = False # used for item which doesn't have # children but has a [+] button self._isBold = False # render the label in bold font self._isItalic = False # render the label in italic font self._ownsAttr = False # delete attribute when done - self._type = ct_type # item type: 0=normal, 1=check, 2=radio + self._type = None # item type. Set by SetType call below. self._is3State = False # true for 3-state checkbox items self._checked = False # only meaningful for check and radio items self._enabled = True # flag to enable/disable an item @@ -1704,22 +1822,20 @@ def __init__(self, parent, text="", ct_type=0, wnd=None, image=-1, selImage=-1, self._visited = False # visited state for an hypertext item self._hidden = False # hidden items are not painted - if self._type > 0: - # do not construct the array for normal items - self._checkedimages[TreeItemIcon_Checked] = 0 - self._checkedimages[TreeItemIcon_NotChecked] = 1 - self._checkedimages[TreeItemIcon_Undetermined] = 2 - self._checkedimages[TreeItemIcon_Flagged] = 3 - self._checkedimages[TreeItemIcon_NotFlagged] = 4 + # Build checked images array + self._checkedimages = [None, None, None, None, None] + self.SetType(ct_type) if parent: if parent.GetType() == 2 and not parent.IsChecked(): # if the node parent is a radio not enabled, we are disabled self._enabled = False - self._wnd = wnd # are we holding a window? - self._windowontheright = on_the_right # on the right, or left? - + self._wnd = wnd # are we holding a window? + self._windowontheright = on_the_right # on the right, or left? + self._windowsize = None + self._windowenabled = False + if wnd: self.SetWindow(wnd, on_the_right) @@ -1853,7 +1969,12 @@ def SetImage(self, image, which): :note: Call :meth:`CustomTreeCtrl.SetItemImage` instead to refresh the tree properly. """ - self._images[which] = image + if self._images[which] != image: + # Set image. + self._images[which] = image + # Only go dirty if the current image state was modified. + if which == self.GetCurrentImageState(): + self._dirty = True def SetLeftImage(self, image): @@ -1866,7 +1987,7 @@ def SetLeftImage(self, image): :note: Call :meth:`CustomTreeCtrl.SetItemLeftImage` instead to refresh the tree properly. """ - + # Left image is part of the tree so doesn't affect our item's size. self._leftimage = image @@ -1888,8 +2009,8 @@ def SetHasPlus(self, has=True): :note: Call :meth:`CustomTreeCtrl.SetItemHasChildren` instead to refresh the tree properly. """ - - self._hasPlus = has + # The tree icon is part of the tree so doesn't affect our item's size. + self._hasPlus = bool(has) def SetBold(self, bold): @@ -1901,7 +2022,10 @@ def SetBold(self, bold): :note: Call :meth:`CustomTreeCtrl.SetItemBold` instead to refresh the tree properly. """ - self._isBold = bold + if self._isBold != bold: + self._isBold = bold + self._dirty = True + self._extents = None def SetItalic(self, italic): @@ -1913,7 +2037,10 @@ def SetItalic(self, italic): :note: Call :meth:`CustomTreeCtrl.SetItemItalic` instead to refresh the tree properly. """ - self._isItalic = italic + if self._isItalic != italic: + self._isItalic = italic + self._dirty = True + self._extents = None def GetX(self): @@ -1932,6 +2059,8 @@ def SetX(self, x): """ Sets the `x` position on an item, in logical coordinates. + Used Internally. Set during CalculatePositions(). + :param integer `x`: an integer specifying the x position of the item. """ @@ -1942,6 +2071,8 @@ def SetY(self, y): """ Sets the `y` position on an item, in logical coordinates. + Used Internally. Set during CalculatePositions(). + :param integer `y`: an integer specifying the y position of the item. """ @@ -1971,17 +2102,17 @@ def GetWidth(self): def SetHeight(self, h): """ - Sets the item's height. Used internally. + Sets the item's height. Used internally. Set in CalculateSize(). :param integer `h`: an integer specifying the item's height, in pixels. """ - self._height = h + self._height = int(h) def SetWidth(self, w): """ - Sets the item's width. Used internally. + Sets the item's width. Used internally. Set in CalculateSize(). :param integer `w`: an integer specifying the item's width, in pixels. """ @@ -1989,6 +2120,33 @@ def SetWidth(self, w): self._width = w + def IsDirty(self): + """Returns True if item requires recalculation. Used internally. + + .. versionadded:: 2.8 + """ + + return self._dirty + + + def SetDirty(self, dirty, clear_extents=False): + """ + Marks that the item's size needs to be recalculated. + + Used internally for speed so that CalculateSize() is only called + when required (ie. when font, text, image, window or hide changes). + + :param bool `clear_extents`: Set to True to clear any cached text + extents. This can be done if the text/font changes. + + .. versionadded:: 2.8 + """ + + self._dirty = dirty + if clear_extents is True: + self._extents = None + + def SetWindow(self, wnd, on_the_right=True): """ Sets the window associated to the item. Internal use only. @@ -2008,7 +2166,7 @@ def SetWindow(self, wnd, on_the_right=True): raise Exception("Separator items can not have an associated window") self._wnd = wnd - self._windowontheright = on_the_right + self._windowontheright = bool(on_the_right) if wnd.GetSizer(): # the window is a complex one hold by a sizer size = wnd.GetBestSize() @@ -2026,9 +2184,12 @@ def SetWindow(self, wnd, on_the_right=True): # otherwise it would stay visible. See the demo for an example. self._wnd.Bind(wx.EVT_TREE_ITEM_COLLAPSING, self.OnTreeItemCollapsing) - self._height = size.GetHeight() + 2 - self._width = size.GetWidth() - self._windowsize = size + # We should not set these here? Set during CalculateSize(). + # self._height = size.GetHeight() + 2 + # self._width = size.GetWidth() + if self._windowsize != size: + self._dirty = True + self._windowsize = size ## Hide the window since the position isn't correct yet. It will ## be shown and positioned when the item is painted. @@ -2059,6 +2220,7 @@ def DeleteWindow(self): if self._wnd: self._wnd.Destroy() self._wnd = None + self._dirty = True def GetWindowEnabled(self): @@ -2088,8 +2250,8 @@ def SetWindowEnabled(self, enable=True): if not self._wnd: raise Exception("\nERROR: This Item Has No Window Associated") - self._windowenabled = enable - self._wnd.Enable(enable) + self._windowenabled = bool(enable) + self._wnd.Enable(self._windowenabled) def GetWindowSize(self): @@ -2176,7 +2338,23 @@ def SetType(self, ct_type): :note: Call :meth:`CustomTreeCtrl.SetItemType` instead to refresh the tree properly. """ - self._type = ct_type + if self._type != ct_type: + self._type = ct_type + self._dirty = True + if self._type: + # Assign image indexes into tree's _imageListCheck. + self._checkedimages[TreeItemIcon_Checked] = 0 + self._checkedimages[TreeItemIcon_NotChecked] = 1 + self._checkedimages[TreeItemIcon_Undetermined] = 2 + self._checkedimages[TreeItemIcon_Flagged] = 3 + self._checkedimages[TreeItemIcon_NotFlagged] = 4 + else: + # Set entire list to `None` to disable control images. + self._checkedimages[TreeItemIcon_Checked] = None + self._checkedimages[TreeItemIcon_NotChecked] = None + self._checkedimages[TreeItemIcon_Undetermined] = None + self._checkedimages[TreeItemIcon_Flagged] = None + self._checkedimages[TreeItemIcon_NotFlagged] = None def SetHyperText(self, hyper=True): @@ -2188,7 +2366,11 @@ def SetHyperText(self, hyper=True): :note: Call :meth:`CustomTreeCtrl.SetItemHyperText` instead to refresh the tree properly. """ - self._hypertext = hyper + hyper = bool(hyper) + if self._hypertext != hyper: + self._hypertext = hyper + self._dirty = True + self._extents = None def SetVisited(self, visited=True): @@ -2200,7 +2382,7 @@ def SetVisited(self, visited=True): :note: Call :meth:`CustomTreeCtrl.SetItemVisited` instead to refresh the tree properly. """ - self._visited = visited + self._visited = bool(visited) def GetVisited(self): @@ -2230,7 +2412,9 @@ def Hide(self, hide): :note: Always use :meth:`CustomTreeCtrl.HideItem` instead to update the tree properly. """ - self._hidden = hide + if self._hidden != hide: + self._hidden = hide + self._dirty = True def GetParent(self): @@ -2275,14 +2459,16 @@ def Collapse(self): def SetHilight(self, set=True): """ - Sets the item focus/unfocus. + Sets the item focus/unfocus. Internal use only. - :param bool `set`: ``True`` to set the focus to the item, ``False`` otherwise. + :param bool `set`: ``True`` to set the focus to the item, + ``False`` otherwise. - :note: Call :meth:`CustomTreeCtrl.SelectItem` instead to update the tree properly and send events. + :note: Call :meth:`CustomTreeCtrl.SelectItem` instead to update + the tree properly and send events. """ - self._hasHilight = set + self._hasHilight = bool(set) def HasChildren(self): @@ -2400,7 +2586,7 @@ def Set3State(self, allow): if self._type != 1: return False - self._is3State = allow + self._is3State = bool(allow) return True @@ -2468,7 +2654,7 @@ def Enable(self, enable=True): :note: Call :meth:`CustomTreeCtrl.EnableItem` instead to update the tree properly. """ - self._enabled = enable + self._enabled = bool(enable) def IsEnabled(self): @@ -2515,10 +2701,11 @@ def SetAttributes(self, attr): """ if self._ownsAttr: - del self._attr + del self._attr self._attr = attr self._ownsAttr = False + self._dirty = True def AssignAttributes(self, attr): @@ -2559,6 +2746,9 @@ def DeleteChildren(self, tree): if child in tree._itemWithWindow: tree._itemWithWindow.remove(child) + if child in tree._selectedItems: + tree._selectedItems.remove(child) + del child self._children = [] @@ -2577,8 +2767,42 @@ def SetText(self, text): if self.IsSeparator(): raise Exception("Separator items can not have text") - + # Set text and clear extents so it gets recalculated. self._text = text + self._dirty = True + self._extents = None + + + def HasExtents(self): + """ + Returns whether the text extents are calculated for this item. + + :return: ``True`` if extents are calculated, ``False`` otherwise. + + .. versionadded:: 2.8 + """ + + return self._extents is not None + + + def GetExtents(self, dc=None): + """ + Calculate text extents of this item using the given ClientDc. + + :param `dc`: an instance of :class:`wx.DC` to use to calculate + text extent if it has not been cached yet. The proper font + should have been already set in the device context. + + :return: A 2-tuple of (width, height). If `dc` is not provided + could return `None`. + + .. versionadded:: 2.8 + """ + if self._extents is None and dc is not None: + # Calculate and cache text extents for this item. + width, height, dummy = dc.GetFullMultiLineTextExtent(self._text) + self._extents = (width, height) + return self._extents def GetChildrenCount(self, recursively=True): @@ -2604,14 +2828,15 @@ def GetChildrenCount(self, recursively=True): def GetSize(self, x, y, theButton): """ - Returns the item size. + Returns the item size (including all children, if expanded). :param integer `x`: the current item's x position; :param integer `y`: the current item's y position; :param `theButton`: an instance of the main :class:`CustomTreeCtrl`. :return: A tuple of (`x`, `y`) dimensions, in pixels, representing the - item's width and height. + item's width and height. If the item is expanded, this includes all + visible children. """ bottomY = self._y + theButton.GetLineHeight(self) @@ -2642,7 +2867,7 @@ def HitTest(self, point, theCtrl, flags=0, level=0): :see: :meth:`CustomTreeCtrl.HitTest() ` method for the flags explanation. - :return: A 2-tuple of (item, flags). The item may be ``None``. + :return: A 2-tuple of (item, flags). The item may be ``None``. """ # Hidden items are never evaluated. if self.IsHidden(): @@ -2657,7 +2882,7 @@ def HitTest(self, point, theCtrl, flags=0, level=0): pointX, pointY = point[0], point[1] if pointY > self._y and pointY < self._y + h: - y_mid = self._y + h//2 + y_mid = self._y + h // 2 if pointY < y_mid: flags |= TREE_HITTEST_ONITEMUPPERPART @@ -2669,15 +2894,15 @@ def HitTest(self, point, theCtrl, flags=0, level=0): if wx.Platform == "__WXMAC__": # according to the drawing code the triangels are drawn # at -4 , -4 from the position up to +10/+10 max - if pointX > xCross-4 and pointX < xCross+10 and pointY > y_mid-4 and \ - pointY < y_mid+10 and self.HasPlus() and theCtrl.HasButtons(): + if (pointX > xCross - 4 and pointX < xCross + 10 and pointY > y_mid - 4 and + pointY < y_mid + 10 and self.HasPlus() and theCtrl.HasButtons()): flags |= TREE_HITTEST_ONITEMBUTTON return self, flags else: # 5 is the size of the plus sign - if pointX > xCross-6 and pointX < xCross+6 and pointY > y_mid-6 and \ - pointY < y_mid+6 and self.HasPlus() and theCtrl.HasButtons(): + if (pointX > xCross - 6 and pointX < xCross + 6 and pointY > y_mid - 6 and + pointY < y_mid + 6 and self.HasPlus() and theCtrl.HasButtons()): flags |= TREE_HITTEST_ONITEMBUTTON return self, flags @@ -2720,50 +2945,73 @@ def HitTest(self, point, theCtrl, flags=0, level=0): # if children are expanded, fall through to evaluate them if not self.IsExpanded(): + # Item is not expanded (or hidden). Return no item found. return None, 0 - # evaluate children - for child in self._children: + # Binary search for last child that is before or equal to point's Y. + lo = BisectChildren(self._children, point[1]) + hi = len(self._children) + + # Now hit test only against prospective children. + for index in range(lo, hi): + child = self._children[index] res, flags = child.HitTest(point, theCtrl, flags, level + 1) if res is not None: return res, flags + if child.GetY() > point[1]: + break # Early exit (we're past the point) + # not found return None, 0 - def GetCurrentImage(self): + def GetCurrentImageState(self): """ - Returns the current item image. + Returns the current item image state. - :return: An integer index that can be used to retrieve the item image inside - a :class:`wx.ImageList`. - """ + :return: An integer enum value can be any of the following values: - image = _NO_IMAGE + ================================= ======================== + Item State Description + ================================= ======================== + ``TreeItemIcon_Normal`` To get the normal item image + ``TreeItemIcon_Selected`` To get the selected item image (i.e. the image which is shown when the item is currently selected) + ``TreeItemIcon_Expanded`` To get the expanded image (this only makes sense for items which have children - then this image is shown when the item is expanded and the normal image is shown when it is collapsed) + ``TreeItemIcon_SelectedExpanded`` To get the selected expanded image (which is shown when an expanded item is currently selected) + ================================= ======================== + .. versionadded:: 2.8 + """ if self.IsExpanded(): - if self.IsSelected(): - - image = self._images[TreeItemIcon_SelectedExpanded] - - if image == _NO_IMAGE: - - # we usually fall back to the normal item, but try just the - # expanded one (and not selected) first in this case - image = self._images[TreeItemIcon_Expanded] - - else: # not expanded - + kind = TreeItemIcon_SelectedExpanded + if self._images[kind] == _NO_IMAGE: + # we usually fall back to the normal item, but try just + # the expanded one (and not selected) first in this case + kind = TreeItemIcon_Expanded + else: + kind = TreeItemIcon_Expanded + else: # not expanded if self.IsSelected(): - image = self._images[TreeItemIcon_Selected] - + kind = TreeItemIcon_Selected + else: + kind = TreeItemIcon_Normal # maybe it doesn't have the specific image we want, # try the default one instead - if image == _NO_IMAGE: - image = self._images[TreeItemIcon_Normal] + if self._images[kind] == _NO_IMAGE: + kind = TreeItemIcon_Normal + return kind + + + def GetCurrentImage(self): + """ + Returns the current item image. + + :return: An integer index that can be used to retrieve the item image inside + a :class:`wx.ImageList`. + """ - return image + return self._images[self.GetCurrentImageState()] def GetCurrentCheckedImage(self): @@ -2774,7 +3022,7 @@ def GetCurrentCheckedImage(self): a :class:`wx.ImageList`. """ - if self._type == 0: + if not self._type: return None checked = self.IsChecked() @@ -2793,10 +3041,19 @@ def GetCurrentCheckedImage(self): return self._checkedimages[TreeItemIcon_NotFlagged] -# ----------------------------------------------------------------------------- -# CustomTreeCtrl Main Implementation. -# This Is The Main Class. -# ----------------------------------------------------------------------------- +## --------------------------------------------------------------------- +## CustomTreeCtrl Main Implementation. This Is The Main Class. +## Note: It currently is based off wx.ScrolledWindow which manages focus +## of all child windows. As soon as a window is added to an item +## this breaks keyboard control of the tree on Unix based platforms. +## It is also challenging to work with under Microsoft Windows +## because it attempts to force focus to child windows instead of +## to the tree itself. +## CustomTreeCtrl should be based on the wx.ScrolledCanvas class +## which works much better with the only lost feature being tabbing +## between child windows. To maintain backwards compatibility a +## dummy SetFocusIgnoringChildren() method must be implemented. +## --------------------------------------------------------------------- class CustomTreeCtrl(wx.ScrolledWindow): """ @@ -2828,7 +3085,8 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default self._current = self._key_current = self._anchor = self._select_me = None self._hasFocus = False - self._dirty = False + self._dirty = False # Indicates if tree recalculation required. + self._selectedItems = set() # Default line height: it will soon be changed self._lineHeight = 10 @@ -2851,6 +3109,8 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default # image list for icons self._imageListNormal = self._imageListButtons = self._imageListState = self._imageListCheck = self._imageListLeft = None self._ownsImageListNormal = self._ownsImageListButtons = self._ownsImageListState = self._ownsImageListLeft = False + self._imgWidth = self._imgHeight = 0 + self._leftWidth = self._leftHeight = 0 # Drag and drop initial settings self._dragCount = 0 @@ -2867,9 +3127,13 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default # This one allows us to handle Freeze() and Thaw() calls self._freezeCount = 0 + self._freezeDC = None # Used to speed up text metrics - self._findPrefix = "" - self._findTimer = None + self._findPrefix = "" # Keyboard typed text search. + self._findTimer = None # Search expiry timer. + self._findBellOnNoMatch = False # wx.Bell() on no search match. + self._findFail = False # Flags self._findPrefix not found. + self._findAll = True # Search all or only visible items. self._dropEffectAboveItem = False self._lastOnSame = False @@ -2918,10 +3182,10 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default self._sendEvent = True # Connection lines style - grey = (160,160,160) + grey = (160, 160, 160) if wx.Platform != "__WXMAC__": self._dottedPen = wx.Pen(grey, 1, wx.USER_DASH) - self._dottedPen.SetDashes([1,1]) + self._dottedPen.SetDashes([1, 1]) self._dottedPen.SetCap(wx.CAP_BUTT) else: self._dottedPen = wx.Pen(grey, 1) @@ -2932,13 +3196,13 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default # For Appended Windows self._hasWindows = False - self._itemWithWindow = [] + self._itemWithWindow = set() if wx.Platform == "__WXMAC__": agwStyle &= ~TR_LINES_AT_ROOT agwStyle |= TR_NO_LINES - platform, major, minor, micro = wx.GetOsVersion() + platform, major, minor = wx.GetOsVersion()[:3] if major < 10: agwStyle |= TR_ROW_LINES @@ -2953,7 +3217,7 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default self._separatorPen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)) # Create our container... at last! - wx.ScrolledWindow.__init__(self, parent, id, pos, size, style|wx.HSCROLL|wx.VSCROLL, name) + wx.ScrolledWindow.__init__(self, parent, id, pos, size, style | wx.HSCROLL | wx.VSCROLL, name) self._agwStyle = agwStyle @@ -2964,7 +3228,7 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default # connecting lines, we can use a narrower layout. # It may not be a good idea to force this... if not self.HasButtons() and not self.HasAGWFlag(TR_NO_LINES): - self._indent= 10 + self._indent = 10 self._spacing = 10 self.SetValidator(validator) @@ -2977,6 +3241,9 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default self.SetOwnFont(attr.font) self.SetSize(size) + # Width and height of the tree. Set during CalculatePositions(). + self._width = 0 + self._height = 0 # Bind the events self.Bind(wx.EVT_PAINT, self.OnPaint) @@ -2994,6 +3261,11 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default self.SetFocus() +## def SetFocusIgnoringChildren(self): +## """Emulate wx.ScrolledWindow method for backwards compatibility.""" +## self.SetFocus() + + def AcceptsFocus(self): """ Can this window be given focus by mouse click? @@ -3122,7 +3394,7 @@ def GetSelection(self): """ return self._current - + # Provide method for compatibility with wx.TreeCtrl GetFocusedItem = GetSelection @@ -3189,11 +3461,21 @@ def EnableItem(self, item, enable=True, torefresh=True): if torefresh: # We have to refresh the item line - dc = wx.ClientDC(self) - self.CalculateSize(item, dc) self.RefreshLine(item) + def IsEmpty(self): + """ + Returns True if the control is empty (i.e. has no items, even no root one). + + :return: ``True`` if the tree is empty, ``False`` otherwise. + + .. versionadded:: 2.8 + """ + + return True if not self._anchor else False + + def IsItemEnabled(self, item): """ Returns whether an item is enabled or disabled. @@ -3212,7 +3494,8 @@ def SetDisabledColour(self, colour): """ self._disabledColour = colour - self._dirty = True + # Repaint to show changes. + self.Refresh() def GetDisabledColour(self): @@ -3288,6 +3571,7 @@ def SetItem3StateValue(self, item, state): """ item.Set3StateValue(state) + self.RefreshLine(item) def SetItem3State(self, item, allow): @@ -3303,6 +3587,7 @@ def SetItem3State(self, item, allow): :note: This method is meaningful only for checkbox-like items. """ + self.RefreshLine(item) return item.Set3State(allow) @@ -3321,8 +3606,6 @@ def CheckItem2(self, item, checked=True, torefresh=False): item.Check(checked) if torefresh: - dc = wx.ClientDC(self) - self.CalculateSize(item, dc) self.RefreshLine(item) @@ -3392,7 +3675,6 @@ def CheckItem(self, item, checked=True): else: item.Check(checked) - dc = wx.ClientDC(self) self.RefreshLine(item) if self.HasAGWFlag(TR_AUTO_CHECK_CHILD): @@ -3483,7 +3765,7 @@ def AutoCheckParent(self, item, checked): def CheckChilds(self, item, checked=True): """ - Programmatically check/uncheck item children. + Programatically check/uncheck item children. :param `item`: an instance of :class:`GenericTreeItem`; :param bool `checked`: ``True`` to check an item, ``False`` to uncheck it. @@ -3539,6 +3821,24 @@ def EditLabel(self, item): self.Edit(item) + def EndEditLabel(self, item, discardChanges=False): + """Ends label editing. Mimicks wx.TreeCtrl's method. + + :param `item`: an instance of :class:`GenericTreeItem`. If an + edit is not ongoing for this item, then nothing is done. + + .. versionadded:: 2.8 + """ + if self._editCtrl is not None and item == self._editCtrl.item(): + if discardChanges is True: + # Send cancel event and finish (calls ResetEditControl). + self._editCtrl.StopEditing() + else: + # Send events then finish (calls ResetEditControl). + self._editCtrl.AcceptChanges() + self._editCtrl.Finish() + + def ShouldInheritColours(self): """ Return ``True`` from here to allow the colours of this window to be @@ -3562,6 +3862,9 @@ def SetIndent(self, indent): """ self._indent = indent + # Mark all items for recalculation. + if self._anchor: + self.SetItemsDirty(self._anchor, clear_extents=True) self._dirty = True @@ -3573,6 +3876,9 @@ def SetSpacing(self, spacing): """ self._spacing = spacing + # Mark all items for recalculation. + if self._anchor: + self.SetItemsDirty(self._anchor, clear_extents=True) self._dirty = True @@ -3637,7 +3943,11 @@ def SetAGWWindowStyleFlag(self, agwStyle): self.SelectItem(select, False) self._agwStyle = agwStyle - self._dirty = True + # Mark all items for recalculation. + if self._anchor: + self.SetItemsDirty(self._anchor, clear_extents=True) + # Force re-calculation of tree. Required if item X positions change. + self.CalculatePositions() def GetAGWWindowStyleFlag(self): @@ -3821,17 +4131,24 @@ def SetItemText(self, item, text): if item.IsSeparator(): raise Exception("Separator items can not have text") + # Tree strings must be valid utf8 or we will crash on GetExtent calls. + text = EnsureText(text) - dc = wx.ClientDC(self) - oldtext = item.GetText() + # Save old size and set item's text. + old_size = item.GetExtents() item.SetText(text) - # Avoid Calculating tree unless number of lines changed (slow). - if oldtext.count('\n') != text.count('\n'): - self.CalculatePositions() - self.Refresh() - self.AdjustMyScrollbars() + + # If tree already dirty don't attempt to refresh line. + if self._dirty: + return + + # Calculate new size of item's text. + dc = self._freezeDC if self._freezeDC else wx.ClientDC(self) + new_size = item.GetExtents(dc) + # If text height changed (number of lines) we need to recalculate tree. + if old_size is None or new_size[1] != old_size[1]: + self._dirty = True else: - self.CalculateSize(item, dc) self.RefreshLine(item) @@ -3847,11 +4164,20 @@ def SetItemImage(self, item, image, which=TreeItemIcon_Normal): :see: :meth:`~CustomTreeCtrl.GetItemImage` for an explanation of the `which` parameter. """ + # Force 'image' to integer index and set (flag errors now). + image = _NO_IMAGE if image is None else int(image) item.SetImage(image, which) - - dc = wx.ClientDC(self) - self.CalculateSize(item, dc) - self.RefreshLine(item) + # Calculate new size of item, if dirty. + if not self._dirty and item.IsDirty() and self.IsItemShown(item): + # Calculate item size to see if it changed height. + old_height = self.GetLineHeight(item) + dc = self._freezeDC if self._freezeDC else wx.ClientDC(self) + self.CalculateSize(item, dc) + # If the height changes, we need to recalculate the tree. + if self.GetLineHeight(item) != old_height: + self._dirty = True + else: + self.RefreshLine(item) def SetItemLeftImage(self, item, image): @@ -3865,9 +4191,7 @@ def SetItemLeftImage(self, item, image): """ item.SetLeftImage(image) - - dc = wx.ClientDC(self) - self.CalculateSize(item, dc) + # Left image doesn't affect item size so no need to CalculateSize(). self.RefreshLine(item) @@ -3897,6 +4221,24 @@ def SetItemHasChildren(self, item, has=True): self.RefreshLine(item) + def SetItemHilight(self, item, selected=True): + """ + Sets whether an item is selected or not (hilighted). + + :param `item`: an instance of :class:`GenericTreeItem`; + :param bool `selected`: ``True`` to hilight the item, ``False`` otherwise. + + .. versionadded:: 2.8 + """ + + item.SetHilight(selected) + # Maintain internal list of selected items. + if selected: + self._selectedItems.add(item) + elif item in self._selectedItems: + self._selectedItems.remove(item) + + def SetItemBold(self, item, bold=True): """ Sets the item font as bold/unbold. @@ -3985,8 +4327,9 @@ def SetItemFont(self, item, font): :param `item`: an instance of :class:`GenericTreeItem`; :param `font`: a valid :class:`wx.Font` instance. """ - + # Set item font and clear text extents so it gets recalculated. item.Attr().SetFont(font) + item.SetDirty(True, clear_extents=True) self._dirty = True @@ -4014,14 +4357,25 @@ def SetFont(self, font): self._italicFont = wx.Font(self._normalFont.GetPointSize(), family, wx.FONTSTYLE_ITALIC, wx.FONTWEIGHT_NORMAL, self._normalFont.GetUnderlined(), self._normalFont.GetFaceName(), self._normalFont.GetEncoding()) - - self.CalculatePositions() - self.Refresh() - self.AdjustMyScrollbars() + # Mark all items for recalculation. + if self._anchor: + self.SetItemsDirty(self._anchor, clear_extents=True) + self._dirty = True return True + def SetItemsDirty(self, item, clear_extents=False): + """ + Mark the item and all of its children to require size recalculation. + + .. versionadded:: 2.8 + """ + item.SetDirty(True, clear_extents) + for child in item.GetChildren(): + self.SetItemsDirty(child, clear_extents) + + def GetHyperTextFont(self): """ Returns the font used to render hypertext items. @@ -4044,6 +4398,8 @@ def SetHyperTextFont(self, font): """ self._hypertextfont = font + if self._anchor: + self.SetItemsDirty(self._anchor, clear_extents=True) self._dirty = True @@ -4057,7 +4413,8 @@ def SetHyperTextNewColour(self, colour): """ self._hypertextnewcolour = colour - self._dirty = True + # Repaint to show changes. + self.Refresh() def GetHyperTextNewColour(self): @@ -4082,7 +4439,8 @@ def SetHyperTextVisitedColour(self, colour): """ self._hypertextvisitedcolour = colour - self._dirty = True + # Repaint to show changes. + self.Refresh() def GetHyperTextVisitedColour(self): @@ -4329,7 +4687,8 @@ def SetConnectionPen(self, pen): """ self._dottedPen = pen - self._dirty = True + # Repaint to show changes. + self.Refresh() def GetConnectionPen(self): @@ -4433,16 +4792,15 @@ def SetItemWindow(self, item, wnd, on_the_right=True): if wnd is not None: self._hasWindows = True if item not in self._itemWithWindow: - self._itemWithWindow.append(item) + self._itemWithWindow.add(item) else: self.DeleteItemWindow(item) else: self.DeleteItemWindow(item) item.SetWindow(wnd, on_the_right) - self.CalculatePositions() - self.Refresh() - self.AdjustMyScrollbars() + if item.IsDirty(): + self._dirty = True def DeleteItemWindow(self, item): @@ -4458,7 +4816,8 @@ def DeleteItemWindow(self, item): item.DeleteWindow() if item in self._itemWithWindow: self._itemWithWindow.remove(item) - self._dirty = True + if item.IsDirty(): + self._dirty = True def GetItemWindowEnabled(self, item): @@ -4528,8 +4887,8 @@ def SetItemType(self, item, ct_type): """ item.SetType(ct_type) - self.CalculatePositions() - self.Refresh() + if item.IsDirty(): + self._dirty = True def GetDragFullScreen(self): @@ -4566,14 +4925,16 @@ def SetDragFullScreen(self, fullScreen=False): # item status inquiries # ----------------------------------------------------------------------------- - def IsVisible(self, item): + def IsItemShown(self, item): """ - Returns whether the item is visible or not (i.e., its hierarchy is expanded - enough to show the item, and it has not been hidden). + Returns whether the item is visible or not (i.e., its hierarchy + is expanded enough to show the item and it has not been hidden). :param `item`: an instance of :class:`GenericTreeItem`. - :return: ``True`` if the item is visible, ``False`` if it is hidden. + :return: ``True`` if the item is shown, ``False`` if it is hidden. + + .. versionadded:: 2.8 """ # Hidden items are never visible. if item.IsHidden(): @@ -4588,6 +4949,24 @@ def IsVisible(self, item): return False parent = parent.GetParent() + return True + + + def IsVisible(self, item): + """ + Returns whether the item is visible or not to the user. + + It must be shown (i.e., its hierarchy is expanded enough to show + the item, and it has not been hidden) and present in the current + client area view. + + :param `item`: an instance of :class:`GenericTreeItem`. + + :return: ``True`` if the item is visible, ``False`` if it is hidden. + """ + # If the item is not shown, it can't be visible. + if self.IsItemShown(item) is False: + return False startX, startY = self.GetViewStart() clientSize = self.GetClientSize() @@ -4727,7 +5106,7 @@ def GetNextChild(self, item, cookie): if cookie < len(children): - return children[cookie], cookie+1 + return children[cookie], cookie + 1 else: @@ -4768,15 +5147,56 @@ def GetNextSibling(self, item): return None siblings = parent.GetChildren() - if item not in siblings: + try: + index = siblings.index(item) + except ValueError: # Item is unlinked from tree return None - - index = siblings.index(item) + n = index + 1 return (n == len(siblings) and [None] or [siblings[n]])[0] + def GetNextSiblingFast(self, item, cookie=None): + """ + Returns the next sibling of an item with speed enhancement. + + :param `item`: an instance of :class:`GenericTreeItem`. + :param `cookie`: a parameter which is opaque for the application + but is necessary for the library to make this function fast. If + cookie is `None` this function will still work but much slower. + + :return: A tuple with the first value being an instance of + :class:`GenericTreeItem` or ``None`` if there are no further + siblings, and as second value an integer parameter 'cookie'. + + .. versionadded:: 2.8 + """ + # Find the parent of this item. + parent = item.GetParent() + if parent is None: + # root item doesn't have any siblings + return None, cookie + # Get list of this item's siblings (cookie is item index in this list). + siblings = parent.GetChildren() + index = cookie + # Is our the index (cookie) valid? + if (index is None or index < 0 or index >= len(siblings) or + siblings[index] != item): + # Find the index of item in siblings the slow way. + try: + index = siblings.index(item) + except ValueError: + # Item is unlinked from the tree. + return None, cookie + # Return our next sibling. + index += 1 + if index < len(siblings): + return (siblings[index], index) + # No more siblings, return None. + return (None, index) + + def GetPrevSibling(self, item): """ Returns the previous sibling of an item. @@ -4786,7 +5206,7 @@ def GetPrevSibling(self, item): :return: An instance of :class:`GenericTreeItem` or ``None`` if there are no further siblings. - :note: This method returns ``None`` if there are no further siblings. + :note: This method returns ``None`` if there are no previous siblings. """ parent = item.GetParent() @@ -4796,17 +5216,62 @@ def GetPrevSibling(self, item): return None siblings = parent.GetChildren() - if item not in siblings: + try: + index = siblings.index(item) + except ValueError: # Item is unlinked from tree return None - index = siblings.index(item) - return (index == 0 and [None] or [siblings[index-1]])[0] + return (index == 0 and [None] or [siblings[index - 1]])[0] + + + def GetPrevSiblingFast(self, item, cookie=None): + """ + Returns the previous sibling of an item. + + :param `item`: an instance of :class:`GenericTreeItem`. + + :return: An instance of :class:`GenericTreeItem` or ``None`` if there are no + further siblings. + :param `cookie`: a parameter which is opaque for the application + but is necessary for the library to make this function fast. If + cookie is `None` this function will still work but much slower. + + :return: A tuple with the first value being an instance of + :class:`GenericTreeItem` or ``None`` if there are no previous + siblings, and as second value an integer parameter 'cookie'. + + .. versionadded:: 2.8 + """ + + parent = item.GetParent() + + if parent is None: + # root item doesn't have any siblings + return None, cookie + + siblings = parent.GetChildren() + index = cookie + # Is our the index (cookie) valid? + if (index is None or index < 0 or index >= len(siblings) or + siblings[index] != item): + # Find the index of item in siblings the slow way. + try: + index = siblings.index(item) + except ValueError: + # Item is unlinked from the tree. + return None, cookie + # Return our previous sibling. + index -= 1 + if index >= 0: + return (siblings[index], index) + # No more siblings, return None. + return (None, index) def GetNext(self, item): """ - Returns the next item. Only for internal use right now. + Returns the next item. :return: An instance of :class:`GenericTreeItem` or ``None`` if there are no further items. @@ -4819,30 +5284,83 @@ def GetNext(self, item): # First see if there are any children. children = i.GetChildren() if len(children) > 0: - return children[0] + return children[0] else: - # Try a sibling of this or ancestor instead - p = item - toFind = None - while p and not toFind: - toFind = self.GetNextSibling(p) - p = self.GetItemParent(p) + # Try a sibling of this or ancestor instead + p = item + toFind = None + while p and not toFind: + toFind = self.GetNextSibling(p) + p = self.GetItemParent(p) - return toFind + return toFind - def GetPrev(self, item): + def GetNextFast(self, item, cookie=None): """ - Returns the previous item. Only for internal use right now. + Returns the next item in the tree with speed enhancement. + + This is the fastest way to search the tree when you need to + start searching at a particular item. :param `item`: an instance of :class:`GenericTreeItem`. + :param `cookie`: a parameter which is opaque for the application + but is necessary for the library to make this function fast. If + cookie is `None` this function will still work but much slower. - :return: An instance of :class:`GenericTreeItem` - """ + :return: A tuple with the first value being an instance of + :class:`GenericTreeItem` or ``None`` if there are no further + items, and as second value 'cookie' as a list. - # Look for a previous sibling of this item - prevSibling = self.GetPrevSibling(item) - if prevSibling: + .. versionadded:: 2.8 + """ + cookie = [] if cookie is None else cookie + # First see if there are any children. + children = item.GetChildren() + if len(children) > 0: + cookie.append(0) + return children[0], cookie + else: + # Try a sibling of this or ancestor instead + sibling = None + parent = self.GetItemParent(item) + while parent and not sibling: + # Move up in hierarchy to parent. + siblings = parent.GetChildren() + # Find item's position amongst siblings. + index = 0 if not cookie else cookie.pop(-1) + try: + index = siblings.index(item, index) + except ValueError: + # Missed cookie index. Search entire siblings list. + try: + index = siblings.index(item) + except ValueError: + # Item is unlinked from parent. + index = None + if index is not None and index + 1 < len(siblings): + # Sibling exists! + sibling = siblings[index + 1] + cookie.append(index + 1) + else: + sibling = None + item = parent + parent = self.GetItemParent(item) + return sibling, cookie + + + def GetPrev(self, item): + """ + Returns the previous item. Only for internal use right now. + + :param `item`: an instance of :class:`GenericTreeItem`. + + :return: An instance of :class:`GenericTreeItem` + """ + + # Look for a previous sibling of this item + prevSibling = self.GetPrevSibling(item) + if prevSibling: # return it's last child or itself if has not got any children if len(prevSibling.GetChildren()) > 0: @@ -4866,7 +5384,7 @@ def GetNextExpanded(self, item): if nextSibling.IsExpanded(): return nextSibling - return self.GetNextExpanded(prevSibling) + return self.GetNextExpanded(nextSibling) return None @@ -4977,17 +5495,20 @@ def ResetEditControl(self): self.CalculatePositions() self.Refresh() - self.AdjustMyScrollbars() - def FindItem(self, idParent, prefixOrig): + def FindItem(self, idParent, prefixOrig, all=True): """ Finds the first item starting with the given prefix after the given parent. :param integer `idParent`: an instance of :class:`GenericTreeItem`; :param string `prefixOrig`: a string containing the item text prefix. + :param bool `all`: defaults to ``True`` to search all items in + the tree. Set to ``False`` to search only the visible + (expanded) tree items. - :return: An instance of :class:`GenericTreeItem` or ``None`` if no item has been found. + :return: An instance of :class:`GenericTreeItem` or ``None`` if + no item has been found. """ # match is case insensitive as this is more convenient to the user: having @@ -5000,34 +5521,62 @@ def FindItem(self, idParent, prefixOrig): # pressing it) but we shouldn't jump to the next one if the user is # continuing to type as otherwise he might easily skip the item he wanted id = idParent + cookie = None + get_next = self.GetNextFast if all is True else self.GetNextShownFast if len(prefix) == 1: - id = self.GetNext(id) + id, cookie = get_next(id, cookie) # look for the item starting with the given prefix after it while id and not self.GetItemText(id).lower().startswith(prefix): - - id = self.GetNext(id) + id, cookie = get_next(id, cookie) # if we haven't found anything... if not id: - # ... wrap to the beginning id = self.GetRootItem() if self.HasAGWFlag(TR_HIDE_ROOT): # can't select virtual root - id = self.GetNext(id) + id, cookie = get_next(id, cookie) if idParent == self.GetRootItem(): # no tree item selected and idParent is not reachable return id # and try all the items (stop when we get to the one we started from) - while id != idParent and not self.GetItemText(id).lower().startswith(prefix): - id = self.GetNext(id) + while id and id != idParent and not self.GetItemText(id).lower().startswith(prefix): + id, cookie = get_next(id, cookie) + + # Return item id, if it matches, otherwise return None. + if self.GetItemText(id).lower().startswith(prefix): + return id + return None + + + def EnableBellOnNoMatch(self, on=True): + """Enable or disable a beep if no item matches the search. + + The default behavior of the tree is for no bell. - return id + :param bool `on`: ``True`` to call wx.Bell() if no match is + found to text entered via keyboard. ``False`` for no bell. + .. versionadded:: 2.8 + """ + self._findBellOnNoMatch = bool(on) + + + def EnableSearchAll(self, all=True): + """Enable or disable keyboard searching the entire tree. + :param bool `all`: ``True`` by default so that text typed into + the tree with the keyboard will search all items in the tree, + even items under unexpanded branches. Set to ``False`` to + limit searching to only those items that are visible. + + .. versionadded:: 2.8 + """ + self._findAll = bool(all) + # ----------------------------------------------------------------------------- # operations # ----------------------------------------------------------------------------- @@ -5073,6 +5622,9 @@ def DoInsertItem(self, parentId, previous, text, ct_type=0, wnd=None, image=-1, if wnd is not None and not self.HasAGWFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert Controls You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") + # Tree strings must be valid utf8 or we will crash on GetExtent calls. + text = EnsureText(text) + if text.find("\n") >= 0 and not self.HasAGWFlag(TR_HAS_VARIABLE_ROW_HEIGHT): raise Exception("\nERROR: In Order To Append/Insert A MultiLine Text You Have To Use The Style TR_HAS_VARIABLE_ROW_HEIGHT") @@ -5093,11 +5645,12 @@ def DoInsertItem(self, parentId, previous, text, ct_type=0, wnd=None, image=-1, self._dirty = True # do this first so stuff below doesn't cause flicker - item = GenericTreeItem(parent, text, ct_type, wnd, image, selImage, data, separator, on_the_right) + item = GenericTreeItem(parent, text, ct_type, wnd, image, selImage, + data, separator, on_the_right) if wnd is not None: self._hasWindows = True - self._itemWithWindow.append(item) + self._itemWithWindow.add(item) parent.Insert(item, previous) @@ -5159,7 +5712,7 @@ def AddRoot(self, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None, o if wnd is not None: self._hasWindows = True - self._itemWithWindow.append(self._anchor) + self._itemWithWindow.add(self._anchor) if self.HasAGWFlag(TR_HIDE_ROOT): @@ -5172,7 +5725,7 @@ def AddRoot(self, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None, o if not self.HasAGWFlag(TR_MULTIPLE): self._current = self._key_current = self._anchor - self._current.SetHilight(True) + self.SetItemHilight(self._anchor, True) return self._anchor @@ -5250,7 +5803,7 @@ def InsertItemByItem(self, parentId, idPrevious, text, ct_type=0, wnd=None, imag except: raise Exception("ERROR: Previous Item In CustomTreeCtrl.InsertItem() Is Not A Sibling") - return self.DoInsertItem(parentId, index+1, text, ct_type, wnd, image, selImage, data, separator, on_the_right) + return self.DoInsertItem(parentId, index + 1, text, ct_type, wnd, image, selImage, data, separator, on_the_right) def InsertItemByIndex(self, parentId, idPrevious, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None, separator=False, on_the_right=True): @@ -5301,7 +5854,7 @@ def InsertItem(self, parentId, input, text, ct_type=0, wnd=None, image=-1, selIm :see: :meth:`~CustomTreeCtrl.DoInsertItem` for possible exceptions generated by this method. """ - if type(input) == type(1): + if type(input) in six.integer_types: return self.InsertItemByIndex(parentId, input, text, ct_type, wnd, image, selImage, data, separator, on_the_right) else: return self.InsertItemByItem(parentId, input, text, ct_type, wnd, image, selImage, data, separator, on_the_right) @@ -5423,26 +5976,33 @@ def IsDescendantOf(self, parent, item): return False - # Don't leave edit or selection on a child which is about to disappear def ChildrenClosing(self, item): """ - We are about to destroy the item children. + We are about to collapse or destroy the item's children. Don't + leave edit or selection on any descendants of this item as they + are about to disappear. Shift selection to the item instead. :param `item`: an instance of :class:`GenericTreeItem`. """ - - if self._editCtrl is not None and item != self._editCtrl.item() and self.IsDescendantOf(item, self._editCtrl.item()): + # If editing a descendant of this item, stop editing before collapsing. + if (self._editCtrl is not None and item != self._editCtrl.item() and + self.IsDescendantOf(item, self._editCtrl.item())): self._editCtrl.StopEditing() + # If keyboard current is a descendant of the collapsing item clear it. if item != self._key_current and self.IsDescendantOf(item, self._key_current): self._key_current = None + # If selection is set to change to a descendant, shift it to the item. if self.IsDescendantOf(item, self._select_me): self._select_me = item + # If a descendant is currently selected, select the item instead. if item != self._current and self.IsDescendantOf(item, self._current): - self._current.SetHilight(False) + # Deselect the currently selected descendant. + self.SetItemHilight(self._current, False) self._current = None + # Flag item to be selected in the next idle handler. self._select_me = item @@ -5452,12 +6012,11 @@ def DeleteChildren(self, item): :param `item`: an instance of :class:`GenericTreeItem`. """ + if item.HasChildren(): + self._dirty = True # do this first so stuff below doesn't cause flicker - self._dirty = True # do this first so stuff below doesn't cause flicker - - self.ChildrenClosing(item) - item.DeleteChildren(self) - + self.ChildrenClosing(item) + item.DeleteChildren(self) def Delete(self, item): """ @@ -5506,7 +6065,7 @@ def Delete(self, item): parent.GetChildren().remove(item) # remove by value - else: # deleting the root + else: # deleting the root # nothing will be left in the tree self._anchor = None @@ -5526,6 +6085,10 @@ def Delete(self, item): item._wnd = None self._itemWithWindow.remove(item) + # Remove from selected items. + if item in self._selectedItems: + self._selectedItems.remove(item) + del item @@ -5548,7 +6111,7 @@ def Expand(self, item): """ if self.HasAGWFlag(TR_HIDE_ROOT) and item == self.GetRootItem(): - raise Exception("\nERROR: Can't Expand An Hidden Root. ") + raise Exception("\nERROR: Can't Expand An Hidden Root. ") if not item.HasPlus(): return @@ -5638,7 +6201,7 @@ def Collapse(self, item): """ if self.HasAGWFlag(TR_HIDE_ROOT) and item == self.GetRootItem(): - raise Exception("\nERROR: Can't Collapse An Hidden Root. ") + raise Exception("\nERROR: Can't Collapse An Hidden Root. ") if not item.IsExpanded(): return @@ -5659,8 +6222,6 @@ def Collapse(self, item): if self._hasWindows: self.HideWindows() - self.AdjustMyScrollbars() - event.SetEventType(wxEVT_TREE_ITEM_COLLAPSED) self.GetEventHandler().ProcessEvent(event) @@ -5705,7 +6266,7 @@ def HideItemWindows(self, item): if wnd: wnd.Hide() for child in item.GetChildren(): - self.HideItemWindows(child) + self.HideItemWindows(child) def HideItem(self, item, hide=True): @@ -5726,32 +6287,47 @@ def HideItem(self, item, hide=True): # Hide all windows for this item and its children. self.HideItemWindows(item) - # Refresh the tree. - self.CalculatePositions() - self.Refresh() - self.AdjustMyScrollbars() + # Tree needs to be recalculated (next paint or idle handler) + self._dirty = True def Unselect(self): """ Unselects the current selection. """ if self._current: - self._current.SetHilight(False) + self.SetItemHilight(self._current, False) self.RefreshLine(self._current) self._current = None self._select_me = None + # Provide method for compatibility with wx.TreeCtrl + ClearFocusedItem = Unselect + + + def UnselectItem(self, item): + """Unselects the given item, if it is selected. + + For multi-selection controls only. Simply calls :meth:`~CustomTreeCtrl.SetItemHilight` + to unselect the item. Use :meth:`~CustomTreeCtrl.Unselect` for + single-selection controls. + + :param `item`: an instance of :class:`GenericTreeItem`. + + .. versionadded:: 2.8 + """ + self.SetItemHilight(item, False) def UnselectAllChildren(self, item): """ Unselects all the children of the given item. + Mostly deprecated (no longer used internally as it's too slow). :param `item`: an instance of :class:`GenericTreeItem`. """ if item.IsSelected(): - item.SetHilight(False) + self.SetItemHilight(item, False) self.RefreshLine(item) if item.HasChildren(): @@ -5761,37 +6337,50 @@ def UnselectAllChildren(self, item): def SelectAllChildren(self, item): """ - Selects all the children of the given item. + Selects all the children of the given item. Does not send events. :param `item`: an instance of :class:`GenericTreeItem`. :raise: `Exception` if used without the ``TR_EXTENDED`` or ``TR_MULTIPLE`` style set. - :note: This method can be used only if :class:`CustomTreeCtrl` has the ``TR_MULTIPLE`` or ``TR_EXTENDED`` - style set. + :note: This method can be used only if :class:`CustomTreeCtrl` + has the ``TR_MULTIPLE`` or ``TR_EXTENDED`` style set. """ if not self.HasAGWFlag(TR_MULTIPLE) and not self.HasAGWFlag(TR_EXTENDED): raise Exception("SelectAllChildren can be used only with multiple selection enabled.") if not item.IsSelected(): - item.SetHilight(True) + self.SetItemHilight(item, True) self.RefreshLine(item) if item.HasChildren(): for child in item.GetChildren(): self.SelectAllChildren(child) + # Provide method for compatibility with wx.TreeCtrl + SelectChildren = SelectAllChildren def UnselectAll(self): - """ Unselect all the items. """ + """ Unselect all selected items in the tree. """ - rootItem = self.GetRootItem() + ## Note: When the TR_MULTIPLE style is set this method is called + ## every time the selection changes so it must be very fast. - # the tree might not have the root item at all - if rootItem: - self.UnselectAllChildren(rootItem) + # Calculate start and end of client area in logical Y coordinates. + width, height = self.GetClientSize() + start_y = self.CalcUnscrolledPosition(0, 0)[1] + last_y = self.CalcUnscrolledPosition(0, height)[1] + # Make all selected items unselected and Refresh only the visible ones. + for item in self._selectedItems: + item.SetHilight(False) + if (item.GetY() + self.GetLineHeight(item) >= start_y and + item.GetY() < last_y): + self.RefreshLine(item) + + # Reset state of selected items to none. + self._selectedItems.clear() self.Unselect() @@ -5801,8 +6390,8 @@ def SelectAll(self): :raise: `Exception` if used without the ``TR_EXTENDED`` or ``TR_MULTIPLE`` style set. - :note: This method can be used only if :class:`CustomTreeCtrl` has the ``TR_MULTIPLE`` or ``TR_EXTENDED`` - style set. + :note: This method can be used only if :class:`CustomTreeCtrl` + has the ``TR_MULTIPLE`` or ``TR_EXTENDED`` style set. """ if not self.HasAGWFlag(TR_MULTIPLE) and not self.HasAGWFlag(TR_EXTENDED): @@ -5823,35 +6412,62 @@ def SelectAll(self): # Keep going... if we found last_item, we stop. def TagNextChildren(self, crt_item, last_item, select): - """ Used internally. """ + """ Used internally to select a series of items in the tree. + + It traverses the complete tree looking for ``last_item`` including + all siblings and siblings of its parents. + + :param `crt_item`: an instance of :class:`GenericTreeItem`, + representing the current item in the series. + :param `last_item`: an instance of :class:`GenericTreeItem`, + representing the last item in the series to select. + :param bool `select`: ``True`` to select items, ``False`` to deselect them. + + :return: ``True`` if last_item has been reached, ``False`` otherwise. + """ parent = crt_item.GetParent() - if parent is None: # This is root item + if parent is None: # This is root item return self.TagAllChildrenUntilLast(crt_item, last_item, select) + # Find our index amonst our siblings. children = parent.GetChildren() index = children.index(crt_item) count = len(children) - for n in range(index+1, count): + # Search all siblings beneath us for last_item. + for n in range(index + 1, count): if self.TagAllChildrenUntilLast(children[n], last_item, select): return True + # last_item still not found. Continue search into parent's siblings. return self.TagNextChildren(parent, last_item, select) def TagAllChildrenUntilLast(self, crt_item, last_item, select): - """ Used internally. """ + """ Used internally to select a series of children in the tree. - crt_item.SetHilight(select) - self.RefreshLine(crt_item) + :param `crt_item`: an instance of :class:`GenericTreeItem`, + representing the current item in the series. + :param `last_item`: an instance of :class:`GenericTreeItem`, + representing the last item in the series to select. + :param bool `select`: ``True`` to select items, ``False`` to + deselect them. + + :return: ``True`` if last_item has been reached, ``False`` otherwise. + """ + + # Do not select hidden items. + if crt_item.IsHidden() is False: + self.SetItemHilight(crt_item, select) if crt_item == last_item: return True - if crt_item.HasChildren(): + # Do not select hidden (unexpanded) children. + if crt_item.IsExpanded() and crt_item.HasChildren(): for child in crt_item.GetChildren(): if self.TagAllChildrenUntilLast(child, last_item, select): return True @@ -5861,17 +6477,21 @@ def TagAllChildrenUntilLast(self, crt_item, last_item, select): def SelectItemRange(self, item1, item2): """ - Selects all the items between `item1` and `item2`. + Selects or unselects all the items between `item1` and `item2`. - :param `item1`: an instance of :class:`GenericTreeItem`, representing the first - item in the range to select; - :param `item2`: an instance of :class:`GenericTreeItem`, representing the last - item in the range to select. + The selection state of all items will be set to the same as the + current item. Will not select Hidden items but will select + disabled items. + + :param `item1`: an instance of :class:`GenericTreeItem`, + representing the first item in the range to select; + :param `item2`: an instance of :class:`GenericTreeItem`, + representing the last item in the range to select. :raise: `Exception` if used without the ``TR_EXTENDED`` or ``TR_MULTIPLE`` style set. - :note: This method can be used only if :class:`CustomTreeCtrl` has the ``TR_MULTIPLE`` or ``TR_EXTENDED`` - style set. + :note: This method can be used only if :class:`CustomTreeCtrl` + has the ``TR_MULTIPLE`` or ``TR_EXTENDED`` style set. """ if not self.HasAGWFlag(TR_MULTIPLE) and not self.HasAGWFlag(TR_EXTENDED): @@ -5881,29 +6501,36 @@ def SelectItemRange(self, item1, item2): # item2 is not necessary after item1 # choice first' and 'last' between item1 and item2 - first = (item1.GetY() < item2.GetY() and [item1] or [item2])[0] - last = (item1.GetY() < item2.GetY() and [item2] or [item1])[0] + first = item1 if item1.GetY() < item2.GetY() else item2 + last = item2 if item1.GetY() < item2.GetY() else item1 select = self._current.IsSelected() - if self.TagAllChildrenUntilLast(first, last, select): - return + # Tag this item and all of its children. + if self.TagAllChildrenUntilLast(first, last, select) is False: + # Did not find last item in children. Search into siblings/parents. + self.TagNextChildren(first, last, select) - self.TagNextChildren(first, last, select) + if select: + self.RefreshSelected() # Refresh all newly selected items. + else: + self.Refresh() # Refresh entire screen. - def DoSelectItem(self, item, unselect_others=True, extended_select=False, from_key=False): + def DoSelectItem(self, item, unselect_others=True, extended_select=False, + from_key=False): """ - Actually selects/unselects an item, sending ``EVT_TREE_SEL_CHANGING`` and - ``EVT_TREE_SEL_CHANGED`` events. + Actually selects/unselects an item, sending ``EVT_TREE_SEL_CHANGING`` + and ``EVT_TREE_SEL_CHANGED`` events. :param `item`: an instance of :class:`GenericTreeItem`; - :param bool `unselect_others`: if ``True``, all the other selected items are - unselected. - :param bool `extended_select`: ``True`` if the :class:`CustomTreeCtrl` is using the - ``TR_EXTENDED`` style; - :param bool `from_key`: ``True`` to indicate that the selection was made via a keyboard - key, ``False`` if it was a mouse selection. + :param bool `unselect_others`: if ``True``, all the other + selected items are unselected. + :param bool `extended_select`: ``True`` if the :class:`CustomTreeCtrl` + is using the ``TR_EXTENDED`` style; + :param bool `from_key`: ``True`` to indicate that the selection + was made via a keyboard key, ``False`` if it was a mouse + selection (can follow hyperlinks). """ self._select_me = None @@ -5915,7 +6542,7 @@ def DoSelectItem(self, item, unselect_others=True, extended_select=False, from_k if item.IsSelected() and not from_key: # Handles hypertext items self.HandleHyperLink(item) - return # nothing else to do + return # nothing else to do unselect_others = True extended_select = False @@ -5936,20 +6563,15 @@ def DoSelectItem(self, item, unselect_others=True, extended_select=False, from_k if self.GetEventHandler().ProcessEvent(event) and not event.IsAllowed(): return - parent = self.GetItemParent(item) - while parent: - if not self.IsExpanded(parent): - self.Expand(parent) - - parent = self.GetItemParent(parent) + # Ensure item is shown in the tree. + self.EnsureVisible(item) # ctrl press if unselect_others: if is_single: - self.Unselect() # to speed up thing + self.Unselect() # Unselect the current selection (quick). else: - self.UnselectAll() - + self.UnselectAll() # Unselect all items in the tree. # shift press if extended_select: if not self._current: @@ -5960,21 +6582,16 @@ def DoSelectItem(self, item, unselect_others=True, extended_select=False, from_k else: - select = True # the default + select = True # the default - # Check if we need to toggle highlight (ctrl mode) + # Check if we need to toggle hilight (ctrl mode) if not unselect_others: select = not item.IsSelected() self._current = self._key_current = item - self._current.SetHilight(select) + self.SetItemHilight(self._current, select) self.RefreshLine(self._current) - # This can cause idle processing to select the root - # if no item is selected, so it must be after the - # selection is set - self.EnsureVisible(item) - event.SetEventType(wxEVT_TREE_SEL_CHANGED) self.GetEventHandler().ProcessEvent(event) @@ -5990,23 +6607,40 @@ def SelectItem(self, item, select=True): :param `item`: an instance of :class:`GenericTreeItem`; :param bool `select`: ``True`` to select an item, ``False`` to deselect it. - :note: If TR_MULTIPLE is set, this actually toggles selection when select=True. + :note: If TR_MULTIPLE is set, this actually toggles selection + when select=True. """ if select: - + # DoSelectItem calls all event handlers before selecting. self.DoSelectItem(item, not self.HasAGWFlag(TR_MULTIPLE)) - else: # deselect - - item.SetHilight(False) + else: + # deselect item. + self.SetItemHilight(item, False) self.RefreshLine(item) + def SetFocusedItem(self, item): + """Sets the currently focused item. + + Sends selection events, and can be vetoed. Simply calls + :meth:`~CustomTreeCtrl.SelectItem` to select the item. + For multi-selection controls it is better to use + :meth:`~CustomTreeCtrl.SetItemHilight` to select items. + + :param `item`: an instance of :class:`GenericTreeItem`. + + .. versionadded:: 2.8 + """ + self.SelectItem(item, True) + + def FillArray(self, item, array=[]): """ - Internal function. Used to populate an array of selected items when - the style ``TR_MULTIPLE`` is used. + Used to populate an array of selected items when the style + ``TR_MULTIPLE`` is used. Only shown items (non-collapsed) are + returned. :param `item`: an instance of :class:`GenericTreeItem`; :param list `array`: a Python list containing the selected items. @@ -6031,12 +6665,15 @@ def GetSelections(self): """ Returns a list of selected items. - :note: This method can be used only if :class:`CustomTreeCtrl` has the ``TR_MULTIPLE`` or ``TR_EXTENDED`` - style set. + :note: This method can be used regardless if :class:`CustomTreeCtrl` + has the ``TR_MULTIPLE`` or ``TR_EXTENDED`` style set or not but + the return value is always a list (possibly empty). :return: A Python list containing the selected items, all instances of :class:`GenericTreeItem`. """ + ## We cannot simply return list(self._selectedItems) because + ## that would be an unordered list. Scan tree to build array. array = [] idRoot = self.GetRootItem() if idRoot: @@ -6092,14 +6729,23 @@ def ScrollTo(self, item): if not item: return - # We have to call this here because the label in - # question might just have been added and no screen - # update taken place. + # This yielding can cause events to recurse, i.e. when holding + # down the DOWN button the rapid key repeats means we may + # start processing another key during this yield before we're + # done with the current key event. It is a very dangerous, + # reckless, and buggy solution to no apparent problem. +## # We have to call this here because the label in +## # question might just have been added and no screen +## # update taken place. +## if self._dirty: +## if wx.Platform in ["__WXMSW__", "__WXMAC__"]: +## self.Update() +## else: +## wx.YieldIfNeeded() + + # ensure that the position of the item it calculated in any case if self._dirty: - if wx.Platform in ["__WXMSW__", "__WXMAC__"]: - self.Update() - else: - wx.YieldIfNeeded() + self.CalculatePositions() # now scroll to the item item_y = item.GetY() @@ -6107,24 +6753,18 @@ def ScrollTo(self, item): start_y *= _PIXELS_PER_UNIT client_w, client_h = self.GetClientSize() - # Calculate size of entire tree (not necessary anymore?) - x, y = self._anchor.GetSize(0, 0, self) - y += _PIXELS_PER_UNIT + 2 # one more scrollbar unit + 2 pixels - x += _PIXELS_PER_UNIT + 2 # one more scrollbar unit + 2 pixels x_pos = self.GetScrollPos(wx.HORIZONTAL) ## Note: The Scroll() method updates all child window positions ## while the SetScrollBars() does not (on most platforms). - if item_y < start_y+3: + if item_y < start_y + 3: # going down, item should appear at top self.Scroll(x_pos, item_y // _PIXELS_PER_UNIT) - #self.SetScrollbars(_PIXELS_PER_UNIT, _PIXELS_PER_UNIT, x//_PIXELS_PER_UNIT, y//_PIXELS_PER_UNIT, x_pos, item_y//_PIXELS_PER_UNIT) - elif item_y+self.GetLineHeight(item) > start_y+client_h: + elif item_y + self.GetLineHeight(item) > start_y + client_h: # going up, item should appear at bottom - item_y += _PIXELS_PER_UNIT+2 + item_y += _PIXELS_PER_UNIT + 2 self.Scroll(x_pos, (item_y + self.GetLineHeight(item) - client_h) // _PIXELS_PER_UNIT) - #self.SetScrollbars(_PIXELS_PER_UNIT, _PIXELS_PER_UNIT, x//_PIXELS_PER_UNIT, y//_PIXELS_PER_UNIT, x_pos, (item_y+self.GetLineHeight(item)-client_h)//_PIXELS_PER_UNIT ) def OnCompareItems(self, item1, item2): @@ -6223,13 +6863,7 @@ def GetLeftImageList(self): def CalculateLineHeight(self): - """Calculates the base height for all lines in the tree. - - Only used if the TR_HAS_VARIABLE_ROW_HEIGHT style is not used. - This base line height gets adjusted to the max line height - of all items as they are displayed. All rows use this largest - height until this method is called to reset it. - """ + """ Calculates the height of a line. """ dc = wx.ClientDC(self) self._lineHeight = dc.GetCharHeight() @@ -6291,9 +6925,11 @@ def CalculateLineHeight(self): self._lineHeight = height if self._lineHeight < 30: - self._lineHeight += 2 # at least 2 pixels + # Add at least 2 pixels. + self._lineHeight = int(self._lineHeight + 2) else: - self._lineHeight += self._lineHeight//10 # otherwise 10% extra spacing + # Otherwise 10% extra spacing + self._lineHeight = int(self._lineHeight + self._lineHeight // 10) def SetImageList(self, imageList): @@ -6308,6 +6944,9 @@ def SetImageList(self, imageList): self._imageListNormal = imageList self._ownsImageListNormal = False + # Mark all items for recalculation. + if self._anchor: + self.SetItemsDirty(self._anchor) self._dirty = True # Don't do any drawing if we're setting the list to NULL, @@ -6317,6 +6956,7 @@ def SetImageList(self, imageList): # We gray out the image list to use the grayed icons with disabled items sz = imageList.GetSize(0) + self._imgWidth, self._imgHeight = sz self._grayedImageList = wx.ImageList(sz[0], sz[1], True, 0) for ii in range(imageList.GetImageCount()): @@ -6336,7 +6976,9 @@ def SetLeftImageList(self, imageList): self._imageListLeft = imageList self._ownsImageListLeft = False - self._dirty = True + # Mark all items for recalculation. + if self._anchor: + self.SetItemsDirty(self._anchor) # Don't do any drawing if we're setting the list to NULL, # since we may be in the process of deleting the tree control. @@ -6345,6 +6987,7 @@ def SetLeftImageList(self, imageList): # We gray out the image list to use the grayed icons with disabled items sz = imageList.GetSize(0) + self._leftWidth, self._leftHeight = sz self._grayedImageListLeft = wx.ImageList(sz[0], sz[1], True, 0) for ii in range(imageList.GetImageCount()): @@ -6352,11 +6995,15 @@ def SetLeftImageList(self, imageList): newbmp = MakeDisabledBitmap(bmp) self._grayedImageListLeft.Add(newbmp) + # Force re-calculation of tree as all item X positions will change. + self.CalculatePositions() + def SetStateImageList(self, imageList): """ - Sets the state image list for :class:`CustomTreeCtrl` (from which application-defined - state images are taken). + Sets the state image list for :class:`CustomTreeCtrl` (from which + application-defined state images are taken). + State images are currently not implemented. :param `imageList`: an instance of :class:`wx.ImageList`. """ @@ -6381,6 +7028,9 @@ def SetButtonsImageList(self, imageList): self._imageListButtons = imageList self._ownsImageListButtons = False + # Mark all items for recalculation. + if self._anchor: + self.SetItemsDirty(self._anchor) self._dirty = True self.CalculateLineHeight() @@ -6398,7 +7048,7 @@ def SetImageListCheck(self, sizex, sizey, imglist=None): self._grayedCheckList = wx.ImageList(sizex, sizey, True, 0) if imglist is None: - + # Create default image list self._imageListCheck = wx.ImageList(sizex, sizey) # Get the Checkboxes @@ -6444,12 +7094,12 @@ def SetImageListCheck(self, sizex, sizey, imglist=None): enabled=True, x=sizex, y=sizey)) self._grayedCheckList.Add(self.GetControlBmp(checkbox=False, - checked=False, - enabled=False, - x=sizex, y=sizey)) + checked=False, + enabled=False, + x=sizex, y=sizey)) else: - + # Use user-supplied image list. sizex, sizey = imglist.GetSize(0) self._imageListCheck = imglist @@ -6459,6 +7109,9 @@ def SetImageListCheck(self, sizex, sizey, imglist=None): newbmp = MakeDisabledBitmap(bmp) self._grayedCheckList.Add(newbmp) + # Mark all items for recalculation. + if self._anchor: + self.SetItemsDirty(self._anchor) self._dirty = True if imglist: @@ -6515,8 +7168,15 @@ def AssignLeftImageList(self, imageList): # helpers # ----------------------------------------------------------------------------- - def AdjustMyScrollbars(self): - """ Internal method used to adjust the :class:`ScrolledWindow` scrollbars. """ + def AdjustMyScrollbars(self, tree_size=None): + """Internal method used to adjust the :class:`ScrolledWindow` scrollbars. + + This method is always called at the end of CalculatePositions() if the + tree size has changed. + + :param `tree_size`: The full size of the current tree as a wx.Size or + (width, height) tuple. If not specified it will be calculated. + """ if self._freezeCount: # Skip if frozen. Set dirty flag to adjust when thawed. @@ -6524,16 +7184,20 @@ def AdjustMyScrollbars(self): return if self._anchor: + if tree_size is not None: + x, y = tree_size + else: + x, y = self._anchor.GetSize(0, 0, self) - x, y = self._anchor.GetSize(0, 0, self) - y += _PIXELS_PER_UNIT + 2 # one more scrollbar unit + 2 pixels - x += _PIXELS_PER_UNIT + 2 # one more scrollbar unit + 2 pixels + y += _PIXELS_PER_UNIT + 2 # one more scrollbar unit + 2 pixels + x += _PIXELS_PER_UNIT + 2 # one more scrollbar unit + 2 pixels x_pos = self.GetScrollPos(wx.HORIZONTAL) y_pos = self.GetScrollPos(wx.VERTICAL) - self.SetScrollbars(_PIXELS_PER_UNIT, _PIXELS_PER_UNIT, x//_PIXELS_PER_UNIT, y//_PIXELS_PER_UNIT, x_pos, y_pos) - + self.SetScrollbars(_PIXELS_PER_UNIT, _PIXELS_PER_UNIT, + x // _PIXELS_PER_UNIT, y // _PIXELS_PER_UNIT, + x_pos, y_pos) else: - + # No root item. Reset scrollbars. self.SetScrollbars(0, 0, 0, 0) @@ -6545,11 +7209,18 @@ def GetLineHeight(self, item): :return: the item height, in pixels. """ - + # This is called EXTREMELY often so should be very fast. + # This takes ~1.253us (197ms for 160k items) if self.GetAGWWindowStyleFlag() & TR_HAS_VARIABLE_ROW_HEIGHT: - return int(item.GetHeight()) + return item.GetHeight() else: - return int(self._lineHeight) + return self._lineHeight + +## # Comparatively this takes ~0.877us (135ms for 160k items) +## if self._agwStyle & TR_HAS_VARIABLE_ROW_HEIGHT: +## return item._height +## else: +## return self._lineHeight def DrawVerticalGradient(self, dc, rect, hasfocus): @@ -6586,7 +7257,7 @@ def DrawVerticalGradient(self, dc, rect, hasfocus): rf, gf, bf = 0, 0, 0 for y in range(rect.y, rect.y + rect.height): - currCol = (int(r1 + rf), int(g1 + gf), int(b1 + bf)) + currCol = (r1 + rf, g1 + gf, b1 + bf) dc.SetBrush(wx.Brush(currCol, wx.BRUSHSTYLE_SOLID)) dc.DrawRectangle(rect.x, y, rect.width, 1) rf = rf + rstep @@ -6676,7 +7347,7 @@ def DrawVistaRectangle(self, dc, rect, hasfocus): bdrRect = wx.Rect(*rect.Get()) filRect = wx.Rect(*rect.Get()) - filRect.Deflate(1,1) + filRect.Deflate(1, 1) r1, g1, b1 = int(top.Red()), int(top.Green()), int(top.Blue()) r2, g2, b2 = int(bottom.Red()), int(bottom.Green()), int(bottom.Blue()) @@ -6693,7 +7364,7 @@ def DrawVistaRectangle(self, dc, rect, hasfocus): dc.SetPen(wx.TRANSPARENT_PEN) for y in range(filRect.y, filRect.y + filRect.height): - currCol = (int(r1 + rf), int(g1 + gf), int(b1 + bf)) + currCol = (r1 + rf, g1 + gf, b1 + bf) dc.SetBrush(wx.Brush(currCol, wx.BRUSHSTYLE_SOLID)) dc.DrawRectangle(filRect.x, y, filRect.width, 1) rf = rf + rstep @@ -6745,17 +7416,13 @@ def PaintItem(self, item, dc, level, align): else: dc.SetTextForeground(self.GetHyperTextNewColour()) - text_w, text_h, dummy = dc.GetFullMultiLineTextExtent(item.GetText()) + text_w, text_h = item.GetExtents(dc) w, h = self.GetClientSize() image = item.GetCurrentImage() checkimage = item.GetCurrentCheckedImage() - leftimage = _NO_IMAGE separator = item.IsSeparator() - if self._imageListLeft: - leftimage = item.GetLeftImage() - image_w, image_h = 0, 0 if image != _NO_IMAGE: @@ -6775,9 +7442,6 @@ def PaintItem(self, item, dc, level, align): else: wcheck, hcheck = 0, 0 - if leftimage != _NO_IMAGE: - l_image_w, l_image_h = self._imageListLeft.GetSize(leftimage) - total_h = self.GetLineHeight(item) drawItemBackground = False @@ -6791,8 +7455,8 @@ def PaintItem(self, item, dc, level, align): else: dc.SetBrush(self._hilightBrush) else: - dc.SetBrush((self._hasFocus and [self._hilightBrush] or [self._hilightUnfocusedBrush])[0]) - drawItemBackground = True + dc.SetBrush((self._hasFocus and [self._hilightBrush] or [self._hilightUnfocusedBrush])[0]) + drawItemBackground = True else: if attr and attr.HasBackgroundColour(): drawItemBackground = True @@ -6812,7 +7476,8 @@ def PaintItem(self, item, dc, level, align): if self.HasAGWFlag(TR_FULL_ROW_HIGHLIGHT): x = 0 - itemrect = wx.Rect(x, item.GetY()+offset, w, total_h-offset) + itemrect = wx.Rect(x, item.GetY() + offset, + max(w, self._width - 1), total_h - offset) if item.IsSelected(): if self._usegradients: @@ -6825,18 +7490,19 @@ def PaintItem(self, item, dc, level, align): else: if wx.Platform in ["__WXGTK2__", "__WXMAC__"]: flags = wx.CONTROL_SELECTED - if self._hasFocus: flags = flags | wx.CONTROL_FOCUSED + if self._hasFocus: + flags = flags | wx.CONTROL_FOCUSED wx.RendererNative.Get().DrawItemSelectionRect(self, dc, itemrect, flags) else: dc.DrawRectangle(itemrect) else: - if drawItemBackground: - minusicon = wcheck + image_w - 2 - itemrect = wx.Rect(item.GetX()+minusicon, - item.GetY()+offset, - item.GetWidth()-minusicon, - total_h-offset) - dc.DrawRectangle(itemrect) + if drawItemBackground: + minusicon = wcheck + image_w - 2 + itemrect = wx.Rect(item.GetX() + minusicon, + item.GetY() + offset, + item.GetWidth() - minusicon, + total_h - offset) + dc.DrawRectangle(itemrect) else: @@ -6852,14 +7518,14 @@ def PaintItem(self, item, dc, level, align): wndx, wndy = item.GetWindowSize() if separator: - item_width = w + item_width = max(w, self._width - 1) else: item_width = item.GetWidth() - image_w - wcheck + 2 - wndx itemrect = wx.Rect(item.GetX() + wcheck + image_w - 2, - item.GetY()+offset, + item.GetY() + offset, item_width, - total_h-offset) + total_h - offset) if self._usegradients: if self._gradientstyle == 0: # Horizontal @@ -6871,7 +7537,8 @@ def PaintItem(self, item, dc, level, align): else: if wx.Platform in ["__WXGTK2__", "__WXMAC__"]: flags = wx.CONTROL_SELECTED - if self._hasFocus: flags = flags | wx.CONTROL_FOCUSED + if self._hasFocus: + flags = flags | wx.CONTROL_FOCUSED wx.RendererNative.Get().DrawItemSelectionRect(self, dc, itemrect, flags) else: dc.DrawRectangle(itemrect) @@ -6884,14 +7551,14 @@ def PaintItem(self, item, dc, level, align): minusicon = wcheck + image_w - 2 if separator: - item_width = w + item_width = max(w, self._width - 1) else: - item_width = item.GetWidth()-minusicon + item_width = item.GetWidth() - minusicon - itemrect = wx.Rect(item.GetX()+minusicon, - item.GetY()+offset, + itemrect = wx.Rect(item.GetX() + minusicon, + item.GetY() + offset, item_width, - total_h-offset) + total_h - offset) if self._usegradients and self._hasFocus: if self._gradientstyle == 0: # Horizontal @@ -6903,7 +7570,7 @@ def PaintItem(self, item, dc, level, align): if image != _NO_IMAGE: - dc.SetClippingRegion(item.GetX(), item.GetY(), wcheck+image_w-2, total_h) + dc.SetClippingRegion(item.GetX(), item.GetY(), wcheck + image_w - 2, total_h) if item.IsEnabled(): imglist = self._imageListNormal else: @@ -6911,7 +7578,7 @@ def PaintItem(self, item, dc, level, align): imglist.Draw(image, dc, item.GetX() + wcheck, - item.GetY() + ((total_h > image_h) and [(total_h-image_h)//2] or [0])[0], + item.GetY() + ((total_h > image_h) and [(total_h - image_h) // 2] or [0])[0], wx.IMAGELIST_DRAW_TRANSPARENT) dc.DestroyClippingRegion() @@ -6924,29 +7591,33 @@ def PaintItem(self, item, dc, level, align): imglist.Draw(checkimage, dc, item.GetX(), - item.GetY() + ((total_h > hcheck) and [(total_h-hcheck)//2] or [0])[0], + item.GetY() + ((total_h > hcheck) and [(total_h - hcheck) // 2] or [0])[0], wx.IMAGELIST_DRAW_TRANSPARENT) + # Draw the left image for this item, if defined. + leftimage = _NO_IMAGE + if self._imageListLeft: + leftimage = item.GetLeftImage() if leftimage != _NO_IMAGE: if item.IsEnabled(): imglist = self._imageListLeft else: imglist = self._grayedImageListLeft - - imglist.Draw(leftimage, dc, - 4, - item.GetY() + ((total_h > l_image_h) and [(total_h-l_image_h)//2] or [0])[0], + # Center left image if smaller than total line height. + l_image_w, l_image_h = imglist.GetSize(leftimage) + y_offset = (total_h - l_image_h) // 2 if total_h > l_image_h else 0 + imglist.Draw(leftimage, dc, 4, item.GetY() + y_offset, wx.IMAGELIST_DRAW_TRANSPARENT) dc.SetBackgroundMode(wx.TRANSPARENT) - extraH = ((total_h > text_h) and [(total_h - text_h)//2] or [0])[0] + extraH = ((total_h > text_h) and [(total_h - text_h) // 2] or [0])[0] textrect = wx.Rect(wcheck + image_w + item.GetX(), item.GetY() + extraH, text_w, text_h) itemText = item.GetText() if self.HasAGWFlag(TR_ELLIPSIZE_LONG_ITEMS) and not separator: xa, ya = self.CalcScrolledPosition((0, item.GetY())) - maxsize = w - (wcheck + image_w + item.GetX()) + xa + maxsize = w - (wcheck + image_w + item.GetX()) - xa itemText = ChopText(dc, itemText, maxsize) if not item.IsEnabled(): @@ -6963,13 +7634,14 @@ def PaintItem(self, item, dc, level, align): on_right = item._windowontheright # Helio: Should I make a getter? if wnd: if on_right: # Helio: Original behaviour + # This +4 doesn't match the +2 in CalculateSize. wndx = wcheck + image_w + item.GetX() + text_w + 4 else: wndx = wcheck + item.GetX() xa, ya = self.CalcScrolledPosition((0, item.GetY())) wndx += xa if item.GetHeight() > item.GetWindowSize()[1]: - ya += (item.GetHeight() - item.GetWindowSize()[1])//2 + ya += (item.GetHeight() - item.GetWindowSize()[1]) // 2 if align == 1: # Horizontal alignment of windows @@ -6981,6 +7653,11 @@ def PaintItem(self, item, dc, level, align): wndx = w - item.GetWindowSize().x - 2 + xa if wnd.GetPosition() != (wndx, ya): + ## Move window vertically and horizontally. Doing this + ## in the OnPaint handler is somewhat dangerous. Moving + ## the window will invalidate areas that are revealed. + ## This can cause glitching on some platforms but only + ## needs to be done rarely, so it is mostly acceptable. wnd.Move(wndx, ya, flags=wx.SIZE_ALLOW_MINUS_ONE) # Force window visible after any position changes were made. if not wnd.IsShown(): @@ -6998,7 +7675,8 @@ def PaintItem(self, item, dc, level, align): separatorPen = wx.GREY_PEN dc.SetPen(separatorPen) - dc.DrawLine(item.GetX()+2, item.GetY()+total_h//2, w, item.GetY()+total_h//2) + dc.DrawLine(item.GetX() + 2, item.GetY() + total_h // 2, + max(w, self._width - 1), item.GetY() + total_h // 2) dc.SetPen(oldPen) # restore normal font @@ -7029,50 +7707,34 @@ def PaintLevel(self, item, dc, level, y, align): if item.IsHidden(): return y - x = level*self._indent - - left_image_list = 0 - if self._imageListLeft: - left_image_list += self._imageListLeft.GetBitmap(0).GetWidth() - - x += left_image_list - - if not self.HasAGWFlag(TR_HIDE_ROOT): - - x += self._indent - - elif level == 0: + # Get this item's X,Y position. + x = item.GetX() - self._spacing + y = item.GetY() - # always expand hidden root - origY = y + if level == 0 and self.HasAGWFlag(TR_HIDE_ROOT): + # Always expand hidden root. children = item.GetChildren() - count = len(children) - - if count > 0: - n = 0 - while n < count: - oldY = y - y = self.PaintLevel(children[n], dc, 1, y, align) - n = n + 1 - - if not self.HasAGWFlag(TR_NO_LINES) and self.HasAGWFlag(TR_LINES_AT_ROOT) and count > 0: - - # draw line down to last child - origY += self.GetLineHeight(children[0])>>1 - oldY += self.GetLineHeight(children[n-1])>>1 - oldPen = dc.GetPen() - dc.SetPen(self._dottedPen) - dc.DrawLine(3, origY, 3, oldY) - dc.SetPen(oldPen) + for child in children: + y = self.PaintLevel(child, dc, 1, y, align) + + # Draw hidden root line if TR_LINES_AT_ROOT specified. + if (len(children) > 1 and not self.HasAGWFlag(TR_NO_LINES) and + self.HasAGWFlag(TR_LINES_AT_ROOT)): + # Draw line down from first child to last child. + firstY = children[0].GetY() + firstY += self.GetLineHeight(children[0]) // 2 + lastY = children[-1].GetY() + lastY += self.GetLineHeight(children[-1]) // 2 + oldPen = dc.GetPen() + dc.SetPen(self._dottedPen) + dc.DrawLine(3, firstY, 3, lastY) + dc.SetPen(oldPen) return y - item.SetX(x+self._spacing) - item.SetY(y) - h = self.GetLineHeight(item) y_top = y - y_mid = y_top + (h>>1) + y_mid = y_top + (h >> 1) y += h exposed_x = dc.LogicalToDeviceX(0) @@ -7087,10 +7749,7 @@ def PaintLevel(self, item, dc, level, y, align): pen = self._borderPen if item.IsSelected(): - if (wx.Platform == "__WXMAC__" and self._hasFocus): - colText = wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT) - else: - colText = wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT) + colText = wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT) if self._vistaselection: colText = wx.BLACK @@ -7132,7 +7791,7 @@ def PaintLevel(self, item, dc, level, y, align): # draw the horizontal line here dc.SetPen(self._dottedPen) x_start = x - if x > self._indent+left_image_list: + if x > self._indent + self._leftWidth: x_start -= self._indent elif self.HasAGWFlag(TR_LINES_AT_ROOT): x_start = 3 @@ -7152,15 +7811,15 @@ def PaintLevel(self, item, dc, level, y, align): image += TreeItemIcon_Selected - TreeItemIcon_Normal image_w, image_h = self._imageListButtons.GetSize(image) - xx = x - image_w//2 - yy = y_mid - image_h//2 + xx = x - image_w // 2 + yy = y_mid - image_h // 2 dc.SetClippingRegion(xx, yy, image_w, image_h) self._imageListButtons.Draw(image, dc, xx, yy, wx.IMAGELIST_DRAW_TRANSPARENT) dc.DestroyClippingRegion() - else: # no custom buttons + else: # no custom buttons if self.HasAGWFlag(TR_TWIST_BUTTONS): # We draw something like the Mac twist buttons @@ -7199,46 +7858,62 @@ def PaintLevel(self, item, dc, level, y, align): if item == self._underMouse: flag |= _CONTROL_CURRENT - self._drawingfunction(self, dc, wx.Rect(x - wImage//2, y_mid - hImage//2, wImage, hImage), flag) + self._drawingfunction(self, dc, wx.Rect(x - wImage // 2, y_mid - hImage // 2, wImage, hImage), flag) + # If this item is expanded, handle its children. if item.IsExpanded(): children = item.GetChildren() count = len(children) if count > 0: - + # Item has children. Draw only those that are visible. n = 0 level = level + 1 + # Calculate start and end of client area in logical Y coordinates. + width, height = self.GetClientSize() + start_y = self.CalcUnscrolledPosition(0, 0)[1] + last_y = self.CalcUnscrolledPosition(0, height)[1] + + # If this item is off the bottom of the screen, do nothing. + if y_top > last_y: + return y_top # Early exit (for TR_HIDE_ROOT only) + + # Binary search for first child that is within our draw area. + n = BisectChildren(children, start_y) + + # Now paint only prospective children while n < count: - oldY = y y = self.PaintLevel(children[n], dc, level, y, align) n = n + 1 + if y > last_y: + break # Early exit - if not self.HasAGWFlag(TR_NO_LINES) and count > 0: - - # draw line down to last child - oldY += self.GetLineHeight(children[n-1])>>1 + if not self.HasAGWFlag(TR_NO_LINES): + # Draw vertical tree line down to middle of last child. + lastY = children[-1].GetY() + lastY += self.GetLineHeight(children[-1]) // 2 if self.HasButtons(): - y_mid += 5 + y_mid += 5 # Half of button height. - # Only draw the portion of the line that is visible, in case it is huge + # Clip the vertical line to only the visible portion. + # Required speedup since the default _dottedPen is a + # USER_DASH style that draws very slow on some platforms. xOrigin, yOrigin = dc.GetDeviceOrigin() yOrigin = abs(yOrigin) - width, height = self.GetClientSize() - - # Move end points to the beginning/end of the view? + + # Move end points to the begining/end of the view? if y_mid < yOrigin: y_mid = yOrigin - if oldY > yOrigin + height: - oldY = yOrigin + height + if lastY > yOrigin + height: + lastY = yOrigin + height - # after the adjustments if y_mid is larger than oldY then the line - # isn't visible at all so don't draw anything - if y_mid < oldY: + # after the adjustments if y_mid is larger than lastY + # then the line isn't visible at all so don't draw anything + if y_mid < lastY: dc.SetPen(self._dottedPen) - dc.DrawLine(x, y_mid, x, oldY) + dc.DrawLine(x, y_mid, x, lastY) return y @@ -7257,9 +7932,12 @@ def OnPaint(self, event): dc = wx.PaintDC(self) self.PrepareDC(dc) - if not self._anchor: + if not self._anchor or self._freezeCount: return + if self._dirty is True: + self.CalculatePositions() + dc.SetFont(self._normalFont) dc.SetPen(self._dottedPen) @@ -7288,9 +7966,7 @@ def OnSize(self, event): if self.HasAGWFlag(TR_ALIGN_WINDOWS_RIGHT) and self._itemWithWindow: self.RefreshItemWithWindows() - else: - self.RefreshSelected() - + self.RefreshSelected() event.Skip() @@ -7390,8 +8066,8 @@ def OnKeyDown(self, event): self._current = self._key_current = self.GetFirstVisibleItem() # how should the selection work for this event? - is_multiple, extended_select, unselect_others = EventFlagsToSelType(self.GetAGWWindowStyleFlag(), - event.ShiftDown(), event.CmdDown()) + is_multiple, extended_select, unselect_others = EventFlagsToSelType( + self.GetAGWWindowStyleFlag(), event.ShiftDown(), event.CmdDown()) # + : Expand # - : Collaspe @@ -7411,12 +8087,12 @@ def OnKeyDown(self, event): if self._current.HasPlus() and not self.IsExpanded(self._current) and self.IsItemEnabled(self._current): self.Expand(self._current) - elif keyCode in (ord('*'), wx.WXK_MULTIPLY, wx.WXK_NUMPAD_MULTIPLY): # "*" + elif keyCode in (ord('*'), wx.WXK_MULTIPLY, wx.WXK_NUMPAD_MULTIPLY): # "*" if not self.IsExpanded(self._current) and self.IsItemEnabled(self._current): # expand all self.ExpandAllChildren(self._current) - elif keyCode in (ord('-'), wx.WXK_SUBTRACT, wx.WXK_NUMPAD_SUBTRACT): # "-" + elif keyCode in (ord('-'), wx.WXK_SUBTRACT, wx.WXK_NUMPAD_SUBTRACT): # "-" if self.IsExpanded(self._current): self.Collapse(self._current) @@ -7426,7 +8102,7 @@ def OnKeyDown(self, event): event = TreeEvent(wxEVT_TREE_ITEM_MENU, self.GetId()) event._item = self._current # Use the left edge, vertical middle - event._pointDrag = wx.Point(itemRect.GetX(), itemRect.GetY() + itemRect.GetHeight()//2) + event._pointDrag = wx.Point(itemRect.GetX(), itemRect.GetY() + itemRect.GetHeight() // 2) event.SetEventObject(self) self.GetEventHandler().ProcessEvent(event) @@ -7445,7 +8121,7 @@ def OnKeyDown(self, event): if keyCode == wx.WXK_SPACE and self.GetItemType(self._current) > 0: if self.IsItem3State(self._current): checked = self.GetItem3StateValue(self._current) - checked = (checked+1)%3 + checked = (checked + 1) % 3 else: checked = not self.IsItemChecked(self._current) @@ -7465,9 +8141,9 @@ def OnKeyDown(self, event): # of its children if it's expanded elif keyCode == wx.WXK_UP: # Search for a previous, enabled item. - prev = self.GetPrevShown(self._key_current) + prev, cookie = self.GetPrevShownFast(self._key_current) while prev and self.IsItemEnabled(prev) is False: - prev = self.GetPrevShown(prev) + prev, cookie = self.GetPrevShownFast(prev, cookie) # If we found a valid enabled item, select it. if prev: @@ -7503,10 +8179,9 @@ def OnKeyDown(self, event): elif keyCode == wx.WXK_DOWN: # Scan for next enabled item. - next = self.GetNextShown(self._key_current) + next, cookie = self.GetNextShownFast(self._key_current) while next and self.IsItemEnabled(next) is False: - next = self.GetNextShown(next) - + next, cookie = self.GetNextShownFast(next, cookie) if next: self.DoSelectItem(next, unselect_others, extended_select, from_key=True) self._key_current = next @@ -7514,20 +8189,22 @@ def OnKeyDown(self, event): # selects the last enabled tree item. elif keyCode == wx.WXK_END: - top = self.GetRootItem() - if self.HasAGWFlag(TR_HIDE_ROOT): - top, cookie = self.GetFirstChild(top) - + # Get the very last item in the tree. + item = self.GetRootItem() lastEnabled = None - while top: - # Keep track of last enabled item encountered. - if self.IsItemEnabled(top): - lastEnabled = top - # Top-most item is not enabled. Scan for next item. - top = self.GetNextShown(top) + while item: + lastEnabled = item + item = self.GetLastChild(item) + + # Find the last shown and enabled item. + cookie = None + while lastEnabled and (self.IsItemShown(lastEnabled) is False or + self.IsItemEnabled(lastEnabled) is False): + lastEnabled, cookie = self.GetPrevShownFast(lastEnabled, cookie) if lastEnabled: - self.DoSelectItem(lastEnabled, unselect_others, extended_select, from_key=True) + self.DoSelectItem(lastEnabled, unselect_others, + extended_select, from_key=True) # selects the first enabled tree item. elif keyCode == wx.WXK_HOME: @@ -7537,9 +8214,10 @@ def OnKeyDown(self, event): top, cookie = self.GetFirstChild(top) # Scan for first enabled and displayed item. + cookie = None while top and self.IsItemEnabled(top) is False: - top = self.GetNextShown(top) - + top, cookie = self.GetNextShownFast(top, cookie) + if top: self.DoSelectItem(top, unselect_others, extended_select, from_key=True) @@ -7553,7 +8231,7 @@ def OnKeyDown(self, event): return # Is the current item visible? clientWidth, clientHeight = self.GetClientSize() - itemHeight = currentItem.GetHeight() + itemHeight = self.GetLineHeight(currentItem) pageSize = max(int(clientHeight * 0.9), clientHeight - itemHeight) x, y = self.CalcScrolledPosition(0, currentItem.GetY()) if y >= 0 and (y + itemHeight) < clientHeight: @@ -7562,17 +8240,17 @@ def OnKeyDown(self, event): visCount = 0 # Current item not visible. # Move upwards in tree until last visible, or pagesize hit. amount = 0 - targetItem = currentItem - prevItem = self.GetPrevShown(currentItem) + targetItem = None + prevItem, cookie = self.GetPrevShownFast(currentItem) while prevItem: - itemHeight = prevItem.GetHeight() + itemHeight = self.GetLineHeight(prevItem) if visCount: # Is this item also visible? x, y = self.CalcScrolledPosition(0, prevItem.GetY()) if y >= 0 and (y + itemHeight) < clientHeight: visCount += 1 else: - if visCount > 1 and targetItem != currentItem: + if visCount > 1 and targetItem is not None: # Move to top visible item in page. break visCount = 0 @@ -7581,11 +8259,11 @@ def OnKeyDown(self, event): targetItem = prevItem amount += itemHeight # Break loop if we moved up a page size and have a new target. - if amount > pageSize and targetItem != currentItem: + if amount > pageSize and targetItem is not None: break - prevItem = self.GetPrevShown(prevItem) + prevItem, cookie = self.GetPrevShownFast(prevItem, cookie) # If we found a valid target, select it. - if targetItem != currentItem: + if targetItem is not None: self.DoSelectItem(targetItem, unselect_others=True, extended_select=False, from_key=True) else: @@ -7602,7 +8280,7 @@ def OnKeyDown(self, event): return # Is the current item visible? clientWidth, clientHeight = self.GetClientSize() - itemHeight = currentItem.GetHeight() + itemHeight = self.GetLineHeight(currentItem) pageSize = max(int(clientHeight * 0.9), clientHeight - itemHeight) x, y = self.CalcScrolledPosition(0, currentItem.GetY()) if y >= 0 and (y + itemHeight) < clientHeight: @@ -7611,17 +8289,17 @@ def OnKeyDown(self, event): visCount = 0 # Current item not visible. # Move downwards in tree until last visible, or pagesize hit. amount = 0 - targetItem = currentItem - nextItem = self.GetNextShown(currentItem) + targetItem = None + nextItem, cookie = self.GetNextShownFast(currentItem) while nextItem: - itemHeight = nextItem.GetHeight() + itemHeight = self.GetLineHeight(nextItem) if visCount: # Is this item also visible? x, y = self.CalcScrolledPosition(0, nextItem.GetY()) if y >= 0 and (y + itemHeight) < clientHeight: visCount += 1 else: - if visCount > 1 and targetItem != currentItem: + if visCount > 1 and targetItem is not None: # Move to last visible item in page. break visCount = 0 @@ -7630,11 +8308,11 @@ def OnKeyDown(self, event): targetItem = nextItem amount += itemHeight # Break loop if we moved down a page size and have a new target. - if amount > pageSize and targetItem != currentItem: + if amount > pageSize and targetItem is not None: break - nextItem = self.GetNextShown(nextItem) + nextItem, cookie = self.GetNextShownFast(nextItem, cookie) # If we found a valid target, select it. - if targetItem != currentItem: + if targetItem is not None: self.DoSelectItem(targetItem, unselect_others=True, extended_select=False, from_key=True) else: @@ -7644,22 +8322,32 @@ def OnKeyDown(self, event): # Some other key pressed. Consume character for item search. else: - if not event.HasModifiers() and ((keyCode >= ord('0') and keyCode <= ord('9')) or \ - (keyCode >= ord('a') and keyCode <= ord('z')) or \ + if not event.HasModifiers() and ((keyCode >= ord('0') and keyCode <= ord('9')) or + (keyCode >= ord('a') and keyCode <= ord('z')) or (keyCode >= ord('A') and keyCode <= ord('Z'))): # find the next item starting with the given prefix ch = chr(keyCode) - id = self.FindItem(self._current, self._findPrefix + ch) - - if not id: - # no such item - return - - if self.IsItemEnabled(id): - self.SelectItem(id) self._findPrefix += ch + # Don't waste time searching if the last prefix failed. + if self._findFail is False: + # Search for a matching item Id. + id = self.FindItem(self._current, self._findPrefix, + all=self._findAll) + if id: + # Found a matching item. Select it, if enabled. + if self.IsItemEnabled(id): + #self.SelectItem(id) + self.DoSelectItem(id, unselect_others=True) + else: + # Failed to find a match. + if self._findBellOnNoMatch is True: + # Bell only on first failure. + if self._findFail is False: + wx.Bell() + self._findFail = True + # also start the timer to reset the current prefix if the user # doesn't press any more alnum keys soon -- we wouldn't want # to use this prefix for a new item search @@ -7675,70 +8363,163 @@ def OnKeyDown(self, event): def GetPrevShown(self, item): """ - Returns the previous displayed item in the tree. This is either the - last displayed child of its previous sibling, or its parent item. - + Returns the previous displayed item in the tree. This is either + the last displayed child of its previous sibling, or its parent + item. This logic is repeated until a non-hidden item is found. + :param `item`: an instance of :class:`GenericTreeItem`; - :return: An instance of :class:`GenericTreeItem` or ``None`` if no previous item found (root). + :return: An instance of :class:`GenericTreeItem` or ``None`` if + no previous non-hidden item is found (root). """ - if not item: - return None - # Try to get previous sibling. - prev = self.GetPrevSibling(item) - if prev: - # Drill down to last displayed child of previous sibling. - while self.IsExpanded(prev) and self.HasChildren(prev): - prev = self.GetLastChild(prev) - else: - # No previous sibling. Move to parent. - prev = self.GetItemParent(item) + prev = None + while item: + # Try to get previous sibling. + prev = self.GetPrevSibling(item) + if prev: + # Drill down to last displayed child of previous sibling. + while self.IsExpanded(prev) and self.HasChildren(prev): + prev = self.GetLastChild(prev) + else: + # No previous sibling. Move to parent. + prev = self.GetItemParent(item) + item = prev + if prev and prev.IsHidden() is False: + # Found a valid non-hidden item. Break the loop. + item = None # Suppress returning root item if TR_HIDE_ROOT flag is set. if prev == self.GetRootItem() and self.HasAGWFlag(TR_HIDE_ROOT): return None return prev + + + def GetPrevShownFast(self, item, cookie=None): + """ + Returns the previous displayed item in the tree. This is either + the last displayed child of its previous sibling, or its parent + item. This logic is repeated until a non-hidden item is found. + + :param `item`: an instance of :class:`GenericTreeItem`; + :param `cookie`: a parameter which is opaque for the application + but is necessary for the library to make this function fast. If + cookie is `None` this function will still work but much slower. + :return: A tuple with the first value an instance of + :class:`GenericTreeItem` or ``None`` if no previous non-hidden + item is found (root), and as second value an integer 'cookie'. + + .. versionadded:: 2.8 + """ + prev = None + while item: + # Try to get previous sibling. + prev, cookie = self.GetPrevSiblingFast(item, cookie) + if prev: + # Drill down to last displayed child of previous sibling. + while self.IsExpanded(prev) and self.HasChildren(prev): + prev = self.GetLastChild(prev) + else: + # No previous sibling. Move to parent. + prev = self.GetItemParent(item) + item = prev + if prev and prev.IsHidden() is False: + # Found a valid non-hidden item. Break the loop. + item = None + # Suppress returning root item if TR_HIDE_ROOT flag is set. + if prev == self.GetRootItem() and self.HasAGWFlag(TR_HIDE_ROOT): + return None, cookie + return prev, cookie + def GetNextShown(self, item): """ - Returns the next displayed item in the tree. This is either the first - child of the item (if it is expanded and has children) or its next - sibling. If there is no next sibling the tree is walked backwards - until a next sibling for one of its parents is found. + Returns the next displayed item in the tree. This is either the + first child of the item (if it is expanded and has children) or + its next sibling. If there is no next sibling the tree is walked + backwards until a next sibling for one of its parents is found. + This is repeated until a non-hidden item is found. :param `item`: an instance of :class:`GenericTreeItem`; - :return: An instance of :class:`GenericTreeItem` or ``None`` if no item follows this one. + :return: An instance of :class:`GenericTreeItem` or ``None`` if + no non-hidden item follows this one. """ - if not item: - return None - # Is the item expanded and has children? - if self.IsExpanded(item) and self.HasChildren(item): - # Next item = first child. - next, cookie = self.GetFirstChild(item) - else: - # Next item = next sibling. - sibling = self.GetNextSibling(item) - parent = self.GetItemParent(item) - while not sibling and parent and parent != self.GetRootItem(): - # No sibling. Try parent's sibling until root reached. - sibling = self.GetNextSibling(parent) - parent = self.GetItemParent(parent) - next = sibling + next = None + while item: + # Is the item expanded and has children? + if self.IsExpanded(item) and self.HasChildren(item): + # Next item = first child. + next, cookie = self.GetFirstChild(item) + else: + # Next item = next sibling. + sibling = self.GetNextSibling(item) + parent = self.GetItemParent(item) + while not sibling and parent and parent != self.GetRootItem(): + # No sibling. Try parent's sibling until root reached. + sibling = self.GetNextSibling(parent) + parent = self.GetItemParent(parent) + next = sibling + item = next + if next and next.IsHidden() is False: + # Found a valid non-hidden item. Break the loop. + item = None # Return the next item. return next + def GetNextShownFast(self, item, cookie=None): + """ + Returns the next displayed item in the tree. This is either the + first child of the item (if it is expanded and has children) or + its next sibling. If there is no next sibling the tree is walked + backwards until a next sibling for one of its parents is found. + This is repeated until a non-hidden item is found. + + :param `item`: an instance of :class:`GenericTreeItem`; + :param `cookie`: a parameter which is opaque for the application + but is necessary for the library to make this function fast. If + cookie is `None` this function will still work but much slower. + + :return: A tuple with the first value being an instance of + :class:`GenericTreeItem` or ``None`` if no non-hidden item + follows this one, and as second value an integer 'cookie'. + + .. versionadded:: 2.8 + """ + next = None + while item: + # Is the item expanded and has children? + if self.IsExpanded(item) and self.HasChildren(item): + # Next item = first child. + next, cookie = self.GetFirstChild(item) + else: + # Next item = next sibling. + sibling, cookie = self.GetNextSiblingFast(item, cookie) + parent = self.GetItemParent(item) + while not sibling and parent and parent != self.GetRootItem(): + # No sibling. Try parent's sibling until root reached. + sibling, cookie = self.GetNextSiblingFast(parent, cookie) + parent = self.GetItemParent(parent) + next = sibling + item = next + if next and next.IsHidden() is False: + # Found a valid non-hidden item. Break the loop. + item = None + # Return the next item. + return next, cookie + + def GetNextActiveItem(self, item, down=True): """ - Returns the next active item. Used Internally at present. + Returns the next active item. Deprecated, included for + backwards compatibility. :param `item`: an instance of :class:`GenericTreeItem`; - :param bool `down`: ``True`` to search downwards in the hierarchy for an active item, - ``False`` to search upwards. + :param bool `down`: ``True`` to search downwards in the hierarchy + for an active item, ``False`` to search upwards. - :return: An instance of :class:`GenericTreeItem` if an active item has been found or - ``None`` if none has been found. + :return: An instance of :class:`GenericTreeItem` if an active + item has been found or ``None`` if none has been found. """ if down: @@ -7863,8 +8644,8 @@ def GetBoundingRect(self, item, textOnly=False): startX, startY = self.GetViewStart() rect = wx.Rect() - rect.x = i.GetX() - startX*_PIXELS_PER_UNIT - rect.y = i.GetY() - startY*_PIXELS_PER_UNIT + rect.x = i.GetX() - startX * _PIXELS_PER_UNIT + rect.y = i.GetY() - startY * _PIXELS_PER_UNIT rect.width = i.GetWidth() rect.height = self.GetLineHeight(i) @@ -8040,7 +8821,7 @@ def OnMouse(self, event): # (pass to the user code), left dbl click (activate item) and # dragging/moving events for items drag-and-drop - if not (event.LeftDown() or event.LeftUp() or event.RightDown() or event.LeftDClick() or \ + if not (event.LeftDown() or event.LeftUp() or event.RightDown() or event.LeftDClick() or event.Dragging() or ((event.Moving() or event.RightUp()) and self._isDragging)): event.Skip() @@ -8088,13 +8869,13 @@ def OnMouse(self, event): if self._oldSelection: - self._oldSelection.SetHilight(False) + self.SetItemHilight(self._oldSelection, False) self.RefreshLine(self._oldSelection) else: selections = self.GetSelections() if len(selections) == 1: self._oldSelection = selections[0] - self._oldSelection.SetHilight(False) + self.SetItemHilight(self._oldSelection, False) self.RefreshLine(self._oldSelection) if self._dragImage: @@ -8102,7 +8883,7 @@ def OnMouse(self, event): # Create the custom draw image from the icons and the text of the item self._dragImage = DragImage(self, self._current) - self._dragImage.BeginDrag(wx.Point(0,0), self, fullScreen=self._dragFullScreen) + self._dragImage.BeginDrag(wx.Point(0, 0), self, fullScreen=self._dragFullScreen) self._dragImage.Show() self._dragImage.Move(self.CalcScrolledPosition(*pt)) @@ -8119,11 +8900,14 @@ def OnMouse(self, event): if item != self._dropTarget: - # unhighlight the previous drop target if self._dropTarget: + ## Unhighlight the previous drop target. Don't use + ## SetItemHilight() so we don't affect selection. self._dropTarget.SetHilight(False) self.RefreshLine(self._dropTarget) if item: + ## Highlight new potential drop target. Don't use + ## SetItemHilight() so we don't affect selection. item.SetHilight(True) self.RefreshLine(item) self._countDrag = self._countDrag + 1 @@ -8142,11 +8926,11 @@ def OnMouse(self, event): self._dragImage.EndDrag() if self._dropTarget: - self._dropTarget.SetHilight(False) + self.SetItemHilight(self._dropTarget, False) if self._oldSelection: - - self._oldSelection.SetHilight(True) + # Restore old selected item (don't send events). + self.SetItemHilight(self._oldSelection, True) self.RefreshLine(self._oldSelection) self._oldSelection = None @@ -8249,7 +9033,7 @@ def OnMouse(self, event): self._lastOnSame = False - else: # !RightDown() && !LeftUp() ==> LeftDown() || LeftDClick() + else: # !RightDown() && !LeftUp() ==> LeftDown() || LeftDClick() if not item or not item.IsEnabled(): if self._editCtrl is not None and item != self._editCtrl.item(): @@ -8285,7 +9069,7 @@ def OnMouse(self, event): if self.IsItem3State(item): checked = self.GetItem3StateValue(item) - checked = (checked+1)%3 + checked = (checked + 1) % 3 else: checked = not self.IsItemChecked(item) @@ -8373,14 +9157,13 @@ def OnInternalIdle(self): self.CalculatePositions() self.Refresh() - self.AdjustMyScrollbars() # event.Skip() def CalculateSize(self, item, dc, level=-1, align=0): """ - Calculates overall position and size of an item. + Calculates the width and height of an item. Clears the dirty flag :param `item`: an instance of :class:`GenericTreeItem`; :param `dc`: an instance of :class:`wx.DC`; @@ -8397,45 +9180,44 @@ def CalculateSize(self, item, dc, level=-1, align=0): """ - if self._freezeCount: - # Skip calculate if frozen. Set dirty flag to do this when thawed. - self._dirty = True - return + # Clear dirty flag. + item.SetDirty(False) + + # Hidden items have a height of 0 and width is irrelevant. if item.IsHidden(): - # Hidden items have a height of 0. item.SetHeight(0) return - attr = item.GetAttributes() - - if attr and attr.HasFont(): - dc.SetFont(attr.GetFont()) - else: - if item.IsBold(): - dc.SetFont(self._boldFont) + # Calcualte text width + if item.HasExtents() is False: + # Avoid this stuff unless necessary. + attr = item.GetAttributes() + if item.IsHyperText(): + font = self.GetHyperTextFont() # Hypertext font. + elif attr and attr.HasFont(): + font = attr.GetFont() # User-defined font. + elif item.IsBold(): + font = self._boldFont # Bold font. elif item.IsItalic(): - dc.SetFont(self._italicFont) + font = self._italicFont # Italics font. else: - dc.SetFont(self._normalFont) - - text_w, text_h, dummy = dc.GetFullMultiLineTextExtent(item.GetText()) - text_h+=2 + font = self._normalFont # Default font. + dc.SetFont(font) - # restore normal font - dc.SetFont(self._normalFont) + text_w, text_h = item.GetExtents(dc) + text_h += 2 + # Calculate size of item image, if any. image_w, image_h = 0, 0 image = item.GetCurrentImage() if image != _NO_IMAGE: + image_w, image_h = self._imgWidth, self._imgHeight + image_w += 4 - if self._imageListNormal: - - image_w, image_h = self._imageListNormal.GetSize(image) - image_w += 4 - - total_h = ((image_h > text_h) and [image_h] or [text_h])[0] + total_h = image_h if image_h > text_h else text_h + # Calculate size of Check Image (if any) checkimage = item.GetCurrentCheckedImage() if checkimage is not None: wcheck, hcheck = self._imageListCheck.GetSize(checkimage) @@ -8446,28 +9228,19 @@ def CalculateSize(self, item, dc, level=-1, align=0): if total_h < 30: total_h += 2 # at least 2 pixels else: - total_h += total_h//10 # otherwise 10% extra spacing + total_h += total_h // 10 # otherwise 10% extra spacing if total_h > self._lineHeight: - self._lineHeight = total_h + self._lineHeight = int(total_h) wnd = item.GetWindow() if not wnd: - totalWidth = image_w+text_w+wcheck+2 + totalWidth = image_w + text_w + wcheck + 2 totalHeight = total_h else: - totalWidth = item.GetWindowSize()[0]+image_w+text_w+wcheck+2 + totalWidth = item.GetWindowSize()[0] + image_w + text_w + wcheck + 2 totalHeight = max(total_h, item.GetWindowSize()[1]) - if level >= 0 and wnd: - if align == 0: - if level in self.absoluteWindows: - self.absoluteWindows[level] = max(self.absoluteWindows[level], image_w+text_w+wcheck+2) - else: - self.absoluteWindows[level] = image_w+text_w+wcheck+2 - elif align == 1: - self.absoluteWindows[level] = max(self.absoluteWindows[level], image_w+text_w+wcheck+2) - if item.IsSeparator(): totalWidth = self.GetClientSize()[0] totalHeight = total_h @@ -8476,7 +9249,7 @@ def CalculateSize(self, item, dc, level=-1, align=0): item.SetHeight(totalHeight) - def CalculateLevel(self, item, dc, level, y, align=0): + def CalculateLevel(self, item, dc, level, x_colstart, y, align=0): """ Calculates the level of an item inside the tree hierarchy. @@ -8494,65 +9267,64 @@ def CalculateLevel(self, item, dc, level, y, align=0): 2 Windows (in items with windows) are aligned at the rightmost edge of :class:`CustomTreeCtrl`. =============== ========================================= - :return: The new `y` vertical position inside the :class:`ScrolledWindow`. + :return: A 2-tuple of (x, y) where `x` is the maximum width of the tree + thus far and `y` is the new vertical position inside the :class:`ScrolledWindow`. """ - x = level*self._indent - - if not self.HasAGWFlag(TR_HIDE_ROOT): + x = x_colstart + level * self._indent - x += self._indent + # Calculate the width and height of this item only if required. + if item.IsDirty(): + self.CalculateSize(item, dc, level, align) - elif level == 0: - - # a hidden root is not evaluated, but its - # children are always calculated - children = item.GetChildren() - count = len(children) - level = level + 1 - for n in range(count): - y = self.CalculateLevel(children[n], dc, level, y, align) # recurse - - return y - - self.CalculateSize(item, dc, level, align) - - # set its position - item.SetX(x+self._spacing) + # Set its position + item.SetX(x) item.SetY(y) + # hidden items don't get a height (height=0). if item.IsHidden(): return y - height = self.GetLineHeight(item) + item_height = self.GetLineHeight(item) + item_width = item.GetWidth() wnd = item.GetWindow() if wnd: - # move its window, if necessary. + # If necessary move window (vertically only) to correct row. xa, ya = self.CalcScrolledPosition((0, y)) wndWidth, wndHeight = item.GetWindowSize() - if height > wndHeight: - ya += (height - wndHeight) // 2 + if item_height > wndHeight: + ya += (item_height - wndHeight) // 2 wndx, wndy = wnd.GetPosition() if wndy != ya: + # Move window vertically. PaintItem does final x/y positioning. wnd.Move(wndx, ya, flags=wx.SIZE_ALLOW_MINUS_ONE) - - y += height + + if level >= 0 and align == 1: + # Record maximum item width for this level to align windows. + width = item_width - wndWidth + if width > self.absoluteWindows.get(level, 0): + self.absoluteWindows[level] = width + + # Advance Y to next item and update tree width. + y += item_height + if not item.IsSeparator(): + self._width = max(self._width, x + item_width) if not item.IsExpanded(): # we don't need to calculate collapsed branches return y - children = item.GetChildren() - count = len(children) - level = level + 1 - for n in range(count): - y = self.CalculateLevel(children[n], dc, level, y, align) # recurse - + # Recurse + for child in item.GetChildren(): + y = self.CalculateLevel(child, dc, level + 1, x_colstart, y, align) return y def CalculatePositions(self): - """ Calculates all the positions of the visible items. """ + """Calculates the positions of all items in the tree. + + Used internally. Called to clear the self._dirty flag. + """ if not self._anchor: return @@ -8566,15 +9338,56 @@ def CalculatePositions(self): dc = wx.ClientDC(self) self.PrepareDC(dc) + # Save old tree dimensions. Reset width as CalculateLevel updates it. + old_width, old_height = self._width, self._height + self._width = 0 + dc.SetFont(self._normalFont) dc.SetPen(self._dottedPen) + + # pre-calculate image size + if self._imageListNormal: + self._imgWidth, self._imgHeight = self._imageListNormal.GetSize(0) + else: + self._imgWidth = self._imgHeight = 0 + self._imgWidth2 = self._imgWidth // 2 + self._imgHeight2 = self._imgHeight // 2 + + # Pre-calculate the left image list size. + if self._imageListLeft: + self._leftWidth, self._leftHeight = self._imageListLeft.GetSize(0) + else: + self._leftWidth = self._leftHeight = 0 + + # Chintzy speedup for GetLineHeight() because it's called so often. + if self.GetAGWWindowStyleFlag() & TR_HAS_VARIABLE_ROW_HEIGHT: + self.GetLineHeight = lambda item: item.GetHeight() + else: + self.GetLineHeight = lambda item: self._lineHeight + + # Calculate the tree recurisvely starting at root item (anchor). + align = 1 if self.HasAGWFlag(TR_ALIGN_WINDOWS) else 0 + x_colstart = self._leftWidth + self._spacing y = 2 - y = self.CalculateLevel(self._anchor, dc, 0, y) # start recursion - if self.HasAGWFlag(TR_ALIGN_WINDOWS) or self.HasAGWFlag(TR_ALIGN_WINDOWS_RIGHT): - align = (self.HasAGWFlag(TR_ALIGN_WINDOWS) and [1] or [2])[0] - y = 2 - y = self.CalculateLevel(self._anchor, dc, 0, y, align) # start recursion + if not self.HasAGWFlag(TR_HIDE_ROOT): + # Calculate tree from root. + x_colstart += self._indent + y = self.CalculateLevel(self._anchor, dc, 0, x_colstart, y, align) + else: + # A hidden root is not evaluated, but its children are. + for child in self._anchor.GetChildren(): + y = self.CalculateLevel(child, dc, 1, x_colstart, y, align) + + self._height = y + + # If the size of the tree has changed, update our scrollbars. + if self._width != old_width or self._height != old_height: + self.AdjustMyScrollbars(tree_size=(self._width, self._height)) + # Clear tree dirty flag. + self._dirty = False + # Refresh client window. + self.Refresh() def RefreshSubtree(self, item): @@ -8597,7 +9410,8 @@ def RefreshSubtree(self, item): rect.height = client.y self.Refresh(True, rect) - self.AdjustMyScrollbars() + # TODO: Why is this necessary? Nothing changed in size. + #self.AdjustMyScrollbars() def RefreshLine(self, item): @@ -8606,17 +9420,24 @@ def RefreshLine(self, item): :param `item`: an instance of :class:`GenericTreeItem`. """ - + ## Bypass if tree dirty (recalculate necessary). However we + ## still want to refresh if frozen (so tree knows what to + ## repaint when it is thawed). if self._dirty: return - if self._freezeCount: - return - rect = wx.Rect() - x, rect.y = self.CalcScrolledPosition(0, item.GetY()) - rect.width = self.GetClientSize().x - rect.height = self.GetLineHeight(item) + width, height = self.GetClientSize() + x, y = self.CalcScrolledPosition(0, item.GetY()) + item_height = self.GetLineHeight(item) + # Do nothing if not currently visible + if y + item_height < 0 or y > height: + return + # Do nothing if part of collapsed branch. + if not self.IsItemShown(item): + return + # Refresh item rectangle. + rect = wx.Rect(0, y, width, item_height) self.Refresh(True, rect) @@ -8626,8 +9447,7 @@ def RefreshSelected(self): if self._freezeCount: return - # TODO: this is awfully inefficient, we should keep the list of all - # selected items internally, should be much faster + # This is now efficient as it only refreshes visible items. if self._anchor: self.RefreshSelectedUnder(self._anchor) @@ -8639,15 +9459,25 @@ def RefreshSelectedUnder(self, item): :param `item`: an instance of :class:`GenericTreeItem`. """ - if self._freezeCount: - return - if item.IsSelected(): self.RefreshLine(item) children = item.GetChildren() - for child in children: - self.RefreshSelectedUnder(child) + if item.IsExpanded() and children: + # Calculate start and end of client area in logical Y coordinates. + width, height = self.GetClientSize() + start_y = self.CalcUnscrolledPosition(0, 0)[1] + last_y = self.CalcUnscrolledPosition(0, height)[1] + + # Binary search for first visible child. + n = BisectChildren(children, start_y) + + # Now refresh only visible children. + while n < len(children): + self.RefreshSelectedUnder(children[n]) + if children[n].GetY() > last_y: + break # Early exit + n = n + 1 def RefreshItemWithWindows(self, item=None): @@ -8663,26 +9493,16 @@ def RefreshItemWithWindows(self, item=None): if self._freezeCount: return - if item is None: - if self._anchor: - self.RefreshItemWithWindows(self._anchor) - return - - wnd = item.GetWindow() - if wnd and wnd.IsShown(): + for item in self._itemWithWindow: self.RefreshLine(item) - children = item.GetChildren() - for child in children: - self.RefreshItemWithWindows(child) - def Freeze(self): """ Freeze :class:`CustomTreeCtrl`. Freezes the window or, in other words, prevents any updates from taking place - on screen, the window is not redrawn at all. :meth:`~Thaw` must be called to re-enable + on screen, the window is not redrawn at all. :meth:`~Thaw` must be called to reenable window redrawing. Calls to these two functions may be nested. :note: This method is useful for visual appearance optimization (for example, @@ -8695,6 +9515,8 @@ def Freeze(self): ## Freeze() also freezes all child item windows and for ## some reason this can cause them to glitch out. self._freezeCount = self._freezeCount + 1 + if not self._freezeDC: + self._freezeDC = wx.ClientDC(self) def Thaw(self): @@ -8713,7 +9535,11 @@ def Thaw(self): self._freezeCount = self._freezeCount - 1 if not self._freezeCount: - self.Refresh() + # Tree is now fully unfrozen. + self._freezeDC = None + if self._dirty is True: + self.CalculatePositions() + # ---------------------------------------------------------------------------- @@ -8821,7 +9647,7 @@ def GetMaxWidth(self, respect_expansion_state=True): rect = self.GetBoundingRect(root, True) # It looks like the space between the "+" and the node - # rect occupies 4 pixels approximately + # rect occupies 4 pixels approximatively maxwidth = rect.x + rect.width + 4 lastheight = rect.y + rect.height @@ -8861,7 +9687,7 @@ def RecurseOnChildren(self, item, maxwidth, respect_expansion_state): rect = self.GetBoundingRect(child, True) # It looks like the space between the "+" and the node - # rect occupies 4 pixels approximately + # rect occupies 4 pixels approximatively maxwidth = max(maxwidth, rect.x + rect.width + 4) lastheight = rect.y + rect.height @@ -8910,47 +9736,191 @@ def GetClassDefaultAttributes(self): if __name__ == '__main__': import wx - + import wx.lib.agw.customtreectrl as CTC + class MyFrame(wx.Frame): def __init__(self, parent): wx.Frame.__init__(self, parent, -1, "CustomTreeCtrl Demo") + self.spin_parent = None + self.spin_count = 0 + self.hidden = False + self.item_count = 0 - # Create a CustomTreeCtrl instance - custom_tree = CustomTreeCtrl(self, agwStyle=wx.TR_DEFAULT_STYLE) - custom_tree.SetBackgroundColour(wx.WHITE) + panel = wx.Panel(self) + sizer = wx.BoxSizer(orient=wx.VERTICAL) + # Create a CustomTreeCtrl instance + self.tree = CTC.CustomTreeCtrl(panel, agwStyle=wx.TR_DEFAULT_STYLE | + CTC.TR_HAS_VARIABLE_ROW_HEIGHT | + #CTC.TR_ALIGN_WINDOWS_RIGHT | + CTC.TR_ALIGN_WINDOWS | + wx.TR_MULTIPLE | + #wx.TR_FULL_ROW_HIGHLIGHT | + #CTC.TR_FULL_ROW_HIGHLIGHT | + 0) + start = time.time() # Add a root node to it - root = custom_tree.AddRoot("The Root Item") + root = self.tree.AddRoot("The Root Item") # Create an image list to add icons next to an item il = wx.ImageList(16, 16) fldridx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER, wx.ART_OTHER, (16, 16))) fldropenidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_OTHER, (16, 16))) fileidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, (16, 16))) - - custom_tree.SetImageList(il) - - custom_tree.SetItemImage(root, fldridx, wx.TreeItemIcon_Normal) - custom_tree.SetItemImage(root, fldropenidx, wx.TreeItemIcon_Expanded) - - for x in range(15): - child = custom_tree.AppendItem(root, "Item %d" % x) - custom_tree.SetItemImage(child, fldridx, wx.TreeItemIcon_Normal) - custom_tree.SetItemImage(child, fldropenidx, wx.TreeItemIcon_Expanded) - - for y in range(5): - last = custom_tree.AppendItem(child, "item %d-%s" % (x, chr(ord("a")+y))) - custom_tree.SetItemImage(last, fldridx, wx.TreeItemIcon_Normal) - custom_tree.SetItemImage(last, fldropenidx, wx.TreeItemIcon_Expanded) - + delidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_DELETE, wx.ART_OTHER, (16, 16))) + copyidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_COPY, wx.ART_OTHER, (16, 16))) + il.Add(wx.ArtProvider.GetBitmap(wx.ART_CUT, wx.ART_OTHER, (16, 16))) + il.Add(wx.ArtProvider.GetBitmap(wx.ART_PASTE, wx.ART_OTHER, (16, 16))) + il.Add(wx.ArtProvider.GetBitmap(wx.ART_UNDO, wx.ART_OTHER, (16, 16))) + self.tree.SetImageList(il) + + self.tree.SetItemImage(root, fldridx, wx.TreeItemIcon_Normal) + self.tree.SetItemImage(root, fldropenidx, wx.TreeItemIcon_Expanded) + self.tree.Freeze() + for x in range(5): + child = self.tree.AppendItem(root, "Item %d" % x) + self.tree.SetItemImage(child, fldridx, wx.TreeItemIcon_Normal) + self.tree.SetItemImage(child, fldropenidx, wx.TreeItemIcon_Expanded) + self.tree.SetItemImage(child, delidx, wx.TreeItemIcon_Selected) + self.tree.SetItemImage(child, copyidx, wx.TreeItemIcon_SelectedExpanded) + if x == 1: + self.spin_parent = child + for y in range(5000): + last = self.tree.AppendItem(child, "item %d-%d" % (x, y)) + self.tree.SetItemImage(last, fldridx, wx.TreeItemIcon_Normal) + self.tree.SetItemImage(last, fldropenidx, wx.TreeItemIcon_Expanded) + if x < 2 and y == 0: + button = wx.Button(self.tree, label="Show Windows") + self.tree.SetItemWindow(last, button) + button.Bind(wx.EVT_BUTTON, lambda evt, item=child: + self.OnShowButton(evt, item)) + if y == 5: + self.tree.SetItemText(last, self.tree.GetItemText(last) + " Surprise long text!") for z in range(5): - item = custom_tree.AppendItem(last, "item %d-%s-%d" % (x, chr(ord("a")+y), z)) - custom_tree.SetItemImage(item, fileidx, wx.TreeItemIcon_Normal) - - custom_tree.Expand(root) - + item = self.tree.AppendItem(last, "Subordinate-item %d-%d-%s" % + (x, y, chr(ord("a") + z)), 1) + self.tree.SetItemImage(item, fileidx, wx.TreeItemIcon_Normal) + for x in range(2): + self.tree.AppendSeparator(root) + name = "Disable" if x == 1 else "Hide" + child = self.tree.AppendItem(root, "Parent %s %d" % (name, x)) + self.tree.SetItemImage(child, fldridx, wx.TreeItemIcon_Normal) + self.tree.SetItemImage(child, fldropenidx, wx.TreeItemIcon_Expanded) + for y in range(10000): + last = self.tree.AppendItem(child, "%s item %d-%d" % (name, x, y)) + self.tree.SetItemImage(last, fldridx, wx.TreeItemIcon_Normal) + self.tree.SetItemImage(last, fldropenidx, wx.TreeItemIcon_Expanded) + if y > 4 and x == 1: + self.tree.EnableItem(last, True if y & 0x20 else False) + elif y > 4 and x != 1: + self.tree.HideItem(last) + + elapsed = abs(time.time() - start) * 1000 + print("Tree populate took %.3fms" % elapsed) + + start = time.time() + self.tree.Thaw() + elapsed = abs(time.time() - start) * 1000 + print("Thaw took %.3fms" % elapsed) + start = time.time() + self.tree.Expand(root) + elapsed = abs(time.time() - start) * 1000 + print("Expand root took %.3fms" % elapsed) + + sizer.Add(self.tree, proportion=1, flag=wx.EXPAND) + + # Create tree test button row. + row = wx.BoxSizer(orient=wx.HORIZONTAL) + spin_button = wx.ToggleButton(panel, label="Spin") + spin_button.Bind(wx.EVT_TOGGLEBUTTON, self.OnSpinButton) + row.Add(spin_button, flag=wx.RIGHT, border=20) + unhide_button = wx.Button(panel, label="Unhide") + unhide_button.Bind(wx.EVT_BUTTON, self.OnUnhide) + row.Add(unhide_button, flag=wx.RIGHT, border=20) + getsel_button = wx.Button(panel, label="GetSelections") + getsel_button.Bind(wx.EVT_BUTTON, self.OnGetSelections) + row.Add(getsel_button, flag=wx.RIGHT, border=20) + sizer.Add(row, flag=wx.ALIGN_CENTER | wx.ALL, border=5) + + panel.SetSizer(sizer) + self.timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.OnTimer, source=self.timer) + self.tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnActivated) + self.Bind(wx.EVT_CLOSE, self.OnClose) + + def OnSpinButton(self, event): + if self.timer.IsRunning(): + self.timer.Stop() + if event.GetEventObject().GetValue(): + self.timer.Start(milliseconds=300) + + def OnShowButton(self, event, parent): + # Add windows to first 100 items in this branch. + self.tree.Freeze() + self.Freeze() + size, client = (16, 16), wx.ART_OTHER + bitmap = wx.ArtProvider.GetBitmap(wx.ART_COPY, client, size) + count = len(self.tree._itemWithWindow) + start = time.time() + for index, child in enumerate(parent.GetChildren()): + if not self.tree.GetItemWindow(child): + widget = wx.StaticBitmap(self.tree, bitmap=bitmap) + self.tree.SetItemWindow(child, widget) + if index > 1000: + break + self.Thaw() + self.tree.Thaw() + elapsed = abs(time.time() - start) * 1000 + print("Added %d windows in %.3fms" % (len(self.tree._itemWithWindow) - count, elapsed)) + + def OnGetSelections(self, event): + start = time.time() + selections = self.tree.GetSelections() + elapsed = abs(time.time() - start) * 1000 + print("Got %d selections in %.3fms" % (len(selections), elapsed)) + + def OnUnhide(self, event): + # Recursively hide/unhide items with "Hidden" in their text. + start = time.time() + self.item_count = 0 + self.DoHideUnhide(self.tree.GetRootItem(), self.hidden) + elapsed = abs(time.time() - start) * 1000 + print("Changed hide state in %.3fms (%d items scanned)" % + (elapsed, self.item_count)) + self.hidden = not self.hidden + event.GetEventObject().SetLabel("Hide" if self.hidden else "Unhide") + + def DoHideUnhide(self, item, hide): + self.item_count += 1 + text = item.GetText() + if len(text) > 13 and text.startswith('Hide'): + self.tree.HideItem(item, hide) + for child in item.GetChildren(): + self.DoHideUnhide(child, hide) + + def OnActivated(self, event): + item = event.GetItem() + print("Item %r x=%r, y=%r, width=%r, height=%r" % + (item._text, item.GetX(), item.GetY(), item.GetWidth(), item.GetHeight())) + + def OnTimer(self, event): + self.spin_count = (self.spin_count + 1) % 8 + if self.spin_parent: + start = time.time() + self.tree.Freeze() + children = self.spin_parent.GetChildren() + for child in children: + self.tree.SetItemImage(child, self.spin_count, which=0) + self.tree.Thaw() + elapsed = abs(time.time() - start) * 1000 + print("Spun %d items in %.3fms" % (len(children), elapsed)) + + def OnClose(self, event): + if self.timer.IsRunning() is True: + self.timer.Stop() + event.Skip() # our normal wxApp-derived class, as usual From 908599c506cac4fc2d29ffac8575401ab8839e14 Mon Sep 17 00:00:00 2001 From: cbeytas Date: Sun, 23 Jan 2022 20:23:10 +0000 Subject: [PATCH 2/4] Major speedups and bug fixes to HyperTreeList Fixes #2083, #2081, #1981, #1938, #1901, #1898, #1880, #1293 Runs 10-100x faster while using slightly more memory. Adds a new style `TR_LIVE_UPDATE` to show column resizing live rather than drawing a preview line. Bump version from 1.4 to 1.5 --- wx/lib/agw/hypertreelist.py | 1462 ++++++++++++++++++++++------------- 1 file changed, 920 insertions(+), 542 deletions(-) diff --git a/wx/lib/agw/hypertreelist.py b/wx/lib/agw/hypertreelist.py index 60ec7435a..a1cb62c89 100644 --- a/wx/lib/agw/hypertreelist.py +++ b/wx/lib/agw/hypertreelist.py @@ -101,6 +101,7 @@ class supports: * Ellipsization of long items when the horizontal space is low, via the ``TR_ELLIPSIZE_LONG_ITEMS`` style (`New in version 0.9.3`). * Hiding items +* Change background colours for each column individually. And a lot more. Check the demo for an almost complete review of the functionalities. @@ -128,6 +129,13 @@ class supports: - ``TR_ELLIPSIZE_LONG_ITEMS``: ellipsizes long items when the horizontal space for :class:`HyperTreeList` is low (`New in version 0.9.3`). +- ``TR_VIRTUAL``: support is mostly experimental. A TreeCtrl cannot be + made virtual as easily as a ListCtrl. In a ListCtrl the visible items + can be determined directly from the scrollbars because all rows are + the same height and are always visible. In a TreeCtrl the topology of + the tree can be very complex. Each item can be expanded, collapsed, + hidden, and even different heights if the ``wx.TR_HAS_VARIABLE_ROW_HEIGHT`` + style is used. Please note that most TreeCtrl-like APIs are available in this class, although they may not be visible to IDEs or other tools as they are automatically @@ -228,7 +236,7 @@ def __init__(self): ``wx.TR_EDIT_LABELS`` 0x200 Use this style if you wish the user to be able to edit labels in the tree control. ``wx.TR_ROW_LINES`` 0x400 Use this style to draw a contrasting border between displayed rows. ``wx.TR_HIDE_ROOT`` 0x800 Use this style to suppress the display of the root node, effectively causing the first-level nodes to appear as a series of root nodes. -``wx.TR_FULL_ROW_HIGHLIGHT`` 0x2000 Use this style to have the background colour and the selection highlight extend over the entire horizontal row of the tree control window. +``wx.TR_FULL_ROW_HIGHLIGHT`` 0x2000 Use this style to have the background colour and the selection highlight extend over the entire horizontal row of the tree control window. When TR_FILL_WHOLE_COLUMN_BACKGROUND is also set only the selection extends to the whole row, not the background color. **Styles from hypertreelist:** ``TR_EXTENDED`` 0x40 Use this style to allow disjoint items to be selected. (Only partially implemented; may not work in all cases). ``TR_COLUMN_LINES`` 0x1000 Use this style to draw a contrasting border between displayed columns. @@ -239,6 +247,8 @@ def __init__(self): ``TR_NO_HEADER`` 0x40000 Use this style to hide the columns header. ``TR_ELLIPSIZE_LONG_ITEMS`` 0x80000 Flag used to ellipsize long items when the horizontal space for :class:`HyperTreeList` columns is low. ``TR_VIRTUAL`` 0x100000 :class:`HyperTreeList` will have virtual behaviour. +``TR_FILL_WHOLE_COLUMN_BACKGROUND`` 0x200000 Use this style to fill the whole background of item columns. Modifies behavior of :meth:`SetItemBackgroundColour() `. +``TR_LIVE_UPDATE`` 0x400000 Don't draw ``wx.INVERT`` line but resize columns immediately. ================================= =========== ================================================== See :mod:`~wx.lib.agw.customtreectrl` for more information on styles. @@ -291,7 +301,7 @@ def __init__(self): Latest Revision: Andrea Gavana @ 30 Jul 2014, 21.00 GMT -Version 1.4 +Version 1.5 """ @@ -299,14 +309,15 @@ def __init__(self): from wx.lib.agw.customtreectrl import CustomTreeCtrl from wx.lib.agw.customtreectrl import DragImage, TreeEvent, GenericTreeItem, ChopText +from wx.lib.agw.customtreectrl import EnsureText, BisectChildren from wx.lib.agw.customtreectrl import TreeEditTimer as TreeListEditTimer from wx.lib.agw.customtreectrl import EVT_TREE_ITEM_CHECKING, EVT_TREE_ITEM_CHECKED, EVT_TREE_ITEM_HYPERLINK # Python 2/3 compatibility helper import six - +import time # Version Info -__version__ = "1.4" +__version__ = "1.5" # -------------------------------------------------------------------------- # Constants @@ -328,7 +339,7 @@ def __init__(self): _DRAG_TIMER_TICKS = 250 # minimum drag wait time in ms _FIND_TIMER_TICKS = 500 # minimum find wait time in ms -_EDIT_TIMER_TICKS = 250 # minimum edit wait time in ms +_EDIT_TIMER_TICKS = 250 # minimum edit wait time in ms # -------------------------------------------------------------------------- @@ -400,6 +411,17 @@ def __init__(self): """ Use this style to fill the whole background of the item columns. """ # -------------------------------------------------------------------------- +# -------------------------------------------------------------------------- +# Additional HyperTreeList style to do live updates while a column is being +# resized instead of drawing a preview line. The GTK3/MacOS platforms do not +# support the wx.INVERT logical function used to draw the line. Windows 10 +# supports it but it is buggy, especially with more than one monitor and +# when the horizontal scrollbar is not at zero. +# Only Windows XP/7 and GTK2 draw the preview line OK. +TR_LIVE_UPDATE = 0x400000 +""" Use this style to do live updates while resizing a column. """ +# -------------------------------------------------------------------------- + # -------------------------------------------------------------------------- # Additional HyperTreeList style autosize the columns based on the widest # width between column header and cells content @@ -696,6 +718,7 @@ def __init__(self, parent, id=wx.ID_ANY, owner=None, pos=wx.DefaultPosition, self._currentCursor = wx.Cursor(wx.CURSOR_DEFAULT) self._resizeCursor = wx.Cursor(wx.CURSOR_SIZEWE) self._isDragging = False + self._dragStart = 0 self._dirty = False self._total_col_width = 0 self._hotTrackCol = -1 @@ -918,7 +941,7 @@ def OnPaint(self, event): break if not self.IsColumnShown(i): - continue # do next column if not shown + continue # do next column if not shown params = wx.HeaderButtonParams() @@ -948,16 +971,16 @@ def OnPaint(self, event): params.m_labelBitmap = imageList.GetBitmap(image) if self._headerCustomRenderer is not None: - self._headerCustomRenderer.DrawHeaderButton(dc, rect, flags, params) + self._headerCustomRenderer.DrawHeaderButton(dc, rect, flags, params) else: wx.RendererNative.Get().DrawHeaderButton(self, dc, rect, flags, sortIcon, params) # Fill up any unused space to the right of the columns if x < w: - rect = wx.Rect(x, 0, w-x, h) + rect = wx.Rect(x, 0, w - x, h) if self._headerCustomRenderer is not None: - self._headerCustomRenderer.DrawHeaderButton(dc, rect) + self._headerCustomRenderer.DrawHeaderButton(dc, rect) else: wx.RendererNative.Get().DrawHeaderButton(self, dc, rect) @@ -967,9 +990,11 @@ def DrawCurrent(self): x1, y1 = self._currentX, 0 x1, y1 = self.ClientToScreen((x1, y1)) - x2 = self._currentX-1 + # GTK2 needs x2 offset by 1 to draw 2-pixel wide line. Windows does not. + x2 = self._currentX - 1 if wx.Platform == "__WXMSW__": - x2 += 1 # but why ???? + # Undo 1 pixel slant added in previous line under Windows. + x2 += 1 y2 = 0 dummy, y2 = self._owner.GetClientSize() @@ -981,7 +1006,7 @@ def DrawCurrent(self): dc.SetBrush(wx.TRANSPARENT_BRUSH) self.AdjustDC(dc) - dc.DrawLine (x1, y1, x2, y2) + dc.DrawLine(x1, y1, x2, y2) dc.SetLogicalFunction(wx.COPY) @@ -1017,7 +1042,7 @@ def XToCol(self, x): column = self.GetColumn(col) if x < (colLeft + column.GetWidth()): - return col + return col colLeft += column.GetWidth() @@ -1084,35 +1109,51 @@ def OnMouse(self, event): if self._isDragging: + # User is dragging the column header separator. Send event. self.SendListEvent(wx.wxEVT_COMMAND_LIST_COL_DRAGGING, event.GetPosition()) - # we don't draw the line beyond our window, but we allow dragging it - # there - w, dummy = self.GetClientSize() - w, dummy = self._owner.CalcUnscrolledPosition(w, 0) - w -= 6 - - # erase the line if it was drawn - if self._currentX < w: - self.DrawCurrent() + # Calculate time elapsed for this drag, and new/old column sizes. + elapsed_ms = abs(time.time() - self._dragStart) * 1000 + new_width = self._currentX - self._minX + old_width = self.GetColumnWidth(self._column) + + # Live updates or old preview line? + if self._owner.HasAGWFlag(TR_LIVE_UPDATE): + # Avoid making change on first click of double-click. + if elapsed_ms > 333 or abs(new_width - old_width) > 3: + self.SetColumnWidth(self._column, new_width) + self.Refresh() + self.Update() + else: + # Don't draw line beyond our window but allow dragging it there. + w, dummy = self.GetClientSize() + w, dummy = self._owner.CalcUnscrolledPosition(w, 0) + w -= 6 + # Erase the preview line if it was drawn. + if self._currentX < w: + self.DrawCurrent() if event.ButtonUp(): self._isDragging = False if self.HasCapture(): self.ReleaseMouse() self._dirty = True - self.SetColumnWidth(self._column, self._currentX - self._minX) - self.Refresh() - self.SendListEvent(wx.wxEVT_COMMAND_LIST_COL_END_DRAG, event.GetPosition()) + # Avoid making change on first click of double-click. + if elapsed_ms > 333 or abs(new_width - old_width) > 3: + self.SetColumnWidth(self._column, new_width) + self.Refresh() + self.SendListEvent(wx.wxEVT_COMMAND_LIST_COL_END_DRAG, + event.GetPosition()) else: + # Update the new column preview position. self._currentX = max(self._minX + 7, x) + # Draw the preview line in the new location. + if not self._owner.HasAGWFlag(TR_LIVE_UPDATE): + if self._currentX < w: + self.DrawCurrent() - # draw in the new location - if self._currentX < w: - self.DrawCurrent() - - else: # not dragging - + else: + # Not dragging (column resizing) self._minX = 0 hit_border = False @@ -1125,11 +1166,11 @@ def OnMouse(self, event): for column in range(countCol): if not self.IsColumnShown(column): - continue # do next if not shown + continue # do next if not shown xpos += self.GetColumnWidth(column) self._column = column - if abs (x-xpos) < 3 and y < 22: + if abs(x - xpos) < 3 and y < 22: # near the column border hit_border = True break @@ -1143,17 +1184,20 @@ def OnMouse(self, event): if event.LeftDown() or event.RightUp(): if hit_border and event.LeftDown(): self._isDragging = True + self._dragStart = time.time() if not self.HasCapture(): self.CaptureMouse() self._currentX = x - self.DrawCurrent() + if not self._owner.HasAGWFlag(TR_LIVE_UPDATE): + self.DrawCurrent() self.SendListEvent(wx.wxEVT_COMMAND_LIST_COL_BEGIN_DRAG, event.GetPosition()) - else: # click on a column + else: # click on a column evt = (event.LeftDown() and [wx.wxEVT_COMMAND_LIST_COL_CLICK] or [wx.wxEVT_COMMAND_LIST_COL_RIGHT_CLICK])[0] self.SendListEvent(evt, event.GetPosition()) elif event.LeftDClick() and hit_border: - self.SetColumnWidth(self._column, self._owner.GetBestColumnWidth(self._column)) + best_width = self._owner.GetBestColumnWidth(self._column) + self.SetColumnWidth(self._column, best_width) self.Refresh() elif event.Moving(): @@ -1252,7 +1296,7 @@ def SetColumnWidth(self, column, width): self._total_col_width -= self._columns[column].GetWidth() self._columns[column].SetWidth(width) self._total_col_width += width - self._owner.AdjustMyScrollbars() + #self._owner.AdjustMyScrollbars() # Setting dirty does this now. self._owner._dirty = True @@ -1420,6 +1464,7 @@ def __init__(self, mainWin, parent, text="", ct_type=0, wnd=None, image=-1, selI self._wnd = [None] # are we holding a window? self._bgColour = [None] + self._extents = None # Cached text extents if wnd: self.SetWindow(wnd) @@ -1475,7 +1520,7 @@ def HitTest(self, point, theCtrl, flags, column, level): return None, flags, wx.NOT_FOUND # for a hidden root node, don't evaluate it, but do evaluate children - if not theCtrl.HasAGWFlag(wx.TR_HIDE_ROOT) or level > 0: + if level > 0 or not theCtrl.HasAGWFlag(wx.TR_HIDE_ROOT): # reset any previous hit infos flags = 0 @@ -1494,7 +1539,7 @@ def HitTest(self, point, theCtrl, flags, column, level): maincol = theCtrl.GetMainColumn() # check for above/below middle - y_mid = self._y + h//2 + y_mid = self._y + h // 2 if point.y < y_mid: flags |= wx.TREE_HITTEST_ONITEMUPPERPART else: @@ -1517,7 +1562,7 @@ def HitTest(self, point, theCtrl, flags, column, level): if self.GetCurrentImage() != _NO_IMAGE: imageWidth = theCtrl._imgWidth numberOfMargins += 1 - chkX = self._text_x - imageWidth - numberOfMargins*_MARGIN - theCtrl._checkWidth + chkX = self._text_x - imageWidth - numberOfMargins * _MARGIN - theCtrl._checkWidth chkY = y_mid - theCtrl._checkHeight2 if ((point.x >= chkX) and (point.x <= (chkX + theCtrl._checkWidth)) and (point.y >= chkY) and (point.y <= (chkY + theCtrl._checkHeight))): @@ -1543,7 +1588,7 @@ def HitTest(self, point, theCtrl, flags, column, level): # check for indent hit after button and image hit if point.x < self._x: flags |= wx.TREE_HITTEST_ONITEMINDENT - column = -1 # considered not belonging to main column + column = -1 # considered not belonging to main column return self, flags, column # check for right of label @@ -1552,7 +1597,7 @@ def HitTest(self, point, theCtrl, flags, column, level): end += header_win.GetColumnWidth(i) if ((point.x > (self._text_x + self._width)) and (point.x <= end)): flags |= wx.TREE_HITTEST_ONITEMRIGHT - column = -1 # considered not belonging to main column + column = -1 # considered not belonging to main column return self, flags, column # else check for each column except main @@ -1561,7 +1606,7 @@ def HitTest(self, point, theCtrl, flags, column, level): if not header_win.IsColumnShown(j): continue w = header_win.GetColumnWidth(j) - if ((j != maincol) and (point.x >= x and point.x < x+w)): + if ((j != maincol) and (point.x >= x and point.x < x + w)): flags |= TREE_HITTEST_ONITEMCOLUMN column = j return self, flags, column @@ -1571,20 +1616,103 @@ def HitTest(self, point, theCtrl, flags, column, level): # no special flag or column found return self, flags, column - # if children not expanded, return no item + # if children are expanded, fall through to evaluate them if not self.IsExpanded(): + # Item is not expanded (or hidden). Return no item found. return None, flags, wx.NOT_FOUND - # in any case evaluate children - for child in self._children: - hit, flags, column = child.HitTest(point, theCtrl, flags, column, level+1) + # Binary search for last child that is before the point's Y. + lo = BisectChildren(self._children, point[1]) + hi = len(self._children) + + # Now hit test only against prospective children. + for index in range(lo, hi): + child = self._children[index] + hit, flags, column = child.HitTest(point, theCtrl, flags, column, level + 1) if hit: return hit, flags, column + if child.GetY() > point[1]: + break # Early exit (we're past the point) # not found return None, flags, wx.NOT_FOUND + def HasExtents(self, column=None): + """ + Returns whether the text extents are calculated for this item. + + :param `column`: The column to check for extents. If it is ``None``, + check that all columns have extents. + + :return: ``True`` if extents are calculated, ``False`` otherwise. + """ + + if not self._extents: + return False + if len(self._extents) < self._owner.GetColumnCount(): + # Only partial extents list is present. + if column is None or column >= len(self._extents): + return False + if column is None: + # All extents must be present. + return True if all(self._extents) else False + # Return specific extent for column number. + return self._extents[column] is not None + + + def GetExtents(self, dc=None, column=None): + """ + Calculate text extents of this item using the given ClientDc. + + :param `dc`: an instance of :class:`wx.DC` to use to calculate + text extent if it has not been cached yet. The proper font + should have been already set in the device context. + + :param `column`: The column to get extents for. If it is ``None``, + return the width of the main column and maximum height of all + the columns (``dc`` is required in this case). + + :return: A 2-tuple of (width, height). If `dc` was not provided + could return `None`. + """ + # Make sure self._extents array is fully setup. + if self._extents is None: + self._extents = [None] * self._owner.GetColumnCount() + elif len(self._extents) < self._owner.GetColumnCount(): + self._extents.extend([None] * (self._owner.GetColumnCount() - len(self._extents))) + # Are we grabbing all extents? + if column is None: + # Scan all extents. + main_width, max_height = (0, 0) + main_column = self._owner.GetMainColumn() + for column, extent in enumerate(self._extents): + if extent is None: + text = self.GetText(column) + if text or column == main_column: + # Always get extents for main column, even if blank. + width, height, hl = dc.GetFullMultiLineTextExtent(text) + else: + # Blank text, no need for extents. + width = height = 0 + self._extents[column] = (width, height) + else: + width, height = extent + if column == main_column: + main_width = width + max_height = height if height > max_height else max_height + # Return main column width and max height amonst all columns. + return main_width, max_height + elif column < len(self._extents): + if self._extents[column] is None and dc is not None: + # Need to calculate + text = self.GetText(column) + width, height, dummy = dc.GetFullMultiLineTextExtent(text) + self._extents[column] = (width, height) + return self._extents[column] + return None + + def GetText(self, column=None): """ Returns the item text label. @@ -1593,7 +1721,7 @@ def GetText(self, column=None): If it is ``None``, the main column index is used. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] + column = self._owner.GetMainColumn() if column is None else column if len(self._text) > 0: if self._owner.IsVirtual(): @@ -1621,9 +1749,12 @@ def GetImage(self, which=wx.TreeItemIcon_Normal, column=None): :param `column`: if not ``None``, an integer specifying the column index. If it is ``None``, the main column index is used. + + :return: An integer index that can be used to retrieve the item + image inside a :class:`wx.ImageList`. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] + column = self._owner.GetMainColumn() if column is None else column if column == self._owner.GetMainColumn(): return self._images[which] @@ -1642,13 +1773,11 @@ def GetCurrentImage(self, column=None): If it is ``None``, the main column index is used. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] - - if column != self._owner.GetMainColumn(): - return self.GetImage(column=column) - - image = GenericTreeItem.GetCurrentImage(self) - return image + if column is None or column == self._owner.GetMainColumn(): + # Main column image depends on item state. Use base class method. + return GenericTreeItem.GetCurrentImage(self) + # Return column image. + return self.GetImage(column=column) def SetText(self, column, text): @@ -1661,14 +1790,16 @@ def SetText(self, column, text): :note: Call :meth:`~TreeListMainWindow.SetItemText` instead to refresh the tree properly. """ + column = self._owner.GetMainColumn() if column is None else column - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] - - if column < len(self._text): - self._text[column] = text - elif column < self._owner.GetColumnCount(): - self._text.extend([""] * (column - len(self._text) + 1)) + if column < self._owner.GetColumnCount(): + if column >= len(self._text): + self._text.extend([""] * (column - len(self._text) + 1)) self._text[column] = text + # Set dirty flag and clear extents for this column. + self._dirty = True + if self._extents and column < len(self._extents): + self._extents[column] = None def SetImage(self, column, image, which): @@ -1678,22 +1809,30 @@ def SetImage(self, column, image, which): :param `column`: if not ``None``, an integer specifying the column index. If it is ``None``, the main column index is used; :param `image`: an index within the normal image list specifying the image to use; - :param `which`: the item state. + :param `which`: The item state for which this image applies. One + of wx.TreeItemIcon_Xxx. Only valid for the main column. :see: :meth:`~TreeListItem.GetImage` for a list of valid item states. :note: Call :meth:`~TreeListMainWindow.SetItemImage` instead to refresh the tree properly. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] - - if column == self._owner.GetMainColumn(): - self._images[which] = image - elif column < len(self._col_images): - self._col_images[column] = image + if column is None or column == self._owner.GetMainColumn(): + # Set tree column image (one of 4 wx.TreeItemIcon.XXX states). + if self._images[which] != image: + # Only set image if different. + self._images[which] = image + # Only go dirty if the current image state was modified. + if which == self.GetCurrentImageState(): + self._dirty = True elif column < self._owner.GetColumnCount(): - self._col_images.extend([_NO_IMAGE] * (column - len(self._col_images) + 1)) - self._col_images[column] = image + # Set column image. + if column >= len(self._col_images): + self._col_images.extend([_NO_IMAGE] * (column - len(self._col_images) + 1)) + if self._col_images[column] != image: + # Only set image if different. + self._col_images[column] = image + self._dirty = True def GetTextX(self): @@ -1724,19 +1863,25 @@ def SetWindow(self, wnd, column=None): :note: Always use :meth:`~TreeListMainWindow.SetItemWindow` instead to update the tree properly. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] + column = self._owner.GetMainColumn() if column is None else column + if column < 0 or column >= self._owner.GetColumnCount(): + # Invalid column number. + return - if type(self._wnd) != type([]): - self._wnd = [self._wnd] + if type(self._wnd) is not list: + # Convert self._wnd to array, putting window in main column. + main_wnd = self._wnd + self._wnd = [None] * self._owner.GetColumnCount() + self._wnd[self._owner.GetMainColumn()] = main_wnd + elif len(self._wnd) < self._owner.GetColumnCount(): + # Extend array to owner's column count size. + self._wnd.extend([None] * (self._owner.GetColumnCount() - len(self._wnd))) - if column < len(self._wnd): - self._wnd[column] = wnd - elif column < self._owner.GetColumnCount(): - self._wnd.extend([None] * (column - len(self._wnd) + 1)) - self._wnd[column] = wnd + self._wnd[column] = wnd + self._dirty = True if self not in self._owner._itemWithWindow: - self._owner._itemWithWindow.append(self) + self._owner._itemWithWindow.add(self) # We have to bind the wx.EVT_SET_FOCUS for the associated window # No other solution to handle the focus changing from an item in @@ -1745,7 +1890,7 @@ def SetWindow(self, wnd, column=None): wnd.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus) ## Hide the window since the position isn't correct yet. It will - ## be shown and positioned when the item is painted. + ## be shown and positioned when the tree is calculated. wnd.Show(False) # The window is enabled only if the item is enabled @@ -1782,7 +1927,7 @@ def GetWindow(self, column=None): If it is ``None``, the main column index is used. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] + column = self._owner.GetMainColumn() if column is None else column if column >= len(self._wnd): return None @@ -1798,7 +1943,7 @@ def DeleteWindow(self, column=None): If it is ``None``, the main column index is used. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] + column = self._owner.GetMainColumn() if column is None else column if column >= len(self._wnd): return @@ -1807,6 +1952,7 @@ def DeleteWindow(self, column=None): if wnd: wnd.Destroy() self._wnd[column] = None + self._dirty = True if not any(self._wnd) and self in self._owner._itemWithWindow: self._owner._itemWithWindow.remove(self) @@ -1819,10 +1965,10 @@ def GetWindowEnabled(self, column=None): If it is ``None``, the main column index is used. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] + column = self._owner.GetMainColumn() if column is None else column if not self._wnd[column]: - raise Exception("\nERROR: This Item Has No Window Associated At Column %s"%column) + raise Exception("\nERROR: This Item Has No Window Associated At Column %s" % column) return self._wnd[column].IsEnabled() @@ -1836,10 +1982,10 @@ def SetWindowEnabled(self, enable=True, column=None): If it is ``None``, the main column index is used. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] + column = self._owner.GetMainColumn() if column is None else column if not self._wnd[column]: - raise Exception("\nERROR: This Item Has No Window Associated At Column %s"%column) + raise Exception("\nERROR: This Item Has No Window Associated At Column %s" % column) self._wnd[column].Enable(enable) @@ -1852,34 +1998,18 @@ def GetWindowSize(self, column=None): If it is ``None``, the main column index is used. """ - column = (column is not None and [column] or [self._owner.GetMainColumn()])[0] + column = self._owner.GetMainColumn() if column is None else column if not self._wnd[column]: - raise Exception("\nERROR: This Item Has No Window Associated At Column %s"%column) + raise Exception("\nERROR: This Item Has No Window Associated At Column %s" % column) return self._wnd[column].GetSize() - def IsExpanded(self): - """ - Returns whether the item is expanded or not. - - :return: ``True`` if the item is expanded, ``False`` if it is collapsed or hidden. - """ - if self.IsHidden(): - return False - return not self._isCollapsed - - - def IsEnabled(self): - """ - Returns whether the item is enabled or not. + def GetWindows(self): + """Returns a list of all associated windows. May be empty list.""" + return [wnd for wnd in self._wnd if wnd] if self._wnd else [] - :return: ``True`` if the item is enabled, ``False`` if it is disabled or hidden. - """ - if self.IsHidden(): - return False - return self._enabled def GetBackgroundColour(self, column=0): """ @@ -1901,7 +2031,7 @@ def SetBackgroundColour(self, colour, column=0): :param integer `column` """ - if type(self._bgColour) != type([]): + if type(self._bgColour) is not list: self._bgColour = [self._bgColour] if column < len(self._bgColour): @@ -1966,13 +2096,13 @@ def __init__(self, parent, id=wx.ID_ANY, item=None, column=None, owner=None, for i in range(column): if not self._owner.GetParent()._header_win.IsColumnShown(i): - continue # do next column if not shown + continue # do next column if not shown col = self._owner.GetParent()._header_win.GetColumn(i) wCol = col.GetWidth() x += wCol - x, y = self._owner.CalcScrolledPosition(x+2, item.GetY()) + x, y = self._owner.CalcScrolledPosition(x + 2, item.GetY()) image_w = image_h = wcheck = hcheck = 0 image = item.GetCurrentImage(column) @@ -1981,7 +2111,7 @@ def __init__(self, parent, id=wx.ID_ANY, item=None, column=None, owner=None, if self._owner._imageListNormal: image_w, image_h = self._owner._imageListNormal.GetSize(image) - image_w += 2*_MARGIN + image_w += 2 * _MARGIN else: @@ -1991,7 +2121,7 @@ def __init__(self, parent, id=wx.ID_ANY, item=None, column=None, owner=None, checkimage = item.GetCurrentCheckedImage() if checkimage is not None: wcheck, hcheck = self._owner._imageListCheck.GetSize(checkimage) - wcheck += 2*_MARGIN + wcheck += 2 * _MARGIN if wnd: h = max(hcheck, image_h) @@ -2001,11 +2131,11 @@ def __init__(self, parent, id=wx.ID_ANY, item=None, column=None, owner=None, # FIXME: what are all these hardcoded 4, 8 and 11s really? x += image_w + wcheck - w -= image_w + 2*_MARGIN + wcheck + w -= image_w + 2 * _MARGIN + wcheck - super(EditCtrl, self).__init__(parent, id, value, wx.Point(x,y), - wx.Size(w+15, h), - style=style|wx.SIMPLE_BORDER, + super(EditCtrl, self).__init__(parent, id, value, wx.Point(x, y), + wx.Size(w + 15, h), + style=style | wx.SIMPLE_BORDER, name=name, **kwargs) if wx.Platform == "__WXMAC__": @@ -2128,7 +2258,11 @@ def OnChar(self, event): wx.CallAfter(self.Finish) elif keycode == wx.WXK_ESCAPE: - self.StopEditing() + # This calls self.Finish() which calls _owner.ResetEditControl() + # which calls Destroy() on this EditCtrl. We cannot destroy this + # edit control while handing an event for it otherwise wxWidgets + # v3.1+ on GTK2/3 will seg fault. So we must use wx.CallAfter. + wx.CallAfter(self.StopEditing) else: event.Skip() @@ -2196,9 +2330,12 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default self._shiftItem = None self._editItem = None + ## Note: self._selectItem currently does nothing. It appears to + ## have been an abandoned attempt to replicate the logic that + ## customtreectrl subclass uses self._select_me for. self._selectItem = None - self._curColumn = -1 # no current column + self._curColumn = -1 # no current column self._owner = parent self._main_column = 0 self._dragItem = None @@ -2217,7 +2354,7 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default self._left_down_selection = False self._dragTimer = wx.Timer(self) - self._findTimer = wx.Timer(self) + self._findTimer = None self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_MOUSE_EVENTS, self.OnMouse) @@ -2274,11 +2411,8 @@ def GetItemImage(self, item, column=None, which=wx.TreeItemIcon_Normal): ``TreeItemIcon_SelectedExpanded`` To get the selected expanded image (which is shown when an expanded item is currently selected) ================================= ======================== """ - - column = (column is not None and [column] or [self._main_column])[0] - - if column < 0: - return _NO_IMAGE + # wx.lib.mixins.TreeAPIHarmonizer uses -1 for default column! + column = self._main_column if column in (None, -1) else column return item.GetImage(which, column) @@ -2295,16 +2429,22 @@ def SetItemImage(self, item, image, column=None, which=wx.TreeItemIcon_Normal): :see: :meth:`~TreeListMainWindow.GetItemImage` for a list of valid item states. """ - - column = (column is not None and [column] or [self._main_column])[0] - - if column < 0: - return - + # wx.lib.mixins.TreeAPIHarmonizer uses -1 for default column! + column = self._main_column if column in (None, -1) else column + # Force 'image' to integer index and set (flag errors now). + image = _NO_IMAGE if image is None else int(image) item.SetImage(column, image, which) - dc = wx.ClientDC(self) - self.CalculateSize(item, dc) - self.RefreshLine(item) + # Calculate new size of item, if dirty. + if not self._dirty and item.IsDirty() and self.IsItemShown(item): + # Calculate item size to see if it changed height. + old_height = self.GetLineHeight(item) + dc = self._freezeDC if self._freezeDC else wx.ClientDC(self) + self.CalculateSize(item, dc) + # If the height changes, we need to recalculate the tree. + if self.GetLineHeight(item) != old_height: + self._dirty = True + else: + self.RefreshLine(item) def GetItemWindowEnabled(self, item, column=None): @@ -2354,7 +2494,8 @@ def SetItemWindow(self, item, window, column=None): self._hasWindows = True # Recalculate tree during idle time. - self._dirty = True + if item.IsDirty(): + self._dirty = True def SetItemWindowEnabled(self, item, enable=True, column=None): """ @@ -2434,7 +2575,7 @@ def GetPrevChild(self, item, cookie): children = item.GetChildren() if cookie >= 0: - return children[cookie], cookie-1 + return children[cookie], cookie - 1 else: # there are no more of them return None, cookie @@ -2521,8 +2662,12 @@ def DoInsertItem(self, parent, previous, text, ct_type=0, wnd=None, image=-1, se :meth:`CustomTreeCtrl.DoInsertItem() ` changed API. """ - self._dirty = True # do this first so stuff below doesn't cause flicker - arr = [""]*self.GetColumnCount() + self._dirty = True # do this first so stuff below doesn't cause flicker + + # Tree strings must be valid utf8 or we will crash on GetExtent calls. + text = EnsureText(text) + + arr = [""] * self.GetColumnCount() arr[self._main_column] = text if not parent: @@ -2535,7 +2680,7 @@ def DoInsertItem(self, parent, previous, text, ct_type=0, wnd=None, image=-1, se if wnd is not None: self._hasWindows = True - self._itemWithWindow.append(item) + self._itemWithWindow.add(item) parent.Insert(item, previous) @@ -2576,13 +2721,13 @@ def AddRoot(self, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None): raise Exception("\nERROR: Item Type Should Be 0 (Normal), 1 (CheckBox) or 2 (RadioButton). ") self._dirty = True # do this first so stuff below doesn't cause flicker - arr = [""]*self.GetColumnCount() + arr = [""] * self.GetColumnCount() arr[self._main_column] = text self._anchor = TreeListItem(self, None, arr, ct_type, wnd, image, selImage, data) if wnd is not None: self._hasWindows = True - self._itemWithWindow.append(self._anchor) + self._itemWithWindow.add(self._anchor) if self.HasAGWFlag(wx.TR_HIDE_ROOT): # if root is hidden, make sure we can navigate @@ -2593,7 +2738,7 @@ def AddRoot(self, text, ct_type=0, wnd=None, image=-1, selImage=-1, data=None): if not self.HasAGWFlag(wx.TR_MULTIPLE): self._current = self._key_current = self._selectItem = self._anchor - self._current.SetHilight(True) + self.SetItemHilight(self._anchor, True) return self._anchor @@ -2657,16 +2802,22 @@ def ChildrenClosing(self, item): :param `item`: an instance of :class:`TreeListItem`. """ - - if self._editCtrl is not None and item != self._editCtrl.item() and self.IsDescendantOf(item, self._editCtrl.item()): + # If editing a descendant of this item, stop editing before collapsing. + if (self._editCtrl is not None and item != self._editCtrl.item() and + self.IsDescendantOf(item, self._editCtrl.item())): self._editCtrl.StopEditing() + # self._selectItem is currently not used. This logic does nothing. if self.IsDescendantOf(item, self._selectItem): self._selectItem = item + # If a descendant is currently selected, select the item instead. if item != self._current and self.IsDescendantOf(item, self._current): - self._current.SetHilight(False) + # Deselect the currently selected descendant. + self.SetItemHilight(self._current, False) self._current = None + # Flag item to be selected in the next idle handler. + self._select_me = item def DeleteRoot(self): @@ -2709,9 +2860,8 @@ def HideWindows(self): def HideItemWindows(self, item): """Hides all windows belonging to given item and its children.""" # Hide windows for this item. - for column in range(self.GetColumnCount()): - wnd = item.GetWindow(column) - if wnd and wnd.IsShown(): + for wnd in item.GetWindows(): + if wnd.IsShown(): wnd.Hide() # Hide its child windows. for child in item.GetChildren(): @@ -2744,7 +2894,7 @@ def EnableItem(self, item, enable=True, torefresh=True): if torefresh: # We have to refresh the item line - dc = wx.ClientDC(self) + dc = self._freezeDC if self._freezeDC else wx.ClientDC(self) self.CalculateSize(item, dc) self.RefreshLine(item) @@ -2815,28 +2965,21 @@ def ScrollTo(self, item): xUnit, yUnit = self.GetScrollPixelsPerUnit() start_x, start_y = self.GetViewStart() start_y *= yUnit - client_w, client_h = self.GetClientSize () + client_w, client_h = self.GetClientSize() - # Calculate size of entire tree (not necessary anymore?) - x, y = self._anchor.GetSize(0, 0, self) x = self._owner.GetHeaderWindow().GetWidth() - y += yUnit + 2 # one more scrollbar unit + 2 pixels x_pos = self.GetScrollPos(wx.HORIZONTAL) ## Note: The Scroll() method updates all child window positions ## while the SetScrollBars() does not (on most platforms). - if item_y < start_y+3: + if item_y < start_y + 3: # going down, item should appear at top self.Scroll(x_pos, (item_y // yUnit) if yUnit > 0 else 0) -## self.SetScrollbars(xUnit, yUnit, (xUnit and [x//xUnit] or [0])[0], (yUnit and [y//yUnit] or [0])[0], -## x_pos, (yUnit and [item_y//yUnit] or [0])[0]) - elif item_y+self.GetLineHeight(item) > start_y+client_h: + elif item_y + self.GetLineHeight(item) > start_y + client_h: # going up, item should appear at bottom item_y += yUnit + 2 self.Scroll(x_pos, ((item_y + self.GetLineHeight(item) - client_h) // yUnit) if yUnit > 0 else 0) -## self.SetScrollbars(xUnit, yUnit, (xUnit and [x//xUnit] or [0])[0], (yUnit and [y//yUnit] or [0])[0], -## x_pos, (yUnit and [(item_y+self.GetLineHeight(item)-client_h)//yUnit] or [0])[0]) def SetDragItem(self, item): @@ -2858,31 +3001,40 @@ def SetDragItem(self, item): # helpers # ---------------------------------------------------------------------------- - def AdjustMyScrollbars(self): - """ Internal method used to adjust the :class:`ScrolledWindow` scrollbars. """ + def AdjustMyScrollbars(self, tree_size=None): + """Internal method used to adjust the :class:`ScrolledWindow` scrollbars. + This method is always called at the end of CalculatePositions() if the + tree size has changed. + + :param `tree_size`: The full size of the current tree as a wx.Size or + (width, height) tuple. If not specified it will be calculated. + """ if self._freezeCount: # Skip if frozen. Set dirty flag to adjust when thawed. self._dirty = True return if self._anchor: + if tree_size is not None: + x, y = tree_size + else: + x, y = self._anchor.GetSize(0, 0, self) xUnit, yUnit = self.GetScrollPixelsPerUnit() if xUnit == 0: xUnit = self.GetCharWidth() if yUnit == 0: yUnit = self._lineHeight - - x, y = self._anchor.GetSize(0, 0, self) - y += yUnit + 2 # one more scrollbar unit + 2 pixels + y += yUnit + 2 # one more scrollbar unit + 2 pixels x_pos = self.GetScrollPos(wx.HORIZONTAL) y_pos = self.GetScrollPos(wx.VERTICAL) x = self._owner.GetHeaderWindow().GetWidth() + 2 if x < self.GetClientSize().GetWidth(): x_pos = 0 - self.SetScrollbars(xUnit, yUnit, x//xUnit, y//yUnit, x_pos, y_pos) + self.SetScrollbars(xUnit, yUnit, x // xUnit, y // yUnit, x_pos, y_pos) else: + # No root item. Reset scrollbars. self.SetScrollbars(0, 0, 0, 0) @@ -2912,23 +3064,27 @@ def _paintText(text, textrect, alignment): plus = textrect.Width - w if alignment == wx.ALIGN_CENTER: plus //= 2 - dc.DrawLabel(t, wx.Rect(textrect.X + plus, yorigin, w, yorigin+h)) + dc.DrawLabel(t, wx.Rect(textrect.X + plus, yorigin, w, yorigin + h)) yorigin += h return dc.DrawLabel(text, textrect) + # Set the font for this item. attr = item.GetAttributes() - - if attr and attr.HasFont(): - dc.SetFont(attr.GetFont()) - elif item.IsBold(): - dc.SetFont(self._boldFont) if item.IsHyperText(): dc.SetFont(self.GetHyperTextFont()) if item.GetVisited(): dc.SetTextForeground(self.GetHyperTextVisitedColour()) else: dc.SetTextForeground(self.GetHyperTextNewColour()) + elif attr and attr.HasFont(): + dc.SetFont(attr.GetFont()) + elif item.IsBold(): + dc.SetFont(self._boldFont) + elif item.IsItalic(): + dc.SetFont(self._italicFont) + else: + dc.SetFont(self._normalFont) colText = wx.Colour(*dc.GetTextForeground()) @@ -2967,8 +3123,8 @@ def _paintText(text, textrect, alignment): dc.SetPen(wx.TRANSPARENT_PEN) if self.HasAGWFlag(wx.TR_FULL_ROW_HIGHLIGHT): - - itemrect = wx.Rect(0, item.GetY() + off_h, total_w-1, total_h - off_h) + # Draw any highlight or background across entire row. + itemrect = wx.Rect(0, item.GetY() + off_h, total_w - 1, total_h - off_h) if item == self._dragItem: dc.SetBrush(self._hilightBrush) @@ -2978,13 +3134,13 @@ def _paintText(text, textrect, alignment): dc.SetTextForeground(colTextHilight) elif item.IsSelected(): - + # Draw selection rectangle over entire row (all columns) wnd = item.GetWindow(self._main_column) wndx = 0 if wnd: wndx, wndy = item.GetWindowSize(self._main_column) - itemrect = wx.Rect(0, item.GetY() + off_h, total_w-1, total_h - off_h) + itemrect = wx.Rect(0, item.GetY() + off_h, total_w - 1, total_h - off_h) if self._usegradients: if self._gradientstyle == 0: # Horizontal @@ -2996,7 +3152,8 @@ def _paintText(text, textrect, alignment): else: if wx.Platform in ["__WXGTK2__", "__WXMAC__"]: flags = wx.CONTROL_SELECTED - if self._hasFocus: flags = flags | wx.CONTROL_FOCUSED + if self._hasFocus: + flags = flags | wx.CONTROL_FOCUSED wx.RendererNative.Get().DrawItemSelectionRect(self._owner, dc, itemrect, flags) else: dc.SetBrush((self._hasFocus and [self._hilightBrush] or [self._hilightUnfocusedBrush])[0]) @@ -3008,11 +3165,12 @@ def _paintText(text, textrect, alignment): # On GTK+ 2, drawing a 'normal' background is wrong for themes that # don't allow backgrounds to be customized. Not drawing the background, # except for custom item backgrounds, works for both kinds of theme. - elif drawItemBackground: - - pass - # We have to colour the item background for each column separately - # So it is better to move this functionality in the subsequent for loop. + elif drawItemBackground and not self.HasAGWFlag(TR_FILL_WHOLE_COLUMN_BACKGROUND): + # Draw background color under all columns only if TR_FILL_WHOLE_COLUMN_BACKGROUND is not set. + itemrect = wx.Rect(0, item.GetY() + off_h, total_w - 1, total_h - off_h) + dc.SetBrush(wx.Brush(colBg)) + dc.DrawRectangle(itemrect) + dc.SetTextForeground(colText) else: dc.SetTextForeground(colText) @@ -3021,16 +3179,17 @@ def _paintText(text, textrect, alignment): dc.SetTextForeground(colText) - text_extraH = (total_h > text_h and [(total_h - text_h)//2] or [0])[0] - img_extraH = (total_h > self._imgHeight and [(total_h-self._imgHeight)//2] or [0])[0] + text_extraH = (total_h > text_h and [(total_h - text_h) // 2] or [0])[0] + img_extraH = (total_h > self._imgHeight and [(total_h - self._imgHeight) // 2] or [0])[0] x_colstart = 0 + # Draw all columns for this item. for i in range(self.GetColumnCount()): if not self._owner.GetHeaderWindow().IsColumnShown(i): continue col_w = self._owner.GetHeaderWindow().GetColumnWidth(i) - dc.SetClippingRegion(x_colstart, item.GetY(), col_w, total_h) # only within column + dc.SetClippingRegion(x_colstart, item.GetY(), col_w, total_h) # only within column image = _NO_IMAGE x = image_w = wcheck = hcheck = 0 @@ -3038,9 +3197,9 @@ def _paintText(text, textrect, alignment): if i == self.GetMainColumn(): x = item.GetX() + _MARGIN if self.HasButtons(): - x += (self._btnWidth-self._btnWidth2) + _LINEATROOT + x += (self._btnWidth - self._btnWidth2) + _LINEATROOT else: - x -= self._indent//2 + x -= self._indent // 2 if self._imageListNormal: image = item.GetCurrentImage(i) @@ -3064,26 +3223,29 @@ def _paintText(text, textrect, alignment): text_w, dummy, dummy = dc.GetFullMultiLineTextExtent(text) + left_x = x + (2 * _MARGIN if image_w == 0 and wcheck else 0) if alignment == wx.ALIGN_RIGHT: w = col_w - (image_w + wcheck + text_w + off_w + _MARGIN + 1) x += (w > 0 and [w] or [0])[0] elif alignment == wx.ALIGN_CENTER: - w = (col_w - (image_w + wcheck + text_w + off_w + _MARGIN))//2 + w = (col_w - (image_w + wcheck + text_w + off_w + _MARGIN)) // 2 x += (w > 0 and [w] or [0])[0] else: - if image_w == 0 and wcheck: - x += 2*_MARGIN + x = left_x text_x = x + image_w + wcheck + 1 + left_x = left_x + image_w + wcheck + 1 if i == self.GetMainColumn(): item.SetTextX(text_x) if not self.HasAGWFlag(wx.TR_FULL_ROW_HIGHLIGHT): + # Draw item background for column i with FULL_ROW_HIGHLIGHT off. dc.SetBrush((self._hasFocus and [self._hilightBrush] or [self._hilightUnfocusedBrush])[0]) dc.SetPen((self._hasFocus and [self._borderPen] or [wx.TRANSPARENT_PEN])[0]) if i == self.GetMainColumn(): + # Main column (Tree). if item == self._dragItem: if wx.Platform == "__WXMAC__": # don't draw rect outline if we already have the background colour dc.SetPen((item == self._dragItem and [wx.BLACK_PEN] or [wx.TRANSPARENT_PEN])[0]) @@ -3091,9 +3253,18 @@ def _paintText(text, textrect, alignment): dc.SetTextForeground(colTextHilight) elif item.IsSelected(): - - itemrect = wx.Rect(text_x-2, item.GetY() + off_h, text_w+2*_MARGIN, total_h - off_h) - + # If fill whole column background set, draw background + # before selection, since selection covers only text. + if drawItemBackground and self.HasAGWFlag(TR_FILL_WHOLE_COLUMN_BACKGROUND): + itemrect = wx.Rect(x_colstart, item.GetY() + off_h, col_w, total_h - off_h) + dc.SetBrush(wx.Brush(colBg)) + dc.SetPen(wx.TRANSPARENT_PEN) + dc.DrawRectangle(itemrect) + dc.SetBrush(self._hilightBrush if self._hasFocus else self._hilightUnfocusedBrush) + dc.SetPen(self._borderPen if self._hasFocus else wx.TRANSPARENT_PEN) + + # Draw selection over text only. + itemrect = wx.Rect(text_x - 2, item.GetY() + off_h, text_w + 2 * _MARGIN, total_h - off_h) if self._usegradients: if self._gradientstyle == 0: # Horizontal self.DrawHorizontalGradient(dc, itemrect, self._hasFocus) @@ -3104,11 +3275,11 @@ def _paintText(text, textrect, alignment): else: if wx.Platform in ["__WXGTK2__", "__WXMAC__"]: flags = wx.CONTROL_SELECTED - if self._hasFocus: flags = flags | wx.CONTROL_FOCUSED + if self._hasFocus: + flags = flags | wx.CONTROL_FOCUSED wx.RendererNative.Get().DrawItemSelectionRect(self._owner, dc, itemrect, flags) else: dc.DrawRectangle(itemrect) - dc.SetTextForeground(colTextHilight) elif item == self._current: @@ -3120,9 +3291,11 @@ def _paintText(text, textrect, alignment): elif drawItemBackground: if self.HasAGWFlag(TR_FILL_WHOLE_COLUMN_BACKGROUND): - itemrect = wx.Rect(text_x-2, item.GetY() + off_h, col_w-2*_MARGIN, total_h - off_h) + # Draw background under entire main tree width. + itemrect = wx.Rect(x_colstart, item.GetY() + off_h, col_w, total_h - off_h) else: - itemrect = wx.Rect(text_x-2, item.GetY() + off_h, text_w+2*_MARGIN, total_h - off_h) + # Draw background only behind the text. + itemrect = wx.Rect(text_x - 2, item.GetY() + off_h, text_w + 2 * _MARGIN, total_h - off_h) dc.SetBrush(wx.Brush(colBg)) dc.SetPen(wx.TRANSPARENT_PEN) dc.DrawRectangle(itemrect) @@ -3131,11 +3304,11 @@ def _paintText(text, textrect, alignment): dc.SetTextForeground(colText) else: - + # Regular column (non-tree). if self.HasAGWFlag(TR_FILL_WHOLE_COLUMN_BACKGROUND): - itemrect = wx.Rect(text_x-2, item.GetY() + off_h, col_w-2*_MARGIN, total_h - off_h) + itemrect = wx.Rect(x_colstart, item.GetY() + off_h, col_w, total_h - off_h) else: - itemrect = wx.Rect(text_x-2, item.GetY() + off_h, text_w+2*_MARGIN, total_h - off_h) + itemrect = wx.Rect(text_x - 2, item.GetY() + off_h, text_w + 2 * _MARGIN, total_h - off_h) colBgX = item.GetBackgroundColour(i) if colBgX is not None and i != 0: @@ -3146,16 +3319,19 @@ def _paintText(text, textrect, alignment): dc.SetTextForeground(colText) else: - + # TR_FULL_ROW_HIGHLIGHT is ON. The background has already been + # drawn for selected items. If TR_FILL_WHOLE_COLUMN_BACKGROUND + # is not set, it has also been drawn for all columns. if not item.IsSelected(): - + itemrect = None if self.HasAGWFlag(TR_FILL_WHOLE_COLUMN_BACKGROUND): - itemrect = wx.Rect(text_x-2, item.GetY() + off_h, col_w-2*_MARGIN, total_h - off_h) - else: - itemrect = wx.Rect(text_x-2, item.GetY() + off_h, text_w+2*_MARGIN, total_h - off_h) + # Full-row highlight has been suppressed. Draw background for entire column. + itemrect = wx.Rect(x_colstart, item.GetY() + off_h, col_w, total_h - off_h) + elif i != self.GetMainColumn(): + # Allow column-specific backgrounds to draw overtop of any full row highlight. + itemrect = wx.Rect(text_x - 2, item.GetY() + off_h, text_w + 2 * _MARGIN, total_h - off_h) colBgX = item.GetBackgroundColour(i) - - if colBgX is not None: + if itemrect is not None and colBgX is not None: dc.SetBrush(wx.Brush(colBgX, wx.SOLID)) dc.SetPen(wx.TRANSPARENT_PEN) dc.DrawRectangle(itemrect) @@ -3164,7 +3340,7 @@ def _paintText(text, textrect, alignment): if self.HasAGWFlag(TR_COLUMN_LINES): # vertical lines between columns pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DLIGHT), 1, wx.PENSTYLE_SOLID) dc.SetPen((self.GetBackgroundColour() == wx.WHITE and [pen] or [wx.WHITE_PEN])[0]) - dc.DrawLine(x_colstart+col_w-1, item.GetY(), x_colstart+col_w-1, item.GetY()+total_h) + dc.DrawLine(x_colstart + col_w - 1, item.GetY(), x_colstart + col_w - 1, item.GetY() + total_h) dc.SetBackgroundMode(wx.TRANSPARENT) @@ -3193,7 +3369,7 @@ def _paintText(text, textrect, alignment): imglist.Draw(checkimage, dc, item.GetX() + btnWidth + _MARGIN, - item.GetY() + ((total_h > hcheck) and [(total_h-hcheck)//2] or [0])[0]+1, + item.GetY() + ((total_h > hcheck) and [(total_h - hcheck) // 2] or [0])[0] + 1, wx.IMAGELIST_DRAW_TRANSPARENT) if self.HasAGWFlag(TR_ELLIPSIZE_LONG_ITEMS): @@ -3205,7 +3381,7 @@ def _paintText(text, textrect, alignment): text = ChopText(dc, text, maxsize) text_w, text_h, dummy = dc.GetFullMultiLineTextExtent(text) - text_extraH = (total_h > text_h and [(total_h - text_h)//2] or [0])[0] + text_extraH = (total_h > text_h and [(total_h - text_h) // 2] or [0])[0] text_y = item.GetY() + text_extraH textrect = wx.Rect(text_x, text_y, text_w, text_h) @@ -3215,8 +3391,8 @@ def _paintText(text, textrect, alignment): _paintText(text, textrect, alignment) dc.SetTextForeground(foreground) else: - if ( wx.Platform == "__WXMAC__" and item.IsSelected() and - self._hasFocus and i == self.GetMainColumn()) : + if (wx.Platform == "__WXMAC__" and item.IsSelected() and + self._hasFocus and i == self.GetMainColumn()): # Use white on Macs, but only on the primary column if # TR_FULL_ROW_HIGHLIGHT is NOT turned on. dc.SetTextForeground(wx.WHITE) @@ -3238,14 +3414,14 @@ def _paintText(text, textrect, alignment): else: if alignment == wx.ALIGN_RIGHT: # Place window left of text with 2*_MARGIN in between. - wndx = text_x - 2*_MARGIN - wnd.GetSize().width + wndx = text_x - 2 * _MARGIN - wnd.GetSize().width else: # Place window at end of text plus 2*_MARGIN (default). - wndx = text_x + text_w + 2*_MARGIN + wndx = text_x + text_w + 2 * _MARGIN xa, ya = self.CalcScrolledPosition(0, item.GetY()) wndx += xa if item.GetHeight() > item.GetWindowSize(i)[1]: - ya += (item.GetHeight() - item.GetWindowSize(i)[1])//2 + ya += (item.GetHeight() - item.GetWindowSize(i)[1]) // 2 if wnd.GetPosition() != (wndx, ya): wnd.Move(wndx, ya, flags=wx.SIZE_ALLOW_MINUS_ONE) @@ -3256,9 +3432,6 @@ def _paintText(text, textrect, alignment): x_colstart += col_w dc.DestroyClippingRegion() - # restore normal font - dc.SetFont(self._normalFont) - # Now y stands for the top of the item, whereas it used to stand for middle ! def PaintLevel(self, item, dc, level, y, x_maincol): @@ -3269,46 +3442,35 @@ def PaintLevel(self, item, dc, level, y, x_maincol): :param `dc`: an instance of :class:`wx.DC`; :param `level`: the item level in the tree hierarchy; :param `y`: the current vertical position in the :class:`ScrolledWindow`; + It is no longer used. Instead each item's Y position is set by + the CalculatePositions() method. :param `x_maincol`: the horizontal position of the main column. """ # Don't paint hidden items. if item.IsHidden(): return y, x_maincol - # Save window text color. - colText = wx.Colour(*dc.GetTextForeground()) + # Get this item's X,Y position. + x = item.GetX() + y = item.GetY() # Handle hide root (only level 0) - if self.HasAGWFlag(wx.TR_HIDE_ROOT) and level == 0: - for child in item.GetChildren(): + if level == 0 and self.HasAGWFlag(wx.TR_HIDE_ROOT): + # Always expand hidden root. + children = item.GetChildren() + for child in children: y, x_maincol = self.PaintLevel(child, dc, 1, y, x_maincol) + ## TODO: This is where we would draw the vertical line for + ## TR_HIDE_ROOT trees if TR_LINES_AT_ROOT style is set. + ## CustomTreeCtrl does this but it was never implemented here? + # end after expanding root return y, x_maincol - # calculate position of vertical lines - x = x_maincol + _MARGIN # start of column - - if self.HasAGWFlag(wx.TR_LINES_AT_ROOT): - x += _LINEATROOT # space for lines at root - - if self.HasButtons(): - x += (self._btnWidth-self._btnWidth2) # half button space - else: - x += (self._indent-self._indent//2) - - if self.HasAGWFlag(wx.TR_HIDE_ROOT): - x += self._indent*(level-1) # indent but not level 1 - else: - x += self._indent*level # indent according to level - - # set position of vertical line - item.SetX(x) - item.SetY(y) - h = self.GetLineHeight(item) y_top = y - y_mid = y_top + (h//2) + y_mid = y_top + (h // 2) y += h exposed_x = dc.LogicalToDeviceX(0) @@ -3318,18 +3480,8 @@ def PaintLevel(self, item, dc, level, y, x_maincol): draw_row_lines = self.HasAGWFlag(TR_ROW_LINES) if self.IsExposed(exposed_x, exposed_y, _MAX_WIDTH, h + int(draw_row_lines)): - # fill background below twist buttons - if self.HasAGWFlag(TR_FILL_WHOLE_COLUMN_BACKGROUND): - attr = item.GetAttributes() - - if attr and attr.HasBackgroundColour(): - width = self._owner.GetEventHandler().GetColumn(self._main_column).GetWidth() - colBg = attr.GetBackgroundColour() - itemrect = wx.Rect(x_maincol, y-h-1, width, h+1) - - dc.SetBrush(wx.Brush(colBg, wx.SOLID)) - dc.SetPen(wx.TRANSPARENT_PEN) - dc.DrawRectangle(itemrect) + # Save window text color. + prevText = wx.Colour(*dc.GetTextForeground()) # draw item self.PaintItem(item, dc) @@ -3349,7 +3501,7 @@ def PaintLevel(self, item, dc, level, y, x_maincol): x2 = x - self._indent if x2 < (x_maincol + _MARGIN): x2 = x_maincol + _MARGIN - x3 = x + (self._btnWidth-self._btnWidth2) + x3 = x + (self._btnWidth - self._btnWidth2) if self.HasButtons(): if item.HasPlus(): dc.DrawLine(x2, y_mid, x - self._btnWidth2, y_mid) @@ -3357,7 +3509,7 @@ def PaintLevel(self, item, dc, level, y, x_maincol): else: dc.DrawLine(x2, y_mid, x3 + _LINEATROOT, y_mid) else: - dc.DrawLine(x2, y_mid, x - self._indent//2, y_mid) + dc.DrawLine(x2, y_mid, x - self._indent // 2, y_mid) if item.HasPlus() and self.HasButtons(): # should the item show a button? @@ -3382,27 +3534,27 @@ def PaintLevel(self, item, dc, level, y, x_maincol): dc.SetBrush(self._hilightBrush) button = [wx.Point() for j in range(3)] if item.IsExpanded(): - button[0].x = x - (self._btnWidth2+1) - button[0].y = y_mid - (self._btnHeight//3) - button[1].x = x + (self._btnWidth2+1) + button[0].x = x - (self._btnWidth2 + 1) + button[0].y = y_mid - (self._btnHeight // 3) + button[1].x = x + (self._btnWidth2 + 1) button[1].y = button[0].y button[2].x = x - button[2].y = button[0].y + (self._btnHeight2+1) + button[2].y = button[0].y + (self._btnHeight2 + 1) else: - button[0].x = x - (self._btnWidth//3) - button[0].y = y_mid - (self._btnHeight2+1) + button[0].x = x - (self._btnWidth // 3) + button[0].y = y_mid - (self._btnHeight2 + 1) button[1].x = button[0].x - button[1].y = y_mid + (self._btnHeight2+1) - button[2].x = button[0].x + (self._btnWidth2+1) + button[1].y = y_mid + (self._btnHeight2 + 1) + button[2].x = button[0].x + (self._btnWidth2 + 1) button[2].y = y_mid dc.SetClippingRegion(x_maincol + _MARGIN, y_top, clip_width, h) dc.DrawPolygon(button) dc.DestroyClippingRegion() - else: # if (HasAGWFlag(wxTR_HAS_BUTTONS)) + else:# if (HasAGWFlag(wxTR_HAS_BUTTONS)) - rect = wx.Rect(x-self._btnWidth2, y_mid-self._btnHeight2, self._btnWidth, self._btnHeight) + rect = wx.Rect(x - self._btnWidth2, y_mid - self._btnHeight2, self._btnWidth, self._btnHeight) flag = (item.IsExpanded() and [wx.CONTROL_EXPANDED] or [0])[0] wx.RendererNative.GetDefault().DrawTreeItemButton(self, dc, rect, flag) @@ -3413,32 +3565,68 @@ def PaintLevel(self, item, dc, level, y, x_maincol): pen = wx.Pen(wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DLIGHT), 1, wx.PENSTYLE_SOLID) dc.SetPen((self.GetBackgroundColour() == wx.WHITE and [pen] or [wx.WHITE_PEN])[0]) dc.DrawLine(0, y_top, total_width, y_top) - dc.DrawLine(0, y_top+h, total_width, y_top+h) + dc.DrawLine(0, y_top + h, total_width, y_top + h) + # restore DC objects + dc.SetBrush(wx.WHITE_BRUSH) + dc.SetPen(self._dottedPen) + dc.SetTextForeground(prevText) - - # restore DC objects - dc.SetBrush(wx.WHITE_BRUSH) - dc.SetPen(self._dottedPen) - dc.SetTextForeground(colText) - + # If this item is expanded, handle its children. if item.IsExpanded(): - - # process lower levels - if self._imgWidth > 0: - oldY = y_mid + self._imgHeight2 - else: - oldY = y_mid + h//2 - children = item.GetChildren() - for child in children: - y, x_maincol = self.PaintLevel(child, dc, level+1, y, x_maincol) - + count = len(children) - if not self.HasAGWFlag(wx.TR_NO_LINES) and children: - last_child = children[-1] - Y1 = last_child.GetY() + last_child.GetHeight() // 2 - dc.DrawLine(x, oldY, x, Y1) + if count > 0: + # Item has children. Draw only those that are visible. + n = 0 + + # Calculate start and end of client area in logical Y coordinates. + width, height = self.GetClientSize() + start_y = self.CalcUnscrolledPosition(0, 0)[1] + last_y = self.CalcUnscrolledPosition(0, height)[1] + + # If this item is off the bottom of the screen, do nothing. + if y_top > last_y: + return y, x_maincol # Early exit (for TR_HIDE_ROOT only) + + # Binary search for first child that is within our draw area. + n = BisectChildren(children, start_y) + + # Now paint only prospective children + while n < count: + y, x_maincol = self.PaintLevel(children[n], dc, level + 1, y, x_maincol) + n = n + 1 + if y > last_y: + break # Early exit + + if not self.HasAGWFlag(wx.TR_NO_LINES) and children: + # Draw vertical tree line down to middle of last child. + lastY = children[-1].GetY() + lastY += self.GetLineHeight(children[-1]) // 2 + + if self.HasButtons(): + topY = y_mid + self._btnHeight2 # Half of ButtonHeight + else: + topY = y_mid + h // 2 # Half of LineHeight + + # Clip the vertical line to only the visible portion. + # Required speedup since the default _dottedPen is a + # USER_DASH style that draws very slow on some platforms. + xOrigin, yOrigin = dc.GetDeviceOrigin() + yOrigin = abs(yOrigin) + + # Move end points to the begining/end of the view? + if topY < yOrigin: + topY = yOrigin + if lastY > yOrigin + height: + lastY = yOrigin + height + + # after the adjustments if topY is larger than lastY + # then the line isn't visible at all so don't draw anything + if topY < lastY: + dc.SetPen(self._dottedPen) + dc.DrawLine(x, topY, x, lastY) return y, x_maincol @@ -3483,49 +3671,15 @@ def OnPaint(self, event): self.PrepareDC(dc) - if not self._anchor or self.GetColumnCount() <= 0: + if not self._anchor or self._freezeCount or self.GetColumnCount() <= 0: return - # calculate button size - if self._imageListButtons: - self._btnWidth, self._btnHeight = self._imageListButtons.GetSize(0) - elif self.HasButtons(): - self._btnWidth = _BTNWIDTH - self._btnHeight = _BTNHEIGHT - - self._btnWidth2 = self._btnWidth//2 - self._btnHeight2 = self._btnHeight//2 - - # calculate image size - if self._imageListNormal: - self._imgWidth, self._imgHeight = self._imageListNormal.GetSize(0) - - self._imgWidth2 = self._imgWidth//2 - self._imgHeight2 = self._imgHeight//2 - - if self._imageListCheck: - self._checkWidth, self._checkHeight = self._imageListCheck.GetSize(0) - - self._checkWidth2 = self._checkWidth//2 - self._checkHeight2 = self._checkHeight//2 - - # calculate indent size - if self._imageListButtons: - self._indent = max(_MININDENT, self._btnWidth + _MARGIN) - elif self.HasButtons(): - self._indent = max(_MININDENT, self._btnWidth + _LINEATROOT) - - # set default values - dc.SetFont(self._normalFont) - dc.SetPen(self._dottedPen) - - # calculate column start and paint - x_maincol = 0 - for i in range(self.GetMainColumn()): - if not self._owner.GetHeaderWindow().IsColumnShown(i): - continue - x_maincol += self._owner.GetHeaderWindow().GetColumnWidth(i) + # If the tree is dirty, recalculate it. Required for paint. + if self._dirty is True: + self.CalculatePositions() + # Paint the tree. + x_maincol = self._x_maincol y = 2 y, x_maincol = self.PaintLevel(self._anchor, dc, 0, y, x_maincol) @@ -3694,8 +3848,8 @@ def OnMouse(self, event): # we process left mouse up event (enables in-place edit), right down # (pass to the user code), left dbl click (activate item) and # dragging/moving events for items drag-and-drop - if not (event.LeftDown() or event.LeftUp() or event.RightDown() or \ - event.RightUp() or event.LeftDClick() or event.Dragging() or \ + if not (event.LeftDown() or event.LeftUp() or event.RightDown() or + event.RightUp() or event.LeftDClick() or event.Dragging() or event.GetWheelRotation() != 0 or event.Moving()): self._owner.GetEventHandler().ProcessEvent(event) return @@ -3731,7 +3885,7 @@ def OnMouse(self, event): # Determines what item we are hovering over and need a tooltip for hoverItem = item - if (event.LeftDown() or event.LeftUp() or event.RightDown() or \ + if (event.LeftDown() or event.LeftUp() or event.RightDown() or event.RightUp() or event.LeftDClick() or event.Dragging()): if self._editCtrl is not None and (item != self._editCtrl.item() or column != self._editCtrl.column()): self._editCtrl.StopEditing() @@ -3766,7 +3920,7 @@ def OnMouse(self, event): # BeginDrag captures mouse. GTK cannot capture mouse twice. if self.HasCapture() is True: self.ReleaseMouse() - self._dragImage.BeginDrag(wx.Point(0,0), self, fullScreen=self._dragFullScreen) + self._dragImage.BeginDrag(wx.Point(0, 0), self, fullScreen=self._dragFullScreen) self._dragImage.Show() self._dragImage.Move(p) @@ -3783,10 +3937,10 @@ def OnMouse(self, event): # unhighlight the previous drop target if self._dropTarget: - self._dropTarget.SetHilight(False) + self.SetItemHilight(self._dropTarget, False) self.RefreshLine(self._dropTarget) if item: - item.SetHilight(True) + self.SetItemHilight(item, True) self.RefreshLine(item) self._countDrag = self._countDrag + 1 self._dropTarget = item @@ -3798,10 +3952,10 @@ def OnMouse(self, event): self.RefreshLine(self._oldItem) self._countDrag = 0 - return # nothing to do, already done + return # nothing to do, already done if item is None: - return # we need an item to dragging + return # we need an item to dragging # determine drag start if self._dragCount == 0: @@ -3809,7 +3963,7 @@ def OnMouse(self, event): self._dragCount += 1 if self._dragCount < 3: - return # minimum drag 3 pixel + return # minimum drag 3 pixel if self._dragTimer.IsRunning(): return @@ -3820,7 +3974,7 @@ def OnMouse(self, event): command = (event.LeftIsDown() and [wx.wxEVT_COMMAND_TREE_BEGIN_DRAG] or [wx.wxEVT_COMMAND_TREE_BEGIN_RDRAG])[0] nevent = TreeEvent(command, self._owner.GetId()) nevent.SetEventObject(self._owner) - nevent.SetItem(self._current) # the dragged item + nevent.SetItem(self._current) # the dragged item nevent.SetPoint(p) nevent.Veto() # dragging must be explicit allowed! @@ -3837,13 +3991,13 @@ def OnMouse(self, event): if not (self._agwStyle & wx.TR_MULTIPLE): if self._oldSelection: - self._oldSelection.SetHilight(False) + self.SetItemHilight(self._oldSelection, False) self.RefreshLine(self._oldSelection) else: selections = self.GetSelections() if len(selections) == 1: self._oldSelection = selections[0] - self._oldSelection.SetHilight(False) + self.SetItemHilight(self._oldSelection, False) self.RefreshLine(self._oldSelection) elif self._isDragging: # any other event but not event.Dragging() @@ -3858,7 +4012,7 @@ def OnMouse(self, event): # send drag end event event nevent = TreeEvent(wx.wxEVT_COMMAND_TREE_END_DRAG, self._owner.GetId()) nevent.SetEventObject(self._owner) - nevent.SetItem(item) # the item the drag is started + nevent.SetItem(item) # the item the drag is started nevent.SetPoint(p) self._owner.GetEventHandler().ProcessEvent(nevent) @@ -3866,11 +4020,11 @@ def OnMouse(self, event): self._dragImage.EndDrag() if self._dropTarget: - self._dropTarget.SetHilight(False) + self.SetItemHilight(self._dropTarget, False) self.RefreshLine(self._dropTarget) if self._oldSelection: - self._oldSelection.SetHilight(True) + self.SetItemHilight(self._oldSelection, True) self.RefreshLine(self._oldSelection) self._oldSelection = None @@ -3903,23 +4057,23 @@ def OnMouse(self, event): self.SetFocus() nevent = TreeEvent(wx.wxEVT_COMMAND_TREE_ITEM_RIGHT_CLICK, self._owner.GetId()) nevent.SetEventObject(self._owner) - nevent.SetItem(item) # the item clicked - nevent.SetInt(self._curColumn) # the column clicked + nevent.SetItem(item) # the item clicked + nevent.SetInt(self._curColumn) # the column clicked nevent.SetPoint(p) self._owner.GetEventHandler().ProcessEvent(nevent) elif event.LeftUp(): if self._lastOnSame: - if item == self._current and self._curColumn != -1 and \ - self._owner.GetHeaderWindow().IsColumnEditable(self._curColumn) and \ - flags & (wx.TREE_HITTEST_ONITEMLABEL | TREE_HITTEST_ONITEMCOLUMN) and \ - ((self._editCtrl is not None and column != self._editCtrl.column()) or self._editCtrl is None): + if (item == self._current and self._curColumn != -1 and + self._owner.GetHeaderWindow().IsColumnEditable(self._curColumn) and + flags & (wx.TREE_HITTEST_ONITEMLABEL | TREE_HITTEST_ONITEMCOLUMN) and + ((self._editCtrl is not None and column != self._editCtrl.column()) or self._editCtrl is None)): self._editTimer.Start(_EDIT_TIMER_TICKS, wx.TIMER_ONE_SHOT) self._lastOnSame = False - if (((flags & wx.TREE_HITTEST_ONITEMBUTTON) or (flags & wx.TREE_HITTEST_ONITEMICON)) and \ + if (((flags & wx.TREE_HITTEST_ONITEMBUTTON) or (flags & wx.TREE_HITTEST_ONITEMICON)) and self.HasButtons() and item.HasPlus()): # only toggle the item for a single click, double click on @@ -3934,8 +4088,8 @@ def OnMouse(self, event): if not self._left_down_selection: unselect_others = not ((event.ShiftDown() or event.CmdDown()) and self.HasAGWFlag(wx.TR_MULTIPLE)) self.DoSelectItem(item, unselect_others, event.ShiftDown()) - self.EnsureVisible (item) - self._current = self._key_current = item # make the new item the current item + self.EnsureVisible(item) + self._current = self._key_current = item # make the new item the current item else: self._left_down_selection = False @@ -3948,7 +4102,7 @@ def OnMouse(self, event): self.SetFocus() self._lastOnSame = item == self._current - if (((flags & wx.TREE_HITTEST_ONITEMBUTTON) or (flags & wx.TREE_HITTEST_ONITEMICON)) and \ + if (((flags & wx.TREE_HITTEST_ONITEMBUTTON) or (flags & wx.TREE_HITTEST_ONITEMICON)) and self.HasButtons() and item.HasPlus()): # only toggle the item for a single click, double click on @@ -3963,7 +4117,7 @@ def OnMouse(self, event): if item.GetType() > 0: if self.IsItem3State(item): checked = self.GetItem3StateValue(item) - checked = (checked+1)%3 + checked = (checked + 1) % 3 else: checked = not self.IsItemChecked(item) @@ -3975,7 +4129,7 @@ def OnMouse(self, event): unselect_others = not ((event.ShiftDown() or event.CmdDown()) and self.HasAGWFlag(wx.TR_MULTIPLE)) self.DoSelectItem(item, unselect_others, event.ShiftDown()) self.EnsureVisible(item) - self._current = self._key_current = item # make the new item the current item + self._current = self._key_current = item # make the new item the current item self._left_down_selection = True # For some reason, Windows isn't recognizing a left double-click, @@ -3989,8 +4143,8 @@ def OnMouse(self, event): # send activate event first nevent = TreeEvent(wx.wxEVT_COMMAND_TREE_ITEM_ACTIVATED, self._owner.GetId()) nevent.SetEventObject(self._owner) - nevent.SetItem(item) # the item clicked - nevent.SetInt(self._curColumn) # the column clicked + nevent.SetItem(item) # the item clicked + nevent.SetInt(self._curColumn) # the column clicked nevent.SetPoint(p) if not self._owner.GetEventHandler().ProcessEvent(nevent): @@ -4000,8 +4154,8 @@ def OnMouse(self, event): if item.HasPlus(): self.Toggle(item) - else: # any other event skip just in case - + else: + # Any other event skip just in case. event.Skip() @@ -4030,70 +4184,65 @@ def CalculateSize(self, item, dc): :param `item`: an instance of :class:`TreeListItem`; :param `dc`: an instance of :class:`wx.DC`. """ - if self._freezeCount: - # Skip calculate if frozen. Set dirty flag to do this when thawed. - self._dirty = True - return + + # Clear dirty flag. + item.SetDirty(False) + + # Hidden items have a height of 0 and width is irrelevant. if item.IsHidden(): - # Hidden items have a height of 0. item.SetHeight(0) return + + # Calcualte text width + if item.HasExtents() is False: + # Avoid this stuff unless necessary. + attr = item.GetAttributes() + if item.IsHyperText(): + font = self.GetHyperTextFont() # Hypertext font. + elif attr and attr.HasFont(): + font = attr.GetFont() # User-defined font. + elif item.IsBold(): + font = self._boldFont # Bold font. + elif item.IsItalic(): + font = self._italicFont # Italics font. + else: + font = self._normalFont # Default font. + dc.SetFont(font) - attr = item.GetAttributes() - - if attr and attr.HasFont(): - dc.SetFont(attr.GetFont()) - elif item.IsBold(): - dc.SetFont(self._boldFont) - else: - dc.SetFont(self._normalFont) - - text_w = text_h = wnd_w = wnd_h = 0 - for column in range(self.GetColumnCount()): - w, h, dummy = dc.GetFullMultiLineTextExtent(item.GetText(column)) - text_w, text_h = max(w, text_w), max(h, text_h) - - wnd = item.GetWindow(column) - if wnd: - wnd_h = max(wnd_h, item.GetWindowSize(column)[1]) - if column == self._main_column: - wnd_w = item.GetWindowSize(column)[0] - - text_w, dummy, dummy = dc.GetFullMultiLineTextExtent(item.GetText(self._main_column)) - text_h+=2 + text_w, text_h = item.GetExtents(dc) + text_h += 2 - # restore normal font - dc.SetFont(self._normalFont) + wnd_w = wnd_h = 0 + for wnd in item.GetWindows(): + wnd_h = max(wnd_h, wnd.GetSize()[1]) + wnd = item.GetWindow(self._main_column) + if wnd: + wnd_w = wnd.GetSize()[0] image_w, image_h = 0, 0 image = item.GetCurrentImage() - if image != _NO_IMAGE: - - if self._imageListNormal: - - image_w, image_h = self._imageListNormal.GetSize(image) - image_w += 2*_MARGIN - - total_h = ((image_h > text_h) and [image_h] or [text_h])[0] + image_w = self._imgWidth + 2 * _MARGIN + image_h = self._imgHeight + total_h = image_h if image_h > text_h else text_h checkimage = item.GetCurrentCheckedImage() if checkimage is not None: - wcheck, hcheck = self._imageListCheck.GetSize(0) - wcheck += 2*_MARGIN + wcheck, hcheck = self._checkWidth, self._checkHeight + wcheck += 2 * _MARGIN else: wcheck = 0 if total_h < 30: total_h += 2 # at least 2 pixels else: - total_h += total_h//10 # otherwise 10% extra spacing + total_h += total_h // 10 # otherwise 10% extra spacing if total_h > self._lineHeight: - self._lineHeight = max(total_h, wnd_h+2) + self._lineHeight = max(total_h, wnd_h + 2) - item.SetWidth(image_w+text_w+wcheck+2+wnd_w) - item.SetHeight(max(total_h, wnd_h+2)) + item.SetWidth(image_w + text_w + wcheck + 2 + wnd_w) + item.SetHeight(max(total_h, wnd_h + 2)) def CalculateLevel(self, item, dc, level, y, x_colstart): @@ -4103,75 +4252,55 @@ def CalculateLevel(self, item, dc, level, y, x_colstart): :param `item`: an instance of :class:`TreeListItem`; :param `dc`: an instance of :class:`wx.DC`; :param `level`: the item level in the tree hierarchy; - :param `y`: the current vertical position inside the :class:`ScrolledWindow`; - :param `x_colstart`: the x coordinate at which the item's column starts. + :param `y`: the current vertical position in the :class:`ScrolledWindow`; + :param `x_maincol`: the horizontal position of the main column. """ - # calculate position of vertical lines - x = x_colstart + _MARGIN # start of column - if self.HasAGWFlag(wx.TR_LINES_AT_ROOT): - x += _LINEATROOT # space for lines at root - if self.HasButtons(): - x += (self._btnWidth-self._btnWidth2) # half button space - else: - x += (self._indent-self._indent//2) - - if self.HasAGWFlag(wx.TR_HIDE_ROOT): - x += self._indent * (level-1) # indent but not level 1 - else: - x += self._indent * level # indent according to level - - # a hidden root is not evaluated, but its children are always - if self.HasAGWFlag(wx.TR_HIDE_ROOT) and (level == 0): - # a hidden root is not evaluated, but its - # children are always calculated - children = item.GetChildren() - count = len(children) - level = level + 1 - for n in range(count): - y = self.CalculateLevel(children[n], dc, level, y, x_colstart) # recurse - - return y + # Indent according to level + x = x_colstart + level * self._indent - self.CalculateSize(item, dc) + # Calculate the width and height of this item only if required. + if item.IsDirty(): + self.CalculateSize(item, dc) # set its position item.SetX(x) item.SetY(y) + # hidden items don't get a height (height=0). if item.IsHidden(): return y height = self.GetLineHeight(item) - for column in range(self.GetColumnCount()): - wnd = item.GetWindow(column) - if wnd: - # move this window, if necessary. - xa, ya = self.CalcScrolledPosition((0, y)) - wndWidth, wndHeight = item.GetWindowSize(column) - if height > wndHeight: - ya += (height - wndHeight) // 2 - wndx, wndy = wnd.GetPosition() - if wndy != ya: - wnd.Move(wndx, ya, flags=wx.SIZE_ALLOW_MINUS_ONE) - + for wnd in item.GetWindows(): + # Reposition all item windows. + xa, ya = self.CalcScrolledPosition((0, y)) + wndx, wndy, wndWidth, wndHeight = wnd.GetRect() + if height > wndHeight: + ya += (height - wndHeight) // 2 + if wndy != ya: + wnd.Move(wndx, ya, flags=wx.SIZE_ALLOW_MINUS_ONE) + + # Advance Y to next item and update tree width. y += height + self._width = max(self._width, x + item.GetWidth()) if not item.IsExpanded(): - # we don't need to calculate collapsed branches + # We don't need to calculate collapsed branches return y - children = item.GetChildren() - count = len(children) level = level + 1 - for n in range(count): - y = self.CalculateLevel(children[n], dc, level, y, x_colstart) # recurse + for child in item.GetChildren(): + y = self.CalculateLevel(child, dc, level, y, x_colstart) # recurse return y def CalculatePositions(self): - """ Recalculates all the items positions. """ + """Calculates the positions of all items in the tree. + + Used internally. Called to clear the self._dirty flag. + """ if not self._anchor: return @@ -4183,16 +4312,79 @@ def CalculatePositions(self): dc = wx.ClientDC(self) self.PrepareDC(dc) - dc.SetFont(self._normalFont) - dc.SetPen(self._dottedPen) + # Save old tree dimensions. Reset width as CalculateLevel updates it. + old_width, old_height = self._width, self._height + self._width = 0 - y, x_colstart = 2, 0 + # pre-calculate tree button size + if self._imageListButtons: + self._btnWidth, self._btnHeight = self._imageListButtons.GetSize(0) + elif self.HasButtons(): + self._btnWidth = _BTNWIDTH + self._btnHeight = _BTNHEIGHT + self._btnWidth2 = self._btnWidth // 2 + self._btnHeight2 = self._btnHeight // 2 + + # pre-calculate image size + if self._imageListNormal: + self._imgWidth, self._imgHeight = self._imageListNormal.GetSize(0) + self._imgWidth2 = self._imgWidth // 2 + self._imgHeight2 = self._imgHeight // 2 + + # pre-calculate checkbox/radio button size + if self._imageListCheck: + self._checkWidth, self._checkHeight = self._imageListCheck.GetSize(0) + self._checkWidth2 = self._checkWidth // 2 + self._checkHeight2 = self._checkHeight // 2 + + # pre-calculate indent size + if self._imageListButtons: + self._indent = max(_MININDENT, self._btnWidth + _MARGIN) + elif self.HasButtons(): + self._indent = max(_MININDENT, self._btnWidth + _LINEATROOT) + + # Chintzy speedup for GetLineHeight() because it is called so often. + if self.GetAGWWindowStyleFlag() & TR_HAS_VARIABLE_ROW_HEIGHT: + self.GetLineHeight = lambda item: item.GetHeight() + else: + self.GetLineHeight = lambda item: self._lineHeight + + # Calculate X column start. + x_colstart = 0 for i in range(self.GetMainColumn()): if not self._owner.GetHeaderWindow().IsColumnShown(i): continue x_colstart += self._owner.GetHeaderWindow().GetColumnWidth(i) + # Save parameter for use by OnPaint. + self._x_maincol = x_colstart + + # Adjust x_colstart for margin size. + x_colstart += _MARGIN # start of column + if self.HasAGWFlag(wx.TR_LINES_AT_ROOT): + x_colstart += _LINEATROOT # space for lines at root + if self.HasButtons(): + x_colstart += (self._btnWidth - self._btnWidth2) # half button + else: + x_colstart += (self._indent - self._indent // 2) + + # Calculate size of entire tree recursively. + y = 2 + if not self.HasAGWFlag(TR_HIDE_ROOT): + # Calculate tree from root. + y = self.CalculateLevel(self._anchor, dc, 0, y, x_colstart) + else: + # A hidden root is not evaluated, but its children are. + for child in self._anchor.GetChildren(): + y = self.CalculateLevel(child, dc, 0, y, x_colstart) + self._height = y + + # Always update our scrollbars in case the header window size changed. + self.AdjustMyScrollbars(tree_size=(self._width, self._height)) - self.CalculateLevel(self._anchor, dc, 0, y, x_colstart) # start recursion + # Clear tree dirty flag. + self._dirty = False + # Refresh entire window + self.Refresh() def SetItemText(self, item, text, column=None): @@ -4205,16 +4397,25 @@ def SetItemText(self, item, text, column=None): If it is ``None``, the main column index is used. """ - dc = wx.ClientDC(self) - oldtext = item.GetText(column) + column = self.GetMainColumn() if column is None else column + # Tree strings must be valid utf8 or we will crash on GetExtent calls. + text = EnsureText(text) + + # Save old size and set item's text. + old_size = item.GetExtents(column=column) item.SetText(column, text) - # Avoid Calculating tree unless number of lines changed (slow). - if oldtext.count('\n') != text.count('\n'): - self.CalculatePositions() - self.Refresh() - self.AdjustMyScrollbars() + + # If tree already dirty don't attempt to refresh line. + if self._dirty: + return + + # Calculate new size of item's text. + dc = self._freezeDC if self._freezeDC else wx.ClientDC(self) + new_size = item.GetExtents(dc, column) + # If text height changed (number of lines) we need to recalculate tree. + if old_size is None or new_size[1] != old_size[1]: + self._dirty = True else: - self.CalculateSize(item, dc) self.RefreshLine(item) @@ -4244,52 +4445,41 @@ def GetItemWidth(self, item, column): if not item: return 0 - # determine item width - font = self.GetItemFont(item) - if not font.IsOk(): - if item.IsBold(): - font = self._boldFont - elif item.IsItalic(): - font = self._italicFont - elif item.IsHyperText(): - font = self.GetHyperTextFont() - else: - font = self._normalFont - - dc = wx.ClientDC(self) - dc.SetFont(font) - w, h, dummy = dc.GetFullMultiLineTextExtent(item.GetText(column)) - w += 2*_MARGIN - - # calculate width - width = w + 2*_MARGIN if column == self.GetMainColumn(): - width += 2*_MARGIN - if self.HasAGWFlag(wx.TR_LINES_AT_ROOT): - width += _LINEATROOT + # Main column (tree). Calculate item size, if necessary. + if item.IsDirty(): + dc = self._freezeDC if self._freezeDC else wx.ClientDC(self) + self.CalculateSize(item, dc) + width = item.GetX() - self._x_maincol + item.GetWidth() + _MARGIN + 1 if self.HasButtons(): - width += self._btnWidth + _LINEATROOT - if item.GetCurrentImage() != _NO_IMAGE: - width += self._imgWidth - if item.GetType() != 0 and self._imageListCheck: - wcheck, hcheck = self._imageListCheck.GetSize(item.GetType()) - width += wcheck - - # count indent level - level = 0 - parent = item.GetParent() - root = self.GetRootItem() - while (parent and (not self.HasAGWFlag(wx.TR_HIDE_ROOT) or (parent != root))): - level += 1 - parent = parent.GetParent() - - if level: - width += level*self.GetIndent() - - wnd = item.GetWindow(column) - if wnd: - width += wnd.GetSize()[0] + 2*_MARGIN - + width += (self._btnWidth - self._btnWidth2) + _LINEATROOT + else: + width -= self._indent // 2 + else: + # Normal column + if item.HasExtents(column) is False: + # Avoid this stuff unless necessary. + attr = item.GetAttributes() + if item.IsHyperText(): + font = self.GetHyperTextFont() # Hypertext font. + elif attr and attr.HasFont(): + font = attr.GetFont() # User-defined font. + elif item.IsBold(): + font = self._boldFont # Bold font. + elif item.IsItalic(): + font = self._italicFont # Italics font. + else: + font = self._normalFont # Default font. + dc = self._freezeDC if self._freezeDC else wx.ClientDC(self) + dc.SetFont(font) + else: + dc = None + width, height = item.GetExtents(dc, column) + width += 4 * _MARGIN + # If this column has a window, factor that into its width. + wnd = item.GetWindow(column) + if wnd: + width += wnd.GetSize()[0] + 2 * _MARGIN return width @@ -4308,7 +4498,7 @@ def GetBestColumnWidth(self, column, parent=None): # Not shown on screen maxWidth = 1000 - # get root if on item + # get root, if on item if not parent: parent = self.GetRootItem() @@ -4322,24 +4512,25 @@ def GetBestColumnWidth(self, column, parent=None): item, cookie = self.GetFirstChild(parent) while item: - w = self.GetItemWidth(item, column) - if width < w: - width = w - if width > maxWidth: - return maxWidth - - # check the children of this item - if item.IsExpanded(): - w = self.GetBestColumnWidth(column, item) + if item.IsHidden() is False: + w = self.GetItemWidth(item, column) if width < w: width = w if width > maxWidth: return maxWidth + # Recursively check expanded children of this item. + if item.IsExpanded(): + w = self.GetBestColumnWidth(column, item) + if width < w: + width = w + if width > maxWidth: + return maxWidth + # next sibling item, cookie = self.GetNextChild(parent, cookie) - return max(10, width) # Prevent zero column width + return max(10, width) # Prevent zero column width #---------------------------------------------------------------------------- @@ -4375,7 +4566,7 @@ def GetBestColumnWidth(self, column, parent=None): "GetEditControl", "ShouldInheritColours", "GetItemWindow", "SetItemWindow", "DeleteItemWindow", "SetItemTextColour", "HideItem", "DeleteAllItems", "ItemHasChildren", "ToggleItemSelection", "SetItemType", "GetDragFullScreen", "SetDragFullScreen", "GetCurrentItem", "SetItem3State", "SetItem3StateValue", "GetItem3StateValue", "IsItem3State", "GetPrev", - "GetNextShown", "GetPrevShown"] + "GetNextShown", "GetPrevShown", "SetItemData", "GetItemData", "IsVisible", "EnableBellOnNoMatch"] class HyperTreeList(wx.Control): @@ -4408,7 +4599,7 @@ class HyperTreeList(wx.Control): :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.AutoCheckChild` Transverses the tree and checks/unchecks the items. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.AutoCheckParent` Traverses up the tree and checks/unchecks parent items. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.AutoToggleChild` Transverses the tree and toggles the items. - :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckChilds` Programmatically check/uncheck item children. + :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckChilds` Programatically check/uncheck item children. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckItem` Actually checks/uncheks an item, sending the two related events. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckItem2` Used internally to avoid ``EVT_TREE_ITEM_CHECKED`` events. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckSameLevel` Uncheck radio items which are on the same level of the checked one. @@ -4445,7 +4636,7 @@ class HyperTreeList(wx.Control): :meth:`~wx.lib.agw.hypertreelist.TreeListMainWindow.GetFirstExpandedItem` Returns the first item which is in the expanded state. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetFirstGradientColour` Returns the first gradient colour for gradient-style selections. :meth:`~wx.lib.agw.hypertreelist.TreeListMainWindow.GetFirstVisibleItem` Returns the first visible item. - GetFocusedItem Another name for :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetSelection` + GetFocusedItem Another name for :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetSelection` :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetGradientStyle` Returns the gradient style for gradient-style selections. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetHilightFocusColour` Returns the colour used to highlight focused selected items. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetHilightNonFocusColour` Returns the colour used to highlight unfocused selected items. @@ -4460,7 +4651,7 @@ class HyperTreeList(wx.Control): :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetItemFont` Returns the item font. :meth:`~wx.lib.agw.hypertreelist.TreeListMainWindow.GetItemImage` Returns the item image. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetItemParent` Returns the item parent (can be ``None`` for root items). - GetItemPyData Another name for :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetPyData` + GetItemPyData Another name for :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetPyData` :meth:`~wx.lib.agw.hypertreelist.TreeListMainWindow.GetItemText` Returns the item text label. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetItemTextColour` Returns the item text colour or separator horizontal line colour. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.GetItemType` Returns the item type. @@ -4533,7 +4724,7 @@ class HyperTreeList(wx.Control): :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.SetItemHyperText` Sets whether the item is hypertext or not. :meth:`~wx.lib.agw.hypertreelist.TreeListMainWindow.SetItemImage` Sets the item image for a particular item state. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.SetItemItalic` Sets the item font as italic/non-italic. - SetItemPyData Another name for :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.SetPyData` + SetItemPyData Another name for :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.SetPyData` :meth:`~wx.lib.agw.hypertreelist.TreeListMainWindow.SetItemText` Sets the item text label. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.SetItemTextColour` Sets the item text colour or separator horizontal line colour. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.SetItemType` Sets the item type. @@ -4582,8 +4773,9 @@ def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Default self._headerHeight = 0 self._attr_set = False - main_style = style & ~(wx.SIMPLE_BORDER|wx.SUNKEN_BORDER|wx.DOUBLE_BORDER| - wx.RAISED_BORDER|wx.STATIC_BORDER) + main_style = style & ~(wx.SIMPLE_BORDER | wx.SUNKEN_BORDER | + wx.DOUBLE_BORDER | wx.RAISED_BORDER | + wx.STATIC_BORDER) self._agwStyle = agwStyle @@ -4622,7 +4814,7 @@ def Freeze(self): Freezes the HyperTreeList main (tree) and and header windows. This prevents any re-calculation or updates from taking place allowing mass updates to the tree very quickly. :meth:`~Thaw` - must be called to re-enable updates. Calls to these two + must be called to reenable updates. Calls to these two functions may be nested. """ self._main_win.Freeze() @@ -4841,7 +5033,7 @@ def SetColumnWidth(self, column, width): dc = wx.ClientDC(self._header_win) width, dummy = dc.GetMultiLineTextExtent(self._header_win.GetColumnText(column)) # Search TreeListHeaderWindow.OnPaint to understand this: - width += 2*_EXTRA_WIDTH + _MARGIN + width += 2 * _EXTRA_WIDTH + _MARGIN elif width == wx.LIST_AUTOSIZE: @@ -4854,7 +5046,7 @@ def SetColumnWidth(self, column, width): dc = wx.ClientDC(self._header_win) width2, dummy = dc.GetMultiLineTextExtent(self._header_win.GetColumnText(column)) - width2 += 2*_EXTRA_WIDTH + _MARGIN + width2 += 2 * _EXTRA_WIDTH + _MARGIN width = max(width1, width2) @@ -5041,7 +5233,7 @@ def SetColumnShown(self, column, shown): """ if self._main_win.GetMainColumn() == column: - shown = True # Main column cannot be hidden + shown = True # Main column cannot be hidden self.SetColumn(column, self.GetColumn(column).SetShown(shown)) @@ -5196,7 +5388,7 @@ def DoGetBestSize(self): """ # something is better than nothing... - return wx.Size(200, 200) # but it should be specified values! FIXME + return wx.Size(200, 200) # but it should be specified values! FIXME def OnGetItemText(self, item, column): @@ -5344,31 +5536,217 @@ def delegate(self, *args, **kwargs): if __name__ == '__main__': import wx + import wx.lib.agw.hypertreelist as HTL class MyFrame(wx.Frame): + COLUMNS = (("Main Tree", 320, wx.ALIGN_LEFT), + ("Column 1", 320, wx.ALIGN_LEFT), + ("Column 2", 100, wx.ALIGN_CENTER), + ("Column 3", 100, wx.ALIGN_RIGHT)) + NUMBERS = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', + 'seven', 'eight', 'nine'] def __init__(self, parent): - wx.Frame.__init__(self, parent, -1, "HyperTreeList Demo") - - tree_list = HyperTreeList(self) - - tree_list.AddColumn("First column") - - root = tree_list.AddRoot("Root", ct_type=1) - - parent = tree_list.AppendItem(root, "First child", ct_type=1) - child = tree_list.AppendItem(parent, "First Grandchild", ct_type=1) - - tree_list.AppendItem(root, "Second child", ct_type=1) - + wx.Frame.__init__(self, parent, size=(800, 600), + title="HyperTreeList Demo") + self.spin_parent = None + self.spin_count = 0 + self.hidden = False + + panel = wx.Panel(self) + sizer = wx.BoxSizer(orient=wx.VERTICAL) + + # Create a HyperTreeList instance in our panel. + self.tree = HTL.HyperTreeList(panel, agwStyle=wx.TR_DEFAULT_STYLE | + HTL.TR_HAS_VARIABLE_ROW_HEIGHT | + TR_COLUMN_LINES | +# TR_LIVE_UPDATE | + wx.TR_MULTIPLE | + 0) + # Create columns + for title, width, alignment in self.COLUMNS: + self.tree.AddColumn(text=title, width=width, flag=alignment) + self.tree.SetMainColumn(0) + #self.tree.SetBuffered(True) + + # Create an image list to add icons next to an item + size, client = (16, 16), wx.ART_OTHER + il = wx.ImageList(*size) + imgFolder = il.Add(wx.ArtProvider.GetBitmap(wx.ART_NEW_DIR, client, size)) + imgOpen = il.Add(wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, client, size)) + imgFile = il.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, client, size)) + imgExe = il.Add(wx.ArtProvider.GetBitmap(wx.ART_EXECUTABLE_FILE, client, size)) + il.Add(wx.ArtProvider.GetBitmap(wx.ART_COPY, client, size)) + il.Add(wx.ArtProvider.GetBitmap(wx.ART_CUT, client, size)) + il.Add(wx.ArtProvider.GetBitmap(wx.ART_PASTE, client, size)) + il.Add(wx.ArtProvider.GetBitmap(wx.ART_UNDO, client, size)) + self.tree.SetImageList(il) + + # Create root node. + root = self.tree.AddRoot("Root", image=imgFolder, selImage=imgOpen) + self.tree.SetItemBold(root) + + # Freeze tree then populate items. + self.tree.Freeze() + start = time.time() + count = 0 + for x in range(5): + child = self.tree.AppendItem(root, "Item %d" % x) + self.tree.SetItemText(child, self.NUMBERS[x], column=1) + self.tree.SetItemImage(child, imgFolder, which=wx.TreeItemIcon_Normal) + self.tree.SetItemImage(child, imgOpen, which=wx.TreeItemIcon_Expanded) + self.tree.SetItemImage(child, imgFile, which=wx.TreeItemIcon_Selected) + self.tree.SetItemImage(child, imgExe, which=wx.TreeItemIcon_SelectedExpanded) + if x == 1: + self.spin_parent = child + for y in range(5000): + last = self.tree.AppendItem(child, "item %d-%d" % (x, y)) + self.tree.SetItemText(last, "Column 1", column=1) + self.tree.SetItemText(last, "0x%04x" % y, column=2) + self.tree.SetItemText(last, format(y, ','), column=3) + self.tree.SetItemImage(last, imgFolder, which=wx.TreeItemIcon_Normal) + self.tree.SetItemImage(last, imgOpen, which=wx.TreeItemIcon_Expanded) + if x < 2 and y == 0: + button = wx.Button(self.tree, label="Show Windows") + self.tree.SetItemWindow(last, button) + button.Bind(wx.EVT_BUTTON, lambda evt, item=child: + self.OnShowButton(evt, item)) + for z in range(5): + item = self.tree.AppendItem(last, "Subordinate-item %d-%d-%s" % + (x, y, chr(ord("a") + z)), 1) + self.tree.SetItemImage(item, imgFolder, which=wx.TreeItemIcon_Normal) + count += 1 + count += 1 + count += 1 + for x in range(2): + name = "Disable" if x == 1 else "Hide" + child = self.tree.AppendItem(root, "Parent %s %d" % (name, x)) + self.tree.SetItemText(child, self.NUMBERS[x + 5], column=1) + self.tree.SetItemImage(child, imgFolder, which=wx.TreeItemIcon_Normal) + self.tree.SetItemImage(child, imgOpen, which=wx.TreeItemIcon_Expanded) + for y in range(10000): + last = self.tree.AppendItem(child, "%s item %d-%d" % (name, x, y)) + self.tree.SetItemImage(last, imgFolder, which=wx.TreeItemIcon_Normal) + self.tree.SetItemImage(last, imgOpen, which=wx.TreeItemIcon_Expanded) + if y > 4 and x == 1: + self.tree.EnableItem(last, True if y & 0x20 else False) + elif y > 4 and x != 1: + self.tree.HideItem(last) + count += 1 + count += 1 + elapsed = abs(time.time() - start) * 1000 + print("Tree populate %s items took %.3fms" % (count, elapsed)) + start = time.time() + self.tree.Thaw() + elapsed = abs(time.time() - start) * 1000 + print("Thaw took %.3fms" % elapsed) + start = time.time() + self.tree.Expand(root) + elapsed = abs(time.time() - start) * 1000 + print("Expand root took %.3fms" % elapsed) + + sizer.Add(self.tree, proportion=1, flag=wx.EXPAND) + + # Create tree test button row. + row = wx.BoxSizer(orient=wx.HORIZONTAL) + spin_button = wx.ToggleButton(panel, label="Spin") + spin_button.Bind(wx.EVT_TOGGLEBUTTON, self.OnSpinButton) + row.Add(spin_button, flag=wx.RIGHT, border=20) + unhide_button = wx.Button(panel, label="Unhide") + unhide_button.Bind(wx.EVT_BUTTON, self.OnUnhide) + row.Add(unhide_button, flag=wx.RIGHT, border=20) + getsel_button = wx.Button(panel, label="GetSelections") + getsel_button.Bind(wx.EVT_BUTTON, self.OnGetSelections) + row.Add(getsel_button, flag=wx.RIGHT, border=20) + sizer.Add(row, flag=wx.ALIGN_CENTER | wx.ALL, border=5) + + panel.SetSizer(sizer) + self.timer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.OnTimer, source=self.timer) + self.Bind(wx.EVT_CLOSE, self.OnClose) + + def OnSpinButton(self, event): + if self.timer.IsRunning(): + self.timer.Stop() + if event.GetEventObject().GetValue(): + self.timer.Start(milliseconds=300) + + def OnShowButton(self, event, parent): + # Add windows to first 100 items in this branch. + self.tree.Freeze() + self.Freeze() + size, client = (16, 16), wx.ART_OTHER + bitmap = wx.ArtProvider.GetBitmap(wx.ART_COPY, client, size) + hypertree = self.tree.GetMainWindow() + count = len(hypertree._itemWithWindow) + start = time.time() + for index, child in enumerate(parent.GetChildren()): + if not self.tree.GetItemWindow(child, column=3): + widget = wx.StaticBitmap(hypertree, bitmap=bitmap) + self.tree.SetItemWindow(child, widget, column=3) + if index > 1000: + break + self.Thaw() + self.tree.Thaw() + elapsed = abs(time.time() - start) * 1000 + print("Added %d windows in %.3fms" % (len(hypertree._itemWithWindow) - count, elapsed)) + + def OnHideButton(self, event): + start = time.time() + windows = self.tree.GetMainWindow()._itemWithWindow + for item in windows: + for wnd in item.GetWindows(): + wnd.Hide() + elapsed = abs(time.time() - start) * 1000 + print("Hid %d windows in %.3fms" % (len(windows), elapsed)) + + def OnGetSelections(self, event): + start = time.time() + selections = self.tree.GetSelections() + elapsed = abs(time.time() - start) * 1000 + print("Got %d selections in %.3fms" % (len(selections), elapsed)) + + def OnUnhide(self, event): + # Recursively hide/unhide items with "Hidden" in their text. + start = time.time() + self.item_count = 0 + self.DoHideUnhide(self.tree.GetRootItem(), self.hidden) + elapsed = abs(time.time() - start) * 1000 + print("Changed hide state in %.3fms (%d items scanned)" % + (elapsed, self.item_count)) + self.hidden = not self.hidden + event.GetEventObject().SetLabel("Hide" if self.hidden else "Unhide") + + def DoHideUnhide(self, item, hide): + self.item_count += 1 + text = item.GetText() + if len(text) > 13 and text.startswith('Hide'): + self.tree.HideItem(item, hide) + for child in item.GetChildren(): + self.DoHideUnhide(child, hide) + + def OnTimer(self, event): + self.spin_count = (self.spin_count + 1) % 8 + if self.spin_parent: + start = time.time() + self.tree.Freeze() + children = self.spin_parent.GetChildren() + for child in children: + self.tree.SetItemImage(child, self.spin_count, which=0) + self.tree.Thaw() + elapsed = abs(time.time() - start) * 1000 + print("Spun %d items in %.3fms" % (len(children), elapsed)) + + def OnClose(self, event): + if self.timer.IsRunning() is True: + self.timer.Stop() + event.Skip() # our normal wxApp-derived class, as usual - - app = wx.App(0) + app = wx.App(False) locale = wx.Locale(wx.LANGUAGE_DEFAULT) frame = MyFrame(None) - app.SetTopWindow(frame) frame.Show() app.MainLoop() From 45ad9274641060788581ce04c9f2257183788bf1 Mon Sep 17 00:00:00 2001 From: cbeytas Date: Tue, 25 Jan 2022 04:23:14 +0000 Subject: [PATCH 3/4] Put spelling fixes back in --- wx/lib/agw/customtreectrl.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/wx/lib/agw/customtreectrl.py b/wx/lib/agw/customtreectrl.py index 9fb4daf54..09335840f 100644 --- a/wx/lib/agw/customtreectrl.py +++ b/wx/lib/agw/customtreectrl.py @@ -227,7 +227,7 @@ def __init__(self, parent): selected a ``EVT_TREE_SEL_CHANGING`` event is sent which can be vetoed to prevent the selection from occuring. A ``EVT_TREE_SEL_CHANGED`` event is sent out when the selection has successfully changed. If the selection gets cleared -by the user or programatically, the next Idle handler will select the root item +by the user or programmatically, the next Idle handler will select the root item ensuring there is a selected item at all times. If the ``TR_MULTIPLE`` style is set the tree can have any number of selected @@ -273,7 +273,7 @@ def __init__(self, parent): By default item windows are shown immediately after the item text. If the ``TR_ALIGN_WINDOWS`` style is set, all windows for a level of the tree will be aligned to the longest text in that level. The ``TR_ALIGN_WINDOWS_RIGHT`` -style will instead right-alight all windows added to the tree. +style will instead right-align all windows added to the tree. Keyboard focus in the tree can be difficult to manage once windows are added. Most platforms will shift focus to an item window any time the tree gets focus. @@ -967,7 +967,7 @@ def CreateBitmap(self): memory.SelectObject(wx.NullBitmap) - # Gtk and Windows unfortunatly don't do so well with transparent + # Gtk and Windows unfortunately don't do so well with transparent # drawing so this hack corrects the image to have a transparent # background. if wx.Platform != '__WXMAC__': @@ -1777,7 +1777,7 @@ def __init__(self, parent, text="", ct_type=0, wnd=None, image=-1, selImage=-1, """ - # since there can be very many of these, we save size by chosing + # since there can be very many of these, we save size by choosing # the smallest representation for the elements and by ordering # the members to avoid padding. self._text = text # label to be rendered for item @@ -3765,7 +3765,7 @@ def AutoCheckParent(self, item, checked): def CheckChilds(self, item, checked=True): """ - Programatically check/uncheck item children. + Programmatically check/uncheck item children. :param `item`: an instance of :class:`GenericTreeItem`; :param bool `checked`: ``True`` to check an item, ``False`` to uncheck it. @@ -6584,7 +6584,7 @@ def DoSelectItem(self, item, unselect_others=True, extended_select=False, select = True # the default - # Check if we need to toggle hilight (ctrl mode) + # Check if we need to toggle highlight (ctrl mode) if not unselect_others: select = not item.IsSelected() @@ -7257,7 +7257,7 @@ def DrawVerticalGradient(self, dc, rect, hasfocus): rf, gf, bf = 0, 0, 0 for y in range(rect.y, rect.y + rect.height): - currCol = (r1 + rf, g1 + gf, b1 + bf) + currCol = (int(r1 + rf), int(g1 + gf), int(b1 + bf)) dc.SetBrush(wx.Brush(currCol, wx.BRUSHSTYLE_SOLID)) dc.DrawRectangle(rect.x, y, rect.width, 1) rf = rf + rstep @@ -7364,7 +7364,7 @@ def DrawVistaRectangle(self, dc, rect, hasfocus): dc.SetPen(wx.TRANSPARENT_PEN) for y in range(filRect.y, filRect.y + filRect.height): - currCol = (r1 + rf, g1 + gf, b1 + bf) + currCol = (int(r1 + rf), int(g1 + gf), int(b1 + bf)) dc.SetBrush(wx.Brush(currCol, wx.BRUSHSTYLE_SOLID)) dc.DrawRectangle(filRect.x, y, filRect.width, 1) rf = rf + rstep @@ -7903,7 +7903,7 @@ def PaintLevel(self, item, dc, level, y, align): xOrigin, yOrigin = dc.GetDeviceOrigin() yOrigin = abs(yOrigin) - # Move end points to the begining/end of the view? + # Move end points to the beginning/end of the view? if y_mid < yOrigin: y_mid = yOrigin if lastY > yOrigin + height: @@ -9502,7 +9502,7 @@ def Freeze(self): Freeze :class:`CustomTreeCtrl`. Freezes the window or, in other words, prevents any updates from taking place - on screen, the window is not redrawn at all. :meth:`~Thaw` must be called to reenable + on screen, the window is not redrawn at all. :meth:`~Thaw` must be called to re-enable window redrawing. Calls to these two functions may be nested. :note: This method is useful for visual appearance optimization (for example, @@ -9523,7 +9523,7 @@ def Thaw(self): """ Thaw :class:`CustomTreeCtrl`. - Reenables window updating after a previous call to :meth:`~Freeze`. To really thaw the + Re-enables window updating after a previous call to :meth:`~Freeze`. To really thaw the control, it must be called exactly the same number of times as :meth:`~Freeze`. :raise: `Exception` if :meth:`~Thaw` has been called without an un-matching :meth:`~Freeze`. @@ -9647,7 +9647,7 @@ def GetMaxWidth(self, respect_expansion_state=True): rect = self.GetBoundingRect(root, True) # It looks like the space between the "+" and the node - # rect occupies 4 pixels approximatively + # rect occupies 4 pixels approximately maxwidth = rect.x + rect.width + 4 lastheight = rect.y + rect.height @@ -9687,7 +9687,7 @@ def RecurseOnChildren(self, item, maxwidth, respect_expansion_state): rect = self.GetBoundingRect(child, True) # It looks like the space between the "+" and the node - # rect occupies 4 pixels approximatively + # rect occupies 4 pixels approximately maxwidth = max(maxwidth, rect.x + rect.width + 4) lastheight = rect.y + rect.height From 8e14c3822cb0552baa20f1b7715f60bd014cbd31 Mon Sep 17 00:00:00 2001 From: cbeytas Date: Tue, 25 Jan 2022 04:42:59 +0000 Subject: [PATCH 4/4] Put back in spelling fixes and other commits lost --- wx/lib/agw/hypertreelist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wx/lib/agw/hypertreelist.py b/wx/lib/agw/hypertreelist.py index a1cb62c89..17b39f351 100644 --- a/wx/lib/agw/hypertreelist.py +++ b/wx/lib/agw/hypertreelist.py @@ -4599,7 +4599,7 @@ class HyperTreeList(wx.Control): :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.AutoCheckChild` Transverses the tree and checks/unchecks the items. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.AutoCheckParent` Traverses up the tree and checks/unchecks parent items. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.AutoToggleChild` Transverses the tree and toggles the items. - :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckChilds` Programatically check/uncheck item children. + :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckChilds` Programmatically check/uncheck item children. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckItem` Actually checks/uncheks an item, sending the two related events. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckItem2` Used internally to avoid ``EVT_TREE_ITEM_CHECKED`` events. :meth:`~wx.lib.agw.customtreectrl.CustomTreeCtrl.CheckSameLevel` Uncheck radio items which are on the same level of the checked one. @@ -4814,7 +4814,7 @@ def Freeze(self): Freezes the HyperTreeList main (tree) and and header windows. This prevents any re-calculation or updates from taking place allowing mass updates to the tree very quickly. :meth:`~Thaw` - must be called to reenable updates. Calls to these two + must be called to re-enable updates. Calls to these two functions may be nested. """ self._main_win.Freeze() @@ -4825,7 +4825,7 @@ def Thaw(self): """ Thaw :class:`HyperTreeList`. - Reenables updates to the main (tree) and header windows after a + Re-enables updates to the main (tree) and header windows after a previous call to :meth:`~Freeze`. To really thaw the control, it must be called exactly the same number of times as :meth:`~Freeze`. When fully thawed the tree will re-calculate and update itself. @@ -5685,7 +5685,7 @@ def OnShowButton(self, event, parent): if not self.tree.GetItemWindow(child, column=3): widget = wx.StaticBitmap(hypertree, bitmap=bitmap) self.tree.SetItemWindow(child, widget, column=3) - if index > 1000: + if index > 100: break self.Thaw() self.tree.Thaw()