]> git.saurik.com Git - wxWidgets.git/blame - wxPython/wx/lib/mixins/listctrl.py
docstring update
[wxWidgets.git] / wxPython / wx / lib / mixins / listctrl.py
CommitLineData
d14a1e28
RD
1#----------------------------------------------------------------------------
2# Name: wxPython.lib.mixins.listctrl
3# Purpose: Helpful mix-in classes for wxListCtrl
4#
5# Author: Robin Dunn
6#
7# Created: 15-May-2001
8# RCS-ID: $Id$
9# Copyright: (c) 2001 by Total Control Software
10# Licence: wxWindows license
11#----------------------------------------------------------------------------
b881fc78
RD
12# 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net)
13#
14# o 2.5 compatability update.
15# o ListCtrlSelectionManagerMix untested.
16#
d4b73b1b
RD
17# 12/21/2003 - Jeff Grimmett (grimmtooth@softhome.net)
18#
19# o wxColumnSorterMixin -> ColumnSorterMixin
15513a80
RD
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
d4b73b1b 29#
1fded56b 30
b881fc78
RD
31import locale
32import wx
1fded56b 33
d14a1e28
RD
34#----------------------------------------------------------------------------
35
d4b73b1b 36class ColumnSorterMixin:
d14a1e28 37 """
b881fc78 38 A mixin class that handles sorting of a wx.ListCtrl in REPORT mode when
d14a1e28
RD
39 the column header is clicked on.
40
41 There are a few requirments needed in order for this to work genericly:
42
43 1. The combined class must have a GetListCtrl method that
b881fc78
RD
44 returns the wx.ListCtrl to be sorted, and the list control
45 must exist at the time the wx.ColumnSorterMixin.__init__
d14a1e28
RD
46 method is called because it uses GetListCtrl.
47
48 2. Items in the list control must have a unique data value set
49 with list.SetItemData.
50
51 3. The combined class must have an attribute named itemDataMap
52 that is a dictionary mapping the data values to a sequence of
53 objects representing the values in each column. These values
54 are compared in the column sorter to determine sort order.
55
56 Interesting methods to override are GetColumnSorter,
57 GetSecondarySortValues, and GetSortImages. See below for details.
58 """
59
60 def __init__(self, numColumns):
61 self.SetColumnCount(numColumns)
62 list = self.GetListCtrl()
63 if not list:
b881fc78 64 raise ValueError, "No wx.ListCtrl available"
438550d8 65 list.Bind(wx.EVT_LIST_COL_CLICK, self.__OnColClick, list)
d14a1e28
RD
66
67
68 def SetColumnCount(self, newNumColumns):
69 self._colSortFlag = [0] * newNumColumns
70 self._col = -1
71
72
73 def SortListItems(self, col=-1, ascending=1):
74 """Sort the list on demand. Can also be used to set the sort column and order."""
75 oldCol = self._col
76 if col != -1:
77 self._col = col
78 self._colSortFlag[col] = ascending
79 self.GetListCtrl().SortItems(self.GetColumnSorter())
80 self.__updateImages(oldCol)
81
82
83 def GetColumnWidths(self):
84 """
85 Returns a list of column widths. Can be used to help restore the current
86 view later.
87 """
88 list = self.GetListCtrl()
89 rv = []
90 for x in range(len(self._colSortFlag)):
91 rv.append(list.GetColumnWidth(x))
92 return rv
93
94
95 def GetSortImages(self):
96 """
97 Returns a tuple of image list indexesthe indexes in the image list for an image to be put on the column
98 header when sorting in descending order.
99 """
100 return (-1, -1) # (decending, ascending) image IDs
101
102
103 def GetColumnSorter(self):
104 """Returns a callable object to be used for comparing column values when sorting."""
105 return self.__ColumnSorter
106
107
108 def GetSecondarySortValues(self, col, key1, key2):
109 """Returns a tuple of 2 values to use for secondary sort values when the
110 items in the selected column match equal. The default just returns the
111 item data values."""
112 return (key1, key2)
113
114
115 def __OnColClick(self, evt):
116 oldCol = self._col
117 self._col = col = evt.GetColumn()
118 self._colSortFlag[col] = not self._colSortFlag[col]
119 self.GetListCtrl().SortItems(self.GetColumnSorter())
120 self.__updateImages(oldCol)
121 evt.Skip()
122
123
124 def __ColumnSorter(self, key1, key2):
125 col = self._col
126 ascending = self._colSortFlag[col]
127 item1 = self.itemDataMap[key1][col]
128 item2 = self.itemDataMap[key2][col]
129
130 #--- Internationalization of string sorting with locale module
131 if type(item1) == type('') or type(item2) == type(''):
132 cmpVal = locale.strcoll(str(item1), str(item2))
133 else:
134 cmpVal = cmp(item1, item2)
135 #---
136
137 # If the items are equal then pick something else to make the sort value unique
138 if cmpVal == 0:
139 cmpVal = apply(cmp, self.GetSecondarySortValues(col, key1, key2))
140
141 if ascending:
142 return cmpVal
143 else:
144 return -cmpVal
145
146
147 def __updateImages(self, oldCol):
148 sortImages = self.GetSortImages()
149 if self._col != -1 and sortImages[0] != -1:
150 img = sortImages[self._colSortFlag[self._col]]
151 list = self.GetListCtrl()
152 if oldCol != -1:
153 list.ClearColumnImage(oldCol)
154 list.SetColumnImage(self._col, img)
155
156
157#----------------------------------------------------------------------------
158#----------------------------------------------------------------------------
159
d4b73b1b 160class ListCtrlAutoWidthMixin:
d14a1e28 161 """ A mix-in class that automatically resizes the last column to take up
b881fc78 162 the remaining width of the wx.ListCtrl.
d14a1e28 163
b881fc78 164 This causes the wx.ListCtrl to automatically take up the full width of
d14a1e28
RD
165 the list, without either a horizontal scroll bar (unless absolutely
166 necessary) or empty space to the right of the last column.
167
168 NOTE: This only works for report-style lists.
169
b881fc78 170 WARNING: If you override the EVT_SIZE event in your wx.ListCtrl, make
d14a1e28
RD
171 sure you call event.Skip() to ensure that the mixin's
172 _OnResize method is called.
173
174 This mix-in class was written by Erik Westra <ewestra@wave.co.nz>
5841276a 175 """
d14a1e28
RD
176 def __init__(self):
177 """ Standard initialiser.
178 """
27ed367c
RD
179 self._resizeColMinWidth = None
180 self._resizeColStyle = "LAST"
181 self._resizeCol = 0
b881fc78
RD
182 self.Bind(wx.EVT_SIZE, self._onResize)
183 self.Bind(wx.EVT_LIST_COL_END_DRAG, self._onResize, self)
d14a1e28
RD
184
185
27ed367c
RD
186 def setResizeColumn(self, col):
187 """
188 Specify which column that should be autosized. Pass either
189 'LAST' or the column number. Default is 'LAST'.
190 """
191 if col == "LAST":
192 self._resizeColStyle = "LAST"
193 else:
194 self._resizeColStyle = "COL"
195 self._resizeCol = col
196
197
d14a1e28
RD
198 def resizeLastColumn(self, minWidth):
199 """ Resize the last column appropriately.
200
201 If the list's columns are too wide to fit within the window, we use
202 a horizontal scrollbar. Otherwise, we expand the right-most column
203 to take up the remaining free space in the list.
204
b881fc78 205 This method is called automatically when the wx.ListCtrl is resized;
d14a1e28
RD
206 you can also call it yourself whenever you want the last column to
207 be resized appropriately (eg, when adding, removing or resizing
208 columns).
209
210 'minWidth' is the preferred minimum width for the last column.
211 """
e4f0ea6b 212 self.resizeColumn(minWidth)
27ed367c
RD
213
214
215 def resizeColumn(self, minWidth):
216 self._resizeColMinWidth = minWidth
d14a1e28 217 self._doResize()
27ed367c 218
d14a1e28
RD
219
220 # =====================
221 # == Private Methods ==
222 # =====================
223
224 def _onResize(self, event):
b881fc78 225 """ Respond to the wx.ListCtrl being resized.
d14a1e28
RD
226
227 We automatically resize the last column in the list.
228 """
e31753ef
RD
229 if 'gtk2' in wx.PlatformInfo:
230 self._doResize()
231 else:
232 wx.CallAfter(self._doResize)
d14a1e28
RD
233 event.Skip()
234
235
236 def _doResize(self):
237 """ Resize the last column as appropriate.
238
239 If the list's columns are too wide to fit within the window, we use
240 a horizontal scrollbar. Otherwise, we expand the right-most column
241 to take up the remaining free space in the list.
242
243 We remember the current size of the last column, before resizing,
244 as the preferred minimum width if we haven't previously been given
245 or calculated a minimum width. This ensure that repeated calls to
246 _doResize() don't cause the last column to size itself too large.
247 """
a61c65b3
RD
248
249 if not self: # avoid a PyDeadObject error
250 return
251
d14a1e28
RD
252 numCols = self.GetColumnCount()
253 if numCols == 0: return # Nothing to resize.
254
27ed367c
RD
255 if(self._resizeColStyle == "LAST"):
256 resizeCol = self.GetColumnCount()
257 else:
258 resizeCol = self._resizeCol
259
260 if self._resizeColMinWidth == None:
261 self._resizeColMinWidth = self.GetColumnWidth(resizeCol - 1)
d14a1e28
RD
262
263 # We're showing the vertical scrollbar -> allow for scrollbar width
264 # NOTE: on GTK, the scrollbar is included in the client size, but on
265 # Windows it is not included
266 listWidth = self.GetClientSize().width
b881fc78 267 if wx.Platform != '__WXMSW__':
d14a1e28 268 if self.GetItemCount() > self.GetCountPerPage():
b881fc78 269 scrollWidth = wx.SystemSettings_GetMetric(wx.SYS_VSCROLL_X)
d14a1e28
RD
270 listWidth = listWidth - scrollWidth
271
272 totColWidth = 0 # Width of all columns except last one.
27ed367c
RD
273 for col in range(numCols):
274 if col != (resizeCol-1):
275 totColWidth = totColWidth + self.GetColumnWidth(col)
d14a1e28 276
27ed367c 277 resizeColWidth = self.GetColumnWidth(resizeCol - 1)
d14a1e28 278
27ed367c 279 if totColWidth + self._resizeColMinWidth > listWidth:
d14a1e28
RD
280 # We haven't got the width to show the last column at its minimum
281 # width -> set it to its minimum width and allow the horizontal
282 # scrollbar to show.
27ed367c 283 self.SetColumnWidth(resizeCol-1, self._resizeColMinWidth)
d14a1e28
RD
284 return
285
286 # Resize the last column to take up the remaining available space.
287
27ed367c
RD
288 self.SetColumnWidth(resizeCol-1, listWidth - totColWidth)
289
d14a1e28
RD
290
291
292
293#----------------------------------------------------------------------------
294
b881fc78 295SEL_FOC = wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED
d14a1e28
RD
296def selectBeforePopup(event):
297 """Ensures the item the mouse is pointing at is selected before a popup.
298
299 Works with both single-select and multi-select lists."""
300 ctrl = event.GetEventObject()
8ab15340 301 if isinstance(ctrl, wx.ListCtrl):
d14a1e28
RD
302 n, flags = ctrl.HitTest(event.GetPosition())
303 if n >= 0:
b881fc78 304 if not ctrl.GetItemState(n, wx.LIST_STATE_SELECTED):
d14a1e28
RD
305 for i in range(ctrl.GetItemCount()):
306 ctrl.SetItemState(i, 0, SEL_FOC)
307 #for i in getListCtrlSelection(ctrl, SEL_FOC):
308 # ctrl.SetItemState(i, 0, SEL_FOC)
309 ctrl.SetItemState(n, SEL_FOC, SEL_FOC)
310
5841276a 311
b881fc78 312def getListCtrlSelection(listctrl, state=wx.LIST_STATE_SELECTED):
d14a1e28
RD
313 """ Returns list of item indexes of given state (selected by defaults) """
314 res = []
315 idx = -1
316 while 1:
b881fc78 317 idx = listctrl.GetNextItem(idx, wx.LIST_NEXT_ALL, state)
d14a1e28
RD
318 if idx == -1:
319 break
320 res.append(idx)
321 return res
322
b881fc78
RD
323wxEVT_DOPOPUPMENU = wx.NewEventType()
324EVT_DOPOPUPMENU = wx.PyEventBinder(wxEVT_DOPOPUPMENU, 0)
325
5841276a 326
d14a1e28
RD
327class ListCtrlSelectionManagerMix:
328 """Mixin that defines a platform independent selection policy
329
330 As selection single and multi-select list return the item index or a
331 list of item indexes respectively.
332 """
d14a1e28
RD
333 _menu = None
334
335 def __init__(self):
b881fc78
RD
336 self.Bind(wx.EVT_RIGHT_DOWN, self.OnLCSMRightDown)
337 self.Bind(EVT_DOPOPUPMENU, self.OnLCSMDoPopup)
338# self.Connect(-1, -1, self.wxEVT_DOPOPUPMENU, self.OnLCSMDoPopup)
d14a1e28 339
5841276a 340
d14a1e28
RD
341 def getPopupMenu(self):
342 """ Override to implement dynamic menus (create) """
343 return self._menu
344
5841276a 345
d14a1e28
RD
346 def setPopupMenu(self, menu):
347 """ Must be set for default behaviour """
348 self._menu = menu
349
5841276a 350
d14a1e28
RD
351 def afterPopupMenu(self, menu):
352 """ Override to implement dynamic menus (destroy) """
353 pass
354
5841276a 355
d14a1e28
RD
356 def getSelection(self):
357 res = getListCtrlSelection(self)
b881fc78 358 if self.GetWindowStyleFlag() & wx.LC_SINGLE_SEL:
d14a1e28
RD
359 if res:
360 return res[0]
361 else:
362 return -1
363 else:
364 return res
365
5841276a 366
d14a1e28
RD
367 def OnLCSMRightDown(self, event):
368 selectBeforePopup(event)
369 event.Skip()
370 menu = self.getPopupMenu()
371 if menu:
b881fc78
RD
372 evt = wx.PyEvent()
373 evt.SetEventType(wxEVT_DOPOPUPMENU)
d14a1e28
RD
374 evt.menu = menu
375 evt.pos = event.GetPosition()
b881fc78 376 wx.PostEvent(self, evt)
d14a1e28 377
5841276a 378
d14a1e28
RD
379 def OnLCSMDoPopup(self, event):
380 self.PopupMenu(event.menu, event.pos)
381 self.afterPopupMenu(event.menu)
382
5841276a
RD
383
384#----------------------------------------------------------------------------
385from bisect import bisect
386
387
388class TextEditMixin:
6d3c4b2a
RD
389 """
390 A mixin class that enables any text in any column of a
5841276a
RD
391 multi-column listctrl to be edited by clicking on the given row
392 and column. You close the text editor by hitting the ENTER key or
393 clicking somewhere else on the listctrl. You switch to the next
394 column by hiting TAB.
395
396 To use the mixin you have to include it in the class definition
397 and call the __init__ function::
398
6d3c4b2a 399 class TestListCtrl(wx.ListCtrl, TextEditMixin):
5841276a
RD
400 def __init__(self, parent, ID, pos=wx.DefaultPosition,
401 size=wx.DefaultSize, style=0):
402 wx.ListCtrl.__init__(self, parent, ID, pos, size, style)
6d3c4b2a 403 TextEditMixin.__init__(self)
5841276a
RD
404
405
406 Authors: Steve Zatz, Pim Van Heuven (pim@think-wize.com)
407 """
42a04f70
RD
408
409 editorBgColour = wx.Colour(255,255,175) # Yellow
410 editorFgColour = wx.Colour(0,0,0) # black
5841276a
RD
411
412 def __init__(self):
413 #editor = wx.TextCtrl(self, -1, pos=(-1,-1), size=(-1,-1),
414 # style=wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB \
415 # |wx.TE_RICH2)
15513a80
RD
416
417 self.make_editor()
418 self.Bind(wx.EVT_TEXT_ENTER, self.CloseEditor)
419 self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
420 self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDown)
421 self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected)
422
423
424 def make_editor(self, col_style=wx.LIST_FORMAT_LEFT):
5841276a 425 editor = wx.PreTextCtrl()
15513a80
RD
426
427 style =wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB|wx.TE_RICH2
27ed367c
RD
428 style |= {wx.LIST_FORMAT_LEFT: wx.TE_LEFT,
429 wx.LIST_FORMAT_RIGHT: wx.TE_RIGHT,
430 wx.LIST_FORMAT_CENTRE : wx.TE_CENTRE
431 }[col_style]
15513a80
RD
432
433 editor.Create(self, -1, style=style)
42a04f70
RD
434 editor.SetBackgroundColour(self.editorBgColour)
435 editor.SetForegroundColour(self.editorFgColour)
5841276a
RD
436 font = self.GetFont()
437 editor.SetFont(font)
438
15513a80
RD
439 self.curRow = 0
440 self.curCol = 0
441
442 editor.Hide()
5841276a 443 self.editor = editor
15513a80
RD
444
445 self.col_style = col_style
5841276a
RD
446 self.editor.Bind(wx.EVT_CHAR, self.OnChar)
447 self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor)
15513a80 448
5841276a
RD
449
450 def OnItemSelected(self, evt):
451 self.curRow = evt.GetIndex()
452 evt.Skip()
453
454
455 def OnChar(self, event):
15513a80
RD
456 ''' Catch the TAB, Shift-TAB, cursor DOWN/UP key code
457 so we can open the editor at the next column (if any).'''
458
459 keycode = event.GetKeyCode()
460 if keycode == wx.WXK_TAB and event.ShiftDown():
461 self.CloseEditor()
462 if self.curCol-1 >= 0:
463 self.OpenEditor(self.curCol-1, self.curRow)
464
465 elif keycode == wx.WXK_TAB:
5841276a
RD
466 self.CloseEditor()
467 if self.curCol+1 < self.GetColumnCount():
468 self.OpenEditor(self.curCol+1, self.curRow)
15513a80
RD
469
470 elif keycode == wx.WXK_ESCAPE:
471 self.CloseEditor()
472
473 elif keycode == wx.WXK_DOWN:
474 self.CloseEditor()
475 if self.curRow+1 < self.GetItemCount():
476 self._SelectIndex(self.curRow+1)
477 self.OpenEditor(self.curCol, self.curRow)
478
479 elif keycode == wx.WXK_UP:
480 self.CloseEditor()
481 if self.curRow > 0:
482 self._SelectIndex(self.curRow-1)
483 self.OpenEditor(self.curCol, self.curRow)
484
5841276a
RD
485 else:
486 event.Skip()
487
488
489 def OnLeftDown(self, evt=None):
490 ''' Examine the click and double
491 click events to see if a row has been click on twice. If so,
492 determine the current row and columnn and open the editor.'''
493
494 if self.editor.IsShown():
495 self.CloseEditor()
496
497 x,y = evt.GetPosition()
498 row,flags = self.HitTest((x,y))
499
500 if row != self.curRow: # self.curRow keeps track of the current row
501 evt.Skip()
502 return
503
504 # the following should really be done in the mixin's init but
505 # the wx.ListCtrl demo creates the columns after creating the
506 # ListCtrl (generally not a good idea) on the other hand,
507 # doing this here handles adjustable column widths
508
509 self.col_locs = [0]
510 loc = 0
511 for n in range(self.GetColumnCount()):
512 loc = loc + self.GetColumnWidth(n)
513 self.col_locs.append(loc)
15513a80 514
5841276a 515
15513a80 516 col = bisect(self.col_locs, x+self.GetScrollPos(wx.HORIZONTAL)) - 1
5841276a
RD
517 self.OpenEditor(col, row)
518
519
520 def OpenEditor(self, col, row):
521 ''' Opens an editor at the current position. '''
15513a80
RD
522
523 if self.GetColumn(col).m_format != self.col_style:
524 self.make_editor(self.GetColumn(col).m_format)
5841276a
RD
525
526 x0 = self.col_locs[col]
527 x1 = self.col_locs[col+1] - x0
528
15513a80
RD
529 scrolloffset = self.GetScrollPos(wx.HORIZONTAL)
530
27ed367c 531 # scroll forward
15513a80
RD
532 if x0+x1-scrolloffset > self.GetSize()[0]:
533 if wx.Platform == "__WXMSW__":
534 # don't start scrolling unless we really need to
535 offset = x0+x1-self.GetSize()[0]-scrolloffset
536 # scroll a bit more than what is minimum required
537 # so we don't have to scroll everytime the user presses TAB
538 # which is very tireing to the eye
539 addoffset = self.GetSize()[0]/4
540 # but be careful at the end of the list
541 if addoffset + scrolloffset < self.GetSize()[0]:
542 offset += addoffset
543
544 self.ScrollList(offset, 0)
545 scrolloffset = self.GetScrollPos(wx.HORIZONTAL)
546 else:
547 # Since we can not programmatically scroll the ListCtrl
548 # close the editor so the user can scroll and open the editor
549 # again
27ed367c
RD
550 self.editor.SetValue(self.GetItem(row, col).GetText())
551 self.curRow = row
552 self.curCol = col
15513a80
RD
553 self.CloseEditor()
554 return
555
5841276a
RD
556 y0 = self.GetItemRect(row)[1]
557
558 editor = self.editor
15513a80
RD
559 editor.SetDimensions(x0-scrolloffset,y0, x1,-1)
560
5841276a
RD
561 editor.SetValue(self.GetItem(row, col).GetText())
562 editor.Show()
563 editor.Raise()
564 editor.SetSelection(-1,-1)
565 editor.SetFocus()
566
567 self.curRow = row
568 self.curCol = col
569
570
42a04f70
RD
571 # FIXME: this function is usually called twice - second time because
572 # it is binded to wx.EVT_KILL_FOCUS. Can it be avoided? (MW)
5841276a
RD
573 def CloseEditor(self, evt=None):
574 ''' Close the editor and save the new value to the ListCtrl. '''
575 text = self.editor.GetValue()
576 self.editor.Hide()
42a04f70
RD
577 self.SetFocus()
578
579 # post wxEVT_COMMAND_LIST_END_LABEL_EDIT
580 # Event can be vetoed. It doesn't has SetEditCanceled(), what would
581 # require passing extra argument to CloseEditor()
582 evt = wx.ListEvent(wx.wxEVT_COMMAND_LIST_END_LABEL_EDIT, self.GetId())
583 evt.m_itemIndex = self.curRow
584 evt.m_col = self.curCol
585 item = self.GetItem(self.curRow, self.curCol)
586 evt.m_item.SetId(item.GetId())
587 evt.m_item.SetColumn(item.GetColumn())
588 evt.m_item.SetData(item.GetData())
589 evt.m_item.SetText(text) #should be empty string if editor was canceled
590 ret = self.GetEventHandler().ProcessEvent(evt)
591 if not ret or evt.IsAllowed():
592 if self.IsVirtual():
593 # replace by whather you use to populate the virtual ListCtrl
594 # data source
595 self.SetVirtualData(self.curRow, self.curCol, text)
596 else:
597 self.SetStringItem(self.curRow, self.curCol, text)
15513a80
RD
598 self.RefreshItem(self.curRow)
599
600 def _SelectIndex(self, row):
601 listlen = self.GetItemCount()
602 if row < 0 and not listlen:
603 return
604 if row > (listlen-1):
605 row = listlen -1
606
607 self.SetItemState(self.curRow, ~wx.LIST_STATE_SELECTED,
608 wx.LIST_STATE_SELECTED)
609 self.EnsureVisible(row)
610 self.SetItemState(row, wx.LIST_STATE_SELECTED,
611 wx.LIST_STATE_SELECTED)
5841276a
RD
612
613
614
615#----------------------------------------------------------------------------