1#----------------------------------------------------------------------------
2# Name:        wx.lib.mixins.listctrl
3# Purpose:     Helpful mix-in classes for wxListCtrl
4#
5# Author:      Robin Dunn
6#
7# Created:     15-May-2001
8# Copyright:   (c) 2001-2018 by Total Control Software
9# Licence:     wxWindows license
10# Tags:        phoenix-port, py3-port
11#----------------------------------------------------------------------------
12# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net)
13#
14# o 2.5 compatibility update.
15# o ListCtrlSelectionManagerMix untested.
16#
17# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net)
18#
19# o wxColumnSorterMixin -> ColumnSorterMixin
20# o wxListCtrlAutoWidthMixin -> ListCtrlAutoWidthMixin
21# ...
22# 13/10/2004 - Pim Van Heuven (pim@think-wize.com)
23# o wxTextEditMixin: Support Horizontal scrolling when TAB is pressed on long
24#       ListCtrls, support for WXK_DOWN, WXK_UP, performance improvements on
25#       very long ListCtrls, Support for virtual ListCtrls
26#
27# 15-Oct-2004 - Robin Dunn
28# o wxTextEditMixin: Added Shift-TAB support
29#
30# 2008-11-19 - raf <raf@raf.org>
31# o ColumnSorterMixin: Added GetSortState()
32#
33
34import  locale
35import  wx
36import six
37
38if six.PY3:
39    # python 3 lacks cmp:
40    def cmp(a, b):
41        return (a > b) - (a < b)
42
43#----------------------------------------------------------------------------
44
45class ColumnSorterMixin:
46    """
47    A mixin class that handles sorting of a wx.ListCtrl in REPORT mode when
48    the column header is clicked on.
49
50    There are a few requirments needed in order for this to work genericly:
51
52      1. The combined class must have a GetListCtrl method that
53         returns the wx.ListCtrl to be sorted, and the list control
54         must exist at the time the wx.ColumnSorterMixin.__init__
55         method is called because it uses GetListCtrl.
56
57      2. Items in the list control must have a unique data value set
58         with list.SetItemData.
59
60      3. The combined class must have an attribute named itemDataMap
61         that is a dictionary mapping the data values to a sequence of
62         objects representing the values in each column.  These values
63         are compared in the column sorter to determine sort order.
64
65    Interesting methods to override are GetColumnSorter,
66    GetSecondarySortValues, and GetSortImages.  See below for details.
67    """
68
69    def __init__(self, numColumns):
70        self.SetColumnCount(numColumns)
71        list = self.GetListCtrl()
72        if not list:
73            raise ValueError("No wx.ListCtrl available")
74        list.Bind(wx.EVT_LIST_COL_CLICK, self.__OnColClick, list)
75
76
77    def SetColumnCount(self, newNumColumns):
78        self._colSortFlag = [0] * newNumColumns
79        self._col = -1
80
81
82    def SortListItems(self, col=-1, ascending=1):
83        """Sort the list on demand.  Can also be used to set the sort column and order."""
84        oldCol = self._col
85        if col != -1:
86            self._col = col
87            self._colSortFlag[col] = ascending
88        self.GetListCtrl().SortItems(self.GetColumnSorter())
89        self.__updateImages(oldCol)
90
91
92    def GetColumnWidths(self):
93        """
94        Returns a list of column widths.  Can be used to help restore the current
95        view later.
96        """
97        list = self.GetListCtrl()
98        rv = []
99        for x in range(len(self._colSortFlag)):
100            rv.append(list.GetColumnWidth(x))
101        return rv
102
103
104    def GetSortImages(self):
105        """
106        Returns a tuple of image list indexesthe indexes in the image list for an image to be put on the column
107        header when sorting in descending order.
108        """
109        return (-1, -1)  # (decending, ascending) image IDs
110
111
112    def GetColumnSorter(self):
113        """Returns a callable object to be used for comparing column values when sorting."""
114        return self.__ColumnSorter
115
116
117    def GetSecondarySortValues(self, col, key1, key2):
118        """Returns a tuple of 2 values to use for secondary sort values when the
119           items in the selected column match equal.  The default just returns the
120           item data values."""
121        return (key1, key2)
122
123
124    def __OnColClick(self, evt):
125        oldCol = self._col
126        self._col = col = evt.GetColumn()
127        self._colSortFlag[col] = int(not self._colSortFlag[col])
128        self.GetListCtrl().SortItems(self.GetColumnSorter())
129        if wx.Platform != "__WXMAC__" or wx.SystemOptions.GetOptionInt("mac.listctrl.always_use_generic") == 1:
130            self.__updateImages(oldCol)
131        evt.Skip()
132        self.OnSortOrderChanged()
133
134
135    def OnSortOrderChanged(self):
136        """
137        Callback called after sort order has changed (whenever user
138        clicked column header).
139        """
140        pass
141
142
143    def GetSortState(self):
144        """
145        Return a tuple containing the index of the column that was last sorted
146        and the sort direction of that column.
147        Usage:
148        col, ascending = self.GetSortState()
149        # Make changes to list items... then resort
150        self.SortListItems(col, ascending)
151        """
152        return (self._col, self._colSortFlag[self._col])
153
154
155    def __ColumnSorter(self, key1, key2):
156        col = self._col
157        ascending = self._colSortFlag[col]
158        item1 = self.itemDataMap[key1][col]
159        item2 = self.itemDataMap[key2][col]
160
161        #--- Internationalization of string sorting with locale module
162        if isinstance(item1, six.text_type) and isinstance(item2, six.text_type):
163            # both are unicode (py2) or str (py3)
164            cmpVal = locale.strcoll(item1, item2)
165        elif isinstance(item1, six.binary_type) or isinstance(item2, six.binary_type):
166            # at least one is a str (py2) or byte (py3)
167            cmpVal = locale.strcoll(str(item1), str(item2))
168        else:
169            cmpVal = cmp(item1, item2)
170        #---
171
172        # If the items are equal then pick something else to make the sort value unique
173        if cmpVal == 0:
174            cmpVal = cmp(*self.GetSecondarySortValues(col, key1, key2))
175
176        if ascending:
177            return cmpVal
178        else:
179            return -cmpVal
180
181
182    def __updateImages(self, oldCol):
183        sortImages = self.GetSortImages()
184        if self._col != -1 and sortImages[0] != -1:
185            img = sortImages[self._colSortFlag[self._col]]
186            list = self.GetListCtrl()
187            if oldCol != -1:
188                list.ClearColumnImage(oldCol)
189            list.SetColumnImage(self._col, img)
190
191
192#----------------------------------------------------------------------------
193#----------------------------------------------------------------------------
194
195class ListCtrlAutoWidthMixin:
196    """ A mix-in class that automatically resizes the last column to take up
197        the remaining width of the wx.ListCtrl.
198
199        This causes the wx.ListCtrl to automatically take up the full width of
200        the list, without either a horizontal scroll bar (unless absolutely
201        necessary) or empty space to the right of the last column.
202
203        NOTE:    This only works for report-style lists.
204
205        WARNING: If you override the EVT_SIZE event in your wx.ListCtrl, make
206                 sure you call event.Skip() to ensure that the mixin's
207                 _OnResize method is called.
208
209        This mix-in class was written by Erik Westra <ewestra@wave.co.nz>
210    """
211    def __init__(self):
212        """ Standard initialiser.
213        """
214        self._resizeColMinWidth = None
215        self._resizeColStyle = "LAST"
216        self._resizeCol = 0
217        self.Bind(wx.EVT_SIZE, self._onResize)
218        self.Bind(wx.EVT_LIST_COL_END_DRAG, self._onResize, self)
219
220
221    def setResizeColumn(self, col):
222        """
223        Specify which column that should be autosized.  Pass either
224        'LAST' or the column number.  Default is 'LAST'.
225        """
226        if col == "LAST":
227            self._resizeColStyle = "LAST"
228        else:
229            self._resizeColStyle = "COL"
230            self._resizeCol = col
231
232
233    def resizeLastColumn(self, minWidth):
234        """ Resize the last column appropriately.
235
236            If the list's columns are too wide to fit within the window, we use
237            a horizontal scrollbar.  Otherwise, we expand the right-most column
238            to take up the remaining free space in the list.
239
240            This method is called automatically when the wx.ListCtrl is resized;
241            you can also call it yourself whenever you want the last column to
242            be resized appropriately (eg, when adding, removing or resizing
243            columns).
244
245            'minWidth' is the preferred minimum width for the last column.
246        """
247        self.resizeColumn(minWidth)
248
249
250    def resizeColumn(self, minWidth):
251        self._resizeColMinWidth = minWidth
252        self._doResize()
253
254
255    # =====================
256    # == Private Methods ==
257    # =====================
258
259    def _onResize(self, event):
260        """ Respond to the wx.ListCtrl being resized.
261
262            We automatically resize the last column in the list.
263        """
264        if 'gtk2' in wx.PlatformInfo or 'gtk3' in wx.PlatformInfo:
265            self._doResize()
266        else:
267            wx.CallAfter(self._doResize)
268        event.Skip()
269
270
271    def _doResize(self):
272        """ Resize the last column as appropriate.
273
274            If the list's columns are too wide to fit within the window, we use
275            a horizontal scrollbar.  Otherwise, we expand the right-most column
276            to take up the remaining free space in the list.
277
278            We remember the current size of the last column, before resizing,
279            as the preferred minimum width if we haven't previously been given
280            or calculated a minimum width.  This ensure that repeated calls to
281            _doResize() don't cause the last column to size itself too large.
282        """
283
284        if not self:  # avoid a PyDeadObject error
285            return
286
287        if self.GetSize().height < 32:
288            return  # avoid an endless update bug when the height is small.
289
290        numCols = self.GetColumnCount()
291        if numCols == 0: return # Nothing to resize.
292
293        if(self._resizeColStyle == "LAST"):
294            resizeCol = self.GetColumnCount()
295        else:
296            resizeCol = self._resizeCol
297
298        resizeCol = max(1, resizeCol)
299
300        if self._resizeColMinWidth == None:
301            self._resizeColMinWidth = self.GetColumnWidth(resizeCol - 1)
302
303        # Get total width
304        listWidth = self.GetClientSize().width
305
306        totColWidth = 0 # Width of all columns except last one.
307        for col in range(numCols):
308            if col != (resizeCol-1):
309                totColWidth = totColWidth + self.GetColumnWidth(col)
310
311        resizeColWidth = self.GetColumnWidth(resizeCol - 1)
312
313        if totColWidth + self._resizeColMinWidth > listWidth:
314            # We haven't got the width to show the last column at its minimum
315            # width -> set it to its minimum width and allow the horizontal
316            # scrollbar to show.
317            self.SetColumnWidth(resizeCol-1, self._resizeColMinWidth)
318            return
319
320        # Resize the last column to take up the remaining available space.
321
322        self.SetColumnWidth(resizeCol-1, listWidth - totColWidth)
323
324
325
326
327#----------------------------------------------------------------------------
328#----------------------------------------------------------------------------
329
330SEL_FOC = wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED
331def selectBeforePopup(event):
332    """Ensures the item the mouse is pointing at is selected before a popup.
333
334    Works with both single-select and multi-select lists."""
335    ctrl = event.GetEventObject()
336    if isinstance(ctrl, wx.ListCtrl):
337        n, flags = ctrl.HitTest(event.GetPosition())
338        if n >= 0:
339            if not ctrl.GetItemState(n, wx.LIST_STATE_SELECTED):
340                for i in range(ctrl.GetItemCount()):
341                    ctrl.SetItemState(i, 0, SEL_FOC)
342                #for i in getListCtrlSelection(ctrl, SEL_FOC):
343                #    ctrl.SetItemState(i, 0, SEL_FOC)
344                ctrl.SetItemState(n, SEL_FOC, SEL_FOC)
345
346
347def getListCtrlSelection(listctrl, state=wx.LIST_STATE_SELECTED):
348    """ Returns list of item indexes of given state (selected by defaults) """
349    res = []
350    idx = -1
351    while 1:
352        idx = listctrl.GetNextItem(idx, wx.LIST_NEXT_ALL, state)
353        if idx == -1:
354            break
355        res.append(idx)
356    return res
357
358wxEVT_DOPOPUPMENU = wx.NewEventType()
359EVT_DOPOPUPMENU = wx.PyEventBinder(wxEVT_DOPOPUPMENU, 0)
360
361
362class ListCtrlSelectionManagerMix:
363    """Mixin that defines a platform independent selection policy
364
365    As selection single and multi-select list return the item index or a
366    list of item indexes respectively.
367    """
368    _menu = None
369
370    def __init__(self):
371        self.Bind(wx.EVT_RIGHT_DOWN, self.OnLCSMRightDown)
372        self.Bind(EVT_DOPOPUPMENU, self.OnLCSMDoPopup)
373#        self.Connect(-1, -1, self.wxEVT_DOPOPUPMENU, self.OnLCSMDoPopup)
374
375
376    def getPopupMenu(self):
377        """ Override to implement dynamic menus (create) """
378        return self._menu
379
380
381    def setPopupMenu(self, menu):
382        """ Must be set for default behaviour """
383        self._menu = menu
384
385
386    def afterPopupMenu(self, menu):
387        """ Override to implement dynamic menus (destroy) """
388        pass
389
390
391    def getSelection(self):
392        res = getListCtrlSelection(self)
393        if self.GetWindowStyleFlag() & wx.LC_SINGLE_SEL:
394            if res:
395                return res[0]
396            else:
397                return -1
398        else:
399            return res
400
401
402    def OnLCSMRightDown(self, event):
403        selectBeforePopup(event)
404        event.Skip()
405        menu = self.getPopupMenu()
406        if menu:
407            evt = wx.PyEvent()
408            evt.SetEventType(wxEVT_DOPOPUPMENU)
409            evt.menu = menu
410            evt.pos = event.GetPosition()
411            wx.PostEvent(self, evt)
412
413
414    def OnLCSMDoPopup(self, event):
415        self.PopupMenu(event.menu, event.pos)
416        self.afterPopupMenu(event.menu)
417
418
419#----------------------------------------------------------------------------
420#----------------------------------------------------------------------------
421from bisect import bisect
422
423
424class TextEditMixin:
425    """
426    A mixin class that enables any text in any column of a
427    multi-column listctrl to be edited by clicking on the given row
428    and column.  You close the text editor by hitting the ENTER key or
429    clicking somewhere else on the listctrl. You switch to the next
430    column by hiting TAB.
431
432    To use the mixin you have to include it in the class definition
433    and call the __init__ function::
434
435        class TestListCtrl(wx.ListCtrl, TextEditMixin):
436            def __init__(self, parent, ID, pos=wx.DefaultPosition,
437                         size=wx.DefaultSize, style=0):
438                wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
439                TextEditMixin.__init__(self)
440
441
442    Authors:     Steve Zatz, Pim Van Heuven (pim@think-wize.com)
443    """
444
445    editorBgColour = wx.Colour(255,255,175) # Yellow
446    editorFgColour = wx.Colour(0,0,0)       # black
447
448    def __init__(self):
449        #editor = wx.TextCtrl(self, -1, pos=(-1,-1), size=(-1,-1),
450        #                     style=wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB \
451        #                     |wx.TE_RICH2)
452
453        self.make_editor()
454        self.Bind(wx.EVT_TEXT_ENTER, self.CloseEditor)
455        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
456        self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown)
457        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected, self)
458
459
460    def make_editor(self, col_style=wx.LIST_FORMAT_LEFT):
461
462        style =wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB|wx.TE_RICH2
463        style |= {wx.LIST_FORMAT_LEFT: wx.TE_LEFT,
464                  wx.LIST_FORMAT_RIGHT: wx.TE_RIGHT,
465                  wx.LIST_FORMAT_CENTRE : wx.TE_CENTRE
466                  }[col_style]
467
468        editor = wx.TextCtrl(self, -1, style=style)
469        editor.SetBackgroundColour(self.editorBgColour)
470        editor.SetForegroundColour(self.editorFgColour)
471        font = self.GetFont()
472        editor.SetFont(font)
473
474        self.curRow = 0
475        self.curCol = 0
476
477        editor.Hide()
478        if hasattr(self, 'editor'):
479            self.editor.Destroy()
480        self.editor = editor
481
482        self.col_style = col_style
483        self.editor.Bind(wx.EVT_CHAR, self.OnChar)
484        self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor)
485
486
487    def OnItemSelected(self, evt):
488        self.curRow = evt.GetIndex()
489        evt.Skip()
490
491
492    def OnChar(self, event):
493        ''' Catch the TAB, Shift-TAB, cursor DOWN/UP key code
494            so we can open the editor at the next column (if any).'''
495
496        keycode = event.GetKeyCode()
497        if keycode == wx.WXK_TAB and event.ShiftDown():
498            self.CloseEditor()
499            if self.curCol-1 >= 0:
500                self.OpenEditor(self.curCol-1, self.curRow)
501
502        elif keycode == wx.WXK_TAB:
503            self.CloseEditor()
504            if self.curCol+1 < self.GetColumnCount():
505                self.OpenEditor(self.curCol+1, self.curRow)
506
507        elif keycode == wx.WXK_ESCAPE:
508            self.CloseEditor()
509
510        elif keycode == wx.WXK_DOWN:
511            self.CloseEditor()
512            if self.curRow+1 < self.GetItemCount():
513                self._SelectIndex(self.curRow+1)
514                self.OpenEditor(self.curCol, self.curRow)
515
516        elif keycode == wx.WXK_UP:
517            self.CloseEditor()
518            if self.curRow > 0:
519                self._SelectIndex(self.curRow-1)
520                self.OpenEditor(self.curCol, self.curRow)
521
522        else:
523            event.Skip()
524
525
526    def OnLeftDown(self, evt=None):
527        ''' Examine the click and double
528        click events to see if a row has been click on twice. If so,
529        determine the current row and columnn and open the editor.'''
530
531        if self.editor.IsShown():
532            self.CloseEditor()
533
534        x,y = evt.GetPosition()
535        row,flags = self.HitTest((x,y))
536
537        if row != self.curRow: # self.curRow keeps track of the current row
538            evt.Skip()
539            return
540
541        # the following should really be done in the mixin's init but
542        # the wx.ListCtrl demo creates the columns after creating the
543        # ListCtrl (generally not a good idea) on the other hand,
544        # doing this here handles adjustable column widths
545
546        self.col_locs = [0]
547        loc = 0
548        for n in range(self.GetColumnCount()):
549            loc = loc + self.GetColumnWidth(n)
550            self.col_locs.append(loc)
551
552
553        col = bisect(self.col_locs, x+self.GetScrollPos(wx.HORIZONTAL)) - 1
554        self.OpenEditor(col, row)
555
556
557    def OpenEditor(self, col, row):
558        ''' Opens an editor at the current position. '''
559
560        # give the derived class a chance to Allow/Veto this edit.
561        evt = wx.ListEvent(wx.wxEVT_COMMAND_LIST_BEGIN_LABEL_EDIT, self.GetId())
562        evt.Index = row
563        evt.Column = col
564        item = self.GetItem(row, col)
565        evt.Item.SetId(item.GetId())
566        evt.Item.SetColumn(item.GetColumn())
567        evt.Item.SetData(item.GetData())
568        evt.Item.SetText(item.GetText())
569        ret = self.GetEventHandler().ProcessEvent(evt)
570        if ret and not evt.IsAllowed():
571            return   # user code doesn't allow the edit.
572
573        if self.GetColumn(col).Align != self.col_style:
574            self.make_editor(self.GetColumn(col).Align)
575
576        x0 = self.col_locs[col]
577        x1 = self.col_locs[col+1] - x0
578
579        scrolloffset = self.GetScrollPos(wx.HORIZONTAL)
580
581        # scroll forward
582        if x0+x1-scrolloffset > self.GetSize()[0]:
583            if wx.Platform == "__WXMSW__":
584                # don't start scrolling unless we really need to
585                offset = x0+x1-self.GetSize()[0]-scrolloffset
586                # scroll a bit more than what is minimum required
587                # so we don't have to scroll everytime the user presses TAB
588                # which is very tireing to the eye
589                addoffset = self.GetSize()[0]/4
590                # but be careful at the end of the list
591                if addoffset + scrolloffset < self.GetSize()[0]:
592                    offset += addoffset
593
594                self.ScrollList(offset, 0)
595                scrolloffset = self.GetScrollPos(wx.HORIZONTAL)
596            else:
597                # Since we can not programmatically scroll the ListCtrl
598                # close the editor so the user can scroll and open the editor
599                # again
600                self.editor.SetValue(self.GetItem(row, col).GetText())
601                self.curRow = row
602                self.curCol = col
603                self.CloseEditor()
604                return
605
606        y0 = self.GetItemRect(row)[1]
607
608        def _activate_editor(editor):
609            editor.SetSize(x0-scrolloffset,y0, x1,-1, wx.SIZE_USE_EXISTING)
610            editor.SetValue(self.GetItem(row, col).GetText())
611            editor.Show()
612            editor.Raise()
613            editor.SetSelection(-1,-1)
614            editor.SetFocus()
615
616        wx.CallAfter(_activate_editor, self.editor)
617
618        self.curRow = row
619        self.curCol = col
620
621
622    # FIXME: this function is usually called twice - second time because
623    # it is binded to wx.EVT_KILL_FOCUS. Can it be avoided? (MW)
624    def CloseEditor(self, evt=None):
625        ''' Close the editor and save the new value to the ListCtrl. '''
626        if not self.editor.IsShown():
627            return
628        text = self.editor.GetValue()
629        self.editor.Hide()
630        self.SetFocus()
631
632        # post wxEVT_COMMAND_LIST_END_LABEL_EDIT
633        # Event can be vetoed. It doesn't has SetEditCanceled(), what would
634        # require passing extra argument to CloseEditor()
635        evt = wx.ListEvent(wx.wxEVT_COMMAND_LIST_END_LABEL_EDIT, self.GetId())
636        evt.Index = self.curRow
637        evt.Column = self.curCol
638        item = wx.ListItem(self.GetItem(self.curRow, self.curCol))
639        item.SetText(text)
640        evt.SetItem(item)
641
642        ret = self.GetEventHandler().ProcessEvent(evt)
643        if not ret or evt.IsAllowed():
644            if self.IsVirtual():
645                # replace by whather you use to populate the virtual ListCtrl
646                # data source
647                self.SetVirtualData(self.curRow, self.curCol, text)
648            else:
649                self.SetItem(self.curRow, self.curCol, text)
650        self.RefreshItem(self.curRow)
651
652    def _SelectIndex(self, row):
653        listlen = self.GetItemCount()
654        if row < 0 and not listlen:
655            return
656        if row > (listlen-1):
657            row = listlen -1
658
659        self.SetItemState(self.curRow, ~wx.LIST_STATE_SELECTED,
660                          wx.LIST_STATE_SELECTED)
661        self.EnsureVisible(row)
662        self.SetItemState(row, wx.LIST_STATE_SELECTED,
663                          wx.LIST_STATE_SELECTED)
664
665
666
667#----------------------------------------------------------------------------
668#----------------------------------------------------------------------------
669
670"""
671FILENAME: CheckListCtrlMixin.py
672AUTHOR:   Bruce Who (bruce.who.hk at gmail.com)
673DATE:     2006-02-09
674DESCRIPTION:
675    This script provide a mixin for ListCtrl which add a checkbox in the first
676    column of each row. It is inspired by limodou's CheckList.py(which can be
677    got from his NewEdit) and improved:
678        - You can just use InsertStringItem() to insert new items;
679        - Once a checkbox is checked/unchecked, the corresponding item is not
680          selected;
681        - You can use SetItemData() and GetItemData();
682        - Interfaces are changed to OnCheckItem(), IsChecked(), CheckItem().
683
684    You should not set a imagelist for the ListCtrl once this mixin is used.
685
686HISTORY:
6871.3     - You can check/uncheck a group of sequential items by <Shift-click>:
688          First click(or <Shift-Click>) item1 to check/uncheck it, then
689          Shift-click item2 to check/uncheck it, and you'll find that all
690          items between item1 and item2 are check/unchecked!
6911.2     - Add ToggleItem()
6921.1     - Initial version
693"""
694
695class CheckListCtrlMixin(object):
696    """
697    This is a mixin for ListCtrl which add a checkbox in the first
698    column of each row. It is inspired by limodou's CheckList.py(which
699    can be got from his NewEdit) and improved:
700
701        - You can just use InsertStringItem() to insert new items;
702
703        - Once a checkbox is checked/unchecked, the corresponding item
704          is not selected;
705
706        - You can use SetItemData() and GetItemData();
707
708        - Interfaces are changed to OnCheckItem(), IsChecked(),
709          CheckItem().
710
711    You should not set a imagelist for the ListCtrl once this mixin is used.
712    """
713    def __init__(self, check_image=None, uncheck_image=None, imgsz=(16,16)):
714        if check_image is not None:
715            imgsz = check_image.GetSize()
716        elif uncheck_image is not None:
717            imgsz = check_image.GetSize()
718
719        self.__imagelist_ = wx.ImageList(*imgsz)
720
721        # Create default checkbox images if none were specified
722        if check_image is None:
723            check_image = self.__CreateBitmap(wx.CONTROL_CHECKED, imgsz)
724
725        if uncheck_image is None:
726            uncheck_image = self.__CreateBitmap(0, imgsz)
727
728        self.uncheck_image = self.__imagelist_.Add(uncheck_image)
729        self.check_image = self.__imagelist_.Add(check_image)
730        self.AssignImageList(self.__imagelist_, wx.IMAGE_LIST_SMALL)
731        self.__last_check_ = None
732
733        self.Bind(wx.EVT_LEFT_DOWN, self.__OnLeftDown_)
734
735        # Monkey-patch in a new InsertItem so we can also set the image ID for the item
736        self._origInsertItem = self.InsertItem
737        self.InsertItem = self.__InsertItem_
738
739
740    def __InsertItem_(self, *args, **kw):
741        index = self._origInsertItem(*args, **kw)
742        self.SetItemImage(index, self.uncheck_image)
743        return index
744
745
746    def __CreateBitmap(self, flag=0, size=(16, 16)):
747        """Create a bitmap of the platforms native checkbox. The flag
748        is used to determine the checkboxes state (see wx.CONTROL_*)
749
750        """
751        bmp = wx.Bitmap(*size)
752        dc = wx.MemoryDC(bmp)
753        dc.SetBackground(wx.WHITE_BRUSH)
754        dc.Clear()
755        wx.RendererNative.Get().DrawCheckBox(self, dc,
756                                             (0, 0, size[0], size[1]), flag)
757        dc.SelectObject(wx.NullBitmap)
758        return bmp
759
760
761    def __OnLeftDown_(self, evt):
762        (index, flags) = self.HitTest(evt.GetPosition())
763        if flags == wx.LIST_HITTEST_ONITEMICON:
764            img_idx = self.GetItem(index).GetImage()
765            flag_check = img_idx == 0
766            begin_index = index
767            end_index = index
768            if self.__last_check_ is not None \
769                    and wx.GetKeyState(wx.WXK_SHIFT):
770                last_index, last_flag_check = self.__last_check_
771                if last_flag_check == flag_check:
772                    # XXX what if the previous item is deleted or new items
773                    # are inserted?
774                    item_count = self.GetItemCount()
775                    if last_index < item_count:
776                        if last_index < index:
777                            begin_index = last_index
778                            end_index = index
779                        elif last_index > index:
780                            begin_index = index
781                            end_index = last_index
782                        else:
783                            assert False
784            while begin_index <= end_index:
785                self.CheckItem(begin_index, flag_check)
786                begin_index += 1
787            self.__last_check_ = (index, flag_check)
788        else:
789            evt.Skip()
790
791    def OnCheckItem(self, index, flag):
792        pass
793
794    def IsChecked(self, index):
795        return self.GetItem(index).GetImage() == 1
796
797    def CheckItem(self, index, check=True):
798        img_idx = self.GetItem(index).GetImage()
799        if img_idx == 0 and check:
800            self.SetItemImage(index, 1)
801            self.OnCheckItem(index, True)
802        elif img_idx == 1 and not check:
803            self.SetItemImage(index, 0)
804            self.OnCheckItem(index, False)
805
806    def ToggleItem(self, index):
807        self.CheckItem(index, not self.IsChecked(index))
808
809
810#----------------------------------------------------------------------------
811#----------------------------------------------------------------------------
812
813# Mode Flags
814HIGHLIGHT_ODD = 1   # Highlight the Odd rows
815HIGHLIGHT_EVEN = 2  # Highlight the Even rows
816
817class ListRowHighlighter:
818    """Editra Control Library: ListRowHighlighter
819    Mixin class that handles automatic background highlighting of alternate
820    rows in the a ListCtrl. The background of the rows are highlighted
821    automatically as items are added or inserted in the control based on the
822    mixins Mode and set Color. By default the Even rows will be highlighted with
823    the systems highlight color.
824
825    """
826    def __init__(self, color=None, mode=HIGHLIGHT_EVEN):
827        """Initialize the highlighter mixin
828        @keyword color: Set a custom highlight color (default uses system color)
829        @keyword mode: HIGHLIGHT_EVEN (default) or HIGHLIGHT_ODD
830
831        """
832        # Attributes
833        self._color = color
834        self._defaultb = wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX)
835        self._mode = mode
836
837        # Event Handlers
838        self.Bind(wx.EVT_LIST_INSERT_ITEM, lambda evt: self.RefreshRows())
839        self.Bind(wx.EVT_LIST_DELETE_ITEM, lambda evt: self.RefreshRows())
840
841    def RefreshRows(self):
842        """Re-color all the rows"""
843        if self._color is None:
844            if wx.Platform in ('__WXGTK__', '__WXMSW__'):
845                color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DLIGHT)
846            else:
847                color = wx.Colour(237, 243, 254)
848        else:
849            color = self._color
850        local_defaultb = self._defaultb
851        local_mode = self._mode
852        for row in range(self.GetItemCount()):
853            if local_mode & HIGHLIGHT_EVEN:
854                dohlight = not row % 2
855            else:
856                dohlight = row % 2
857
858            if dohlight:
859                self.SetItemBackgroundColour(row, color)
860            elif local_defaultb:
861                self.SetItemBackgroundColour(row, local_defaultb)
862            else: # This part of the loop should only happen once if self._defaultb is None.
863                local_defaultb = self._defaultb = self.GetItemBackgroundColour(row)
864                self.SetItemBackgroundColour(row, local_defaultb)
865
866    def SetHighlightColor(self, color):
867        """Set the color used to highlight the rows. Call :meth:`RefreshRows` after
868        this if you wish to update all the rows highlight colors.
869        @param color: wx.Color or None to set default
870
871        """
872        self._color = color
873
874    def SetHighlightMode(self, mode):
875        """Set the highlighting mode to either HIGHLIGHT_EVEN or to
876        HIGHLIGHT_ODD. Call :meth:`RefreshRows` afterwards to update the list
877        state.
878        @param mode: HIGHLIGHT_* mode value
879
880        """
881        self._mode = mode
882
883#----------------------------------------------------------------------------
884