diff --git a/wx/lib/agw/customtreectrl.py b/wx/lib/agw/customtreectrl.py index 0fbb15c46..09335840f 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 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 +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-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. +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,7 +962,7 @@ 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) @@ -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 # ----------------------------------------------------------------------------- @@ -1664,9 +1781,11 @@ def __init__(self, parent, text="", ct_type=0, wnd=None, image=-1, selImage=-1, # 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): @@ -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. + + :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. - crt_item.SetHilight(select) - self.RefreshLine(crt_item) + :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`. + + 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. + :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) 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): @@ -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()) @@ -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 + # Get this item's X,Y position. + x = item.GetX() - self._spacing + y = item.GetY() - elif level == 0: - - # 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? 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 += self._indent - - 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 + x = x_colstart + level * self._indent - return y + # Calculate the width and height of this item only if required. + if item.IsDirty(): + self.CalculateSize(item, dc, level, align) - 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,19 +9493,9 @@ 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): """ @@ -8695,13 +9515,15 @@ 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): """ 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`. @@ -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() + # ---------------------------------------------------------------------------- @@ -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 diff --git a/wx/lib/agw/hypertreelist.py b/wx/lib/agw/hypertreelist.py index 60ec7435a..17b39f351 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): @@ -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 @@ -4633,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. @@ -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 > 100: + 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()