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