]> git.saurik.com Git - wxWidgets.git/blob - wxPython/wx/lib/mixins/listctrl.py
Applied [ 1235509 ] wxCocoa unicode wxT("fixes")
[wxWidgets.git] / wxPython / wx / lib / mixins / listctrl.py
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 #----------------------------------------------------------------------------
12 # 12/14/2003 - Jeff Grimmett (grimmtooth@softhome.net)
13 #
14 # o 2.5 compatability 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
31 import locale
32 import wx
33
34 #----------------------------------------------------------------------------
35
36 class ColumnSorterMixin:
37 """
38 A mixin class that handles sorting of a wx.ListCtrl in REPORT mode when
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
44 returns the wx.ListCtrl to be sorted, and the list control
45 must exist at the time the wx.ColumnSorterMixin.__init__
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:
64 raise ValueError, "No wx.ListCtrl available"
65 list.Bind(wx.EVT_LIST_COL_CLICK, self.__OnColClick, list)
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
160 class ListCtrlAutoWidthMixin:
161 """ A mix-in class that automatically resizes the last column to take up
162 the remaining width of the wx.ListCtrl.
163
164 This causes the wx.ListCtrl to automatically take up the full width of
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
170 WARNING: If you override the EVT_SIZE event in your wx.ListCtrl, make
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>
175 """
176 def __init__(self):
177 """ Standard initialiser.
178 """
179 self._resizeColMinWidth = None
180 self._resizeColStyle = "LAST"
181 self._resizeCol = 0
182 self.Bind(wx.EVT_SIZE, self._onResize)
183 self.Bind(wx.EVT_LIST_COL_END_DRAG, self._onResize, self)
184
185
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
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
205 This method is called automatically when the wx.ListCtrl is resized;
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 """
212 self.resizeColumn(minWidth)
213
214
215 def resizeColumn(self, minWidth):
216 self._resizeColMinWidth = minWidth
217 self._doResize()
218
219
220 # =====================
221 # == Private Methods ==
222 # =====================
223
224 def _onResize(self, event):
225 """ Respond to the wx.ListCtrl being resized.
226
227 We automatically resize the last column in the list.
228 """
229 if 'gtk2' in wx.PlatformInfo:
230 self._doResize()
231 else:
232 wx.CallAfter(self._doResize)
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 """
248
249 if not self: # avoid a PyDeadObject error
250 return
251
252 numCols = self.GetColumnCount()
253 if numCols == 0: return # Nothing to resize.
254
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)
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
267 if wx.Platform != '__WXMSW__':
268 if self.GetItemCount() > self.GetCountPerPage():
269 scrollWidth = wx.SystemSettings_GetMetric(wx.SYS_VSCROLL_X)
270 listWidth = listWidth - scrollWidth
271
272 totColWidth = 0 # Width of all columns except last one.
273 for col in range(numCols):
274 if col != (resizeCol-1):
275 totColWidth = totColWidth + self.GetColumnWidth(col)
276
277 resizeColWidth = self.GetColumnWidth(resizeCol - 1)
278
279 if totColWidth + self._resizeColMinWidth > listWidth:
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.
283 self.SetColumnWidth(resizeCol-1, self._resizeColMinWidth)
284 return
285
286 # Resize the last column to take up the remaining available space.
287
288 self.SetColumnWidth(resizeCol-1, listWidth - totColWidth)
289
290
291
292
293 #----------------------------------------------------------------------------
294
295 SEL_FOC = wx.LIST_STATE_SELECTED | wx.LIST_STATE_FOCUSED
296 def 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()
301 if isinstance(ctrl, wx.ListCtrl):
302 n, flags = ctrl.HitTest(event.GetPosition())
303 if n >= 0:
304 if not ctrl.GetItemState(n, wx.LIST_STATE_SELECTED):
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
311
312 def getListCtrlSelection(listctrl, state=wx.LIST_STATE_SELECTED):
313 """ Returns list of item indexes of given state (selected by defaults) """
314 res = []
315 idx = -1
316 while 1:
317 idx = listctrl.GetNextItem(idx, wx.LIST_NEXT_ALL, state)
318 if idx == -1:
319 break
320 res.append(idx)
321 return res
322
323 wxEVT_DOPOPUPMENU = wx.NewEventType()
324 EVT_DOPOPUPMENU = wx.PyEventBinder(wxEVT_DOPOPUPMENU, 0)
325
326
327 class 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 """
333 _menu = None
334
335 def __init__(self):
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)
339
340
341 def getPopupMenu(self):
342 """ Override to implement dynamic menus (create) """
343 return self._menu
344
345
346 def setPopupMenu(self, menu):
347 """ Must be set for default behaviour """
348 self._menu = menu
349
350
351 def afterPopupMenu(self, menu):
352 """ Override to implement dynamic menus (destroy) """
353 pass
354
355
356 def getSelection(self):
357 res = getListCtrlSelection(self)
358 if self.GetWindowStyleFlag() & wx.LC_SINGLE_SEL:
359 if res:
360 return res[0]
361 else:
362 return -1
363 else:
364 return res
365
366
367 def OnLCSMRightDown(self, event):
368 selectBeforePopup(event)
369 event.Skip()
370 menu = self.getPopupMenu()
371 if menu:
372 evt = wx.PyEvent()
373 evt.SetEventType(wxEVT_DOPOPUPMENU)
374 evt.menu = menu
375 evt.pos = event.GetPosition()
376 wx.PostEvent(self, evt)
377
378
379 def OnLCSMDoPopup(self, event):
380 self.PopupMenu(event.menu, event.pos)
381 self.afterPopupMenu(event.menu)
382
383
384 #----------------------------------------------------------------------------
385 from bisect import bisect
386
387
388 class TextEditMixin:
389 """
390 A mixin class that enables any text in any column of a
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
399 class TestListCtrl(wx.ListCtrl, TextEditMixin):
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)
403 TextEditMixin.__init__(self)
404
405
406 Authors: Steve Zatz, Pim Van Heuven (pim@think-wize.com)
407 """
408
409 editorBgColour = wx.Colour(255,255,175) # Yellow
410 editorFgColour = wx.Colour(0,0,0) # black
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)
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):
425 editor = wx.PreTextCtrl()
426
427 style =wx.TE_PROCESS_ENTER|wx.TE_PROCESS_TAB|wx.TE_RICH2
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]
432
433 editor.Create(self, -1, style=style)
434 editor.SetBackgroundColour(self.editorBgColour)
435 editor.SetForegroundColour(self.editorFgColour)
436 font = self.GetFont()
437 editor.SetFont(font)
438
439 self.curRow = 0
440 self.curCol = 0
441
442 editor.Hide()
443 self.editor = editor
444
445 self.col_style = col_style
446 self.editor.Bind(wx.EVT_CHAR, self.OnChar)
447 self.editor.Bind(wx.EVT_KILL_FOCUS, self.CloseEditor)
448
449
450 def OnItemSelected(self, evt):
451 self.curRow = evt.GetIndex()
452 evt.Skip()
453
454
455 def OnChar(self, event):
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:
466 self.CloseEditor()
467 if self.curCol+1 < self.GetColumnCount():
468 self.OpenEditor(self.curCol+1, self.curRow)
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
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)
514
515
516 col = bisect(self.col_locs, x+self.GetScrollPos(wx.HORIZONTAL)) - 1
517 self.OpenEditor(col, row)
518
519
520 def OpenEditor(self, col, row):
521 ''' Opens an editor at the current position. '''
522
523 if self.GetColumn(col).m_format != self.col_style:
524 self.make_editor(self.GetColumn(col).m_format)
525
526 x0 = self.col_locs[col]
527 x1 = self.col_locs[col+1] - x0
528
529 scrolloffset = self.GetScrollPos(wx.HORIZONTAL)
530
531 # scroll forward
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
550 self.editor.SetValue(self.GetItem(row, col).GetText())
551 self.curRow = row
552 self.curCol = col
553 self.CloseEditor()
554 return
555
556 y0 = self.GetItemRect(row)[1]
557
558 editor = self.editor
559 editor.SetDimensions(x0-scrolloffset,y0, x1,-1)
560
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
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)
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()
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)
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)
612
613
614
615 #----------------------------------------------------------------------------