]>
Commit | Line | Data |
---|---|---|
30aaddfe 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 | #---------------------------------------------------------------------------- | |
12 | ||
13 | from wxPython.wx import * | |
1e4a197e | 14 | import locale |
30aaddfe RD |
15 | |
16 | #---------------------------------------------------------------------------- | |
17 | ||
18 | class wxColumnSorterMixin: | |
19 | """ | |
20 | A mixin class that handles sorting of a wxListCtrl in REPORT mode when | |
43edcf13 | 21 | the column header is clicked on. |
30aaddfe RD |
22 | |
23 | There are a few requirments needed in order for this to work genericly: | |
24 | ||
25 | 1. The combined class must have a GetListCtrl method that | |
26 | returns the wxListCtrl to be sorted, and the list control | |
27 | must exist at the time the wxColumnSorterMixin.__init__ | |
28 | method is called because it uses GetListCtrl. | |
29 | ||
30 | 2. Items in the list control must have a unique data value set | |
31 | with list.SetItemData. | |
32 | ||
33 | 3. The combined class must have an attribute named itemDataMap | |
34 | that is a dictionary mapping the data values to a sequence of | |
35 | objects representing the values in each column. These values | |
36 | are compared in the column sorter to determine sort order. | |
37 | ||
6d19860f RD |
38 | Interesting methods to override are GetColumnSorter, |
39 | GetSecondarySortValues, and GetSortImages. See below for details. | |
30aaddfe | 40 | """ |
6d19860f | 41 | |
30aaddfe | 42 | def __init__(self, numColumns): |
c85417f6 | 43 | self.SetColumnCount(numColumns) |
30aaddfe RD |
44 | list = self.GetListCtrl() |
45 | if not list: | |
46 | raise ValueError, "No wxListCtrl available" | |
6d19860f | 47 | EVT_LIST_COL_CLICK(list, list.GetId(), self.__OnColClick) |
30aaddfe RD |
48 | |
49 | ||
c85417f6 RD |
50 | def SetColumnCount(self, newNumColumns): |
51 | self._colSortFlag = [0] * newNumColumns | |
52 | self._col = -1 | |
53 | ||
54 | ||
30aaddfe RD |
55 | def SortListItems(self, col=-1, ascending=1): |
56 | """Sort the list on demand. Can also be used to set the sort column and order.""" | |
6d19860f | 57 | oldCol = self._col |
30aaddfe RD |
58 | if col != -1: |
59 | self._col = col | |
60 | self._colSortFlag[col] = ascending | |
6d19860f RD |
61 | self.GetListCtrl().SortItems(self.GetColumnSorter()) |
62 | self.__updateImages(oldCol) | |
63 | ||
64 | ||
65 | def GetColumnWidths(self): | |
66 | """ | |
67 | Returns a list of column widths. Can be used to help restore the current | |
68 | view later. | |
69 | """ | |
70 | list = self.GetListCtrl() | |
71 | rv = [] | |
72 | for x in range(len(self._colSortFlag)): | |
73 | rv.append(list.GetColumnWidth(x)) | |
74 | return rv | |
75 | ||
76 | ||
77 | def GetSortImages(self): | |
78 | """ | |
79 | Returns a tuple of image list indexesthe indexes in the image list for an image to be put on the column | |
80 | header when sorting in descending order. | |
81 | """ | |
82 | return (-1, -1) # (decending, ascending) image IDs | |
83 | ||
84 | ||
85 | def GetColumnSorter(self): | |
86 | """Returns a callable object to be used for comparing column values when sorting.""" | |
87 | return self.__ColumnSorter | |
30aaddfe RD |
88 | |
89 | ||
6d19860f RD |
90 | def GetSecondarySortValues(self, col, key1, key2): |
91 | """Returns a tuple of 2 values to use for secondary sort values when the | |
92 | items in the selected column match equal. The default just returns the | |
93 | item data values.""" | |
94 | return (key1, key2) | |
95 | ||
96 | ||
97 | def __OnColClick(self, evt): | |
98 | oldCol = self._col | |
99 | self._col = col = evt.GetColumn() | |
30aaddfe | 100 | self._colSortFlag[col] = not self._colSortFlag[col] |
6d19860f RD |
101 | self.GetListCtrl().SortItems(self.GetColumnSorter()) |
102 | self.__updateImages(oldCol) | |
103 | evt.Skip() | |
30aaddfe RD |
104 | |
105 | ||
6d19860f | 106 | def __ColumnSorter(self, key1, key2): |
30aaddfe | 107 | col = self._col |
6d19860f | 108 | ascending = self._colSortFlag[col] |
30aaddfe RD |
109 | item1 = self.itemDataMap[key1][col] |
110 | item2 = self.itemDataMap[key2][col] | |
6d19860f | 111 | |
1e4a197e RD |
112 | #--- Internationalization of string sorting with locale module |
113 | if type(item1) == type('') or type(item2) == type(''): | |
114 | cmpVal = locale.strcoll(str(item1), str(item2)) | |
115 | else: | |
116 | cmpVal = cmp(item1, item2) | |
117 | #--- | |
6d19860f RD |
118 | |
119 | # If the items are equal then pick something else to make the sort value unique | |
120 | if cmpVal == 0: | |
121 | cmpVal = apply(cmp, self.GetSecondarySortValues(col, key1, key2)) | |
122 | ||
123 | if ascending: | |
124 | return cmpVal | |
30aaddfe | 125 | else: |
6d19860f | 126 | return -cmpVal |
30aaddfe RD |
127 | |
128 | ||
6d19860f RD |
129 | def __updateImages(self, oldCol): |
130 | sortImages = self.GetSortImages() | |
131 | if self._col != -1 and sortImages[0] != -1: | |
132 | img = sortImages[self._colSortFlag[self._col]] | |
133 | list = self.GetListCtrl() | |
134 | if oldCol != -1: | |
135 | list.ClearColumnImage(oldCol) | |
136 | list.SetColumnImage(self._col, img) | |
137 | ||
30aaddfe RD |
138 | |
139 | #---------------------------------------------------------------------------- | |
726fc00a RD |
140 | #---------------------------------------------------------------------------- |
141 | ||
142 | class wxListCtrlAutoWidthMixin: | |
143 | """ A mix-in class that automatically resizes the last column to take up | |
1e4a197e | 144 | the remaining width of the wxListCtrl. |
726fc00a | 145 | |
1e4a197e RD |
146 | This causes the wxListCtrl to automatically take up the full width of |
147 | the list, without either a horizontal scroll bar (unless absolutely | |
148 | necessary) or empty space to the right of the last column. | |
726fc00a | 149 | |
1e4a197e | 150 | NOTE: This only works for report-style lists. |
726fc00a | 151 | |
1e4a197e RD |
152 | WARNING: If you override the EVT_SIZE event in your wxListCtrl, make |
153 | sure you call event.Skip() to ensure that the mixin's | |
154 | _OnResize method is called. | |
726fc00a | 155 | |
1e4a197e | 156 | This mix-in class was written by Erik Westra <ewestra@wave.co.nz> |
726fc00a RD |
157 | """ |
158 | def __init__(self): | |
1e4a197e RD |
159 | """ Standard initialiser. |
160 | """ | |
161 | self._lastColMinWidth = None | |
726fc00a | 162 | |
1e4a197e RD |
163 | EVT_SIZE(self, self._onResize) |
164 | EVT_LIST_COL_END_DRAG(self, self.GetId(), self._onResize) | |
68bc8549 | 165 | |
726fc00a RD |
166 | |
167 | def resizeLastColumn(self, minWidth): | |
1e4a197e | 168 | """ Resize the last column appropriately. |
726fc00a | 169 | |
1e4a197e RD |
170 | If the list's columns are too wide to fit within the window, we use |
171 | a horizontal scrollbar. Otherwise, we expand the right-most column | |
172 | to take up the remaining free space in the list. | |
726fc00a | 173 | |
1e4a197e RD |
174 | This method is called automatically when the wxListCtrl is resized; |
175 | you can also call it yourself whenever you want the last column to | |
176 | be resized appropriately (eg, when adding, removing or resizing | |
177 | columns). | |
726fc00a | 178 | |
1e4a197e RD |
179 | 'minWidth' is the preferred minimum width for the last column. |
180 | """ | |
181 | self._lastColMinWidth = minWidth | |
182 | self._doResize() | |
726fc00a RD |
183 | |
184 | # ===================== | |
185 | # == Private Methods == | |
186 | # ===================== | |
30aaddfe | 187 | |
726fc00a | 188 | def _onResize(self, event): |
1e4a197e | 189 | """ Respond to the wxListCtrl being resized. |
30aaddfe | 190 | |
1e4a197e RD |
191 | We automatically resize the last column in the list. |
192 | """ | |
193 | wxCallAfter(self._doResize) | |
accfe34d | 194 | event.Skip() |
30aaddfe | 195 | |
68bc8549 | 196 | |
1e4a197e RD |
197 | def _doResize(self): |
198 | """ Resize the last column as appropriate. | |
68bc8549 | 199 | |
1e4a197e RD |
200 | If the list's columns are too wide to fit within the window, we use |
201 | a horizontal scrollbar. Otherwise, we expand the right-most column | |
202 | to take up the remaining free space in the list. | |
68bc8549 | 203 | |
1e4a197e RD |
204 | We remember the current size of the last column, before resizing, |
205 | as the preferred minimum width if we haven't previously been given | |
206 | or calculated a minimum width. This ensure that repeated calls to | |
207 | _doResize() don't cause the last column to size itself too large. | |
208 | """ | |
209 | numCols = self.GetColumnCount() | |
210 | if numCols == 0: return # Nothing to resize. | |
68bc8549 | 211 | |
1e4a197e RD |
212 | if self._lastColMinWidth == None: |
213 | self._lastColMinWidth = self.GetColumnWidth(numCols - 1) | |
68bc8549 | 214 | |
1e4a197e RD |
215 | # We're showing the vertical scrollbar -> allow for scrollbar width |
216 | # NOTE: on GTK, the scrollbar is included in the client size, but on | |
217 | # Windows it is not included | |
218 | listWidth = self.GetClientSize().width | |
219 | if wxPlatform != '__WXMSW__': | |
220 | if self.GetItemCount() > self.GetCountPerPage(): | |
221 | scrollWidth = wxSystemSettings_GetSystemMetric(wxSYS_VSCROLL_X) | |
222 | listWidth = listWidth - scrollWidth | |
68bc8549 | 223 | |
1e4a197e RD |
224 | totColWidth = 0 # Width of all columns except last one. |
225 | for col in range(numCols-1): | |
226 | totColWidth = totColWidth + self.GetColumnWidth(col) | |
227 | ||
228 | lastColWidth = self.GetColumnWidth(numCols - 1) | |
726fc00a | 229 | |
1e4a197e RD |
230 | if totColWidth + self._lastColMinWidth > listWidth: |
231 | # We haven't got the width to show the last column at its minimum | |
232 | # width -> set it to its minimum width and allow the horizontal | |
233 | # scrollbar to show. | |
234 | self.SetColumnWidth(numCols-1, self._lastColMinWidth) | |
235 | return | |
726fc00a | 236 | |
1e4a197e | 237 | # Resize the last column to take up the remaining available space. |
726fc00a | 238 | |
1e4a197e | 239 | self.SetColumnWidth(numCols-1, listWidth - totColWidth) |
726fc00a RD |
240 | |
241 | ||
242 | ||
243 | #---------------------------------------------------------------------------- | |
17571217 RD |
244 | |
245 | SEL_FOC = wxLIST_STATE_SELECTED | wxLIST_STATE_FOCUSED | |
246 | def selectBeforePopup(event): | |
247 | """Ensures the item the mouse is pointing at is selected before a popup. | |
248 | ||
249 | Works with both single-select and multi-select lists.""" | |
250 | ctrl = event.GetEventObject() | |
251 | if isinstance(ctrl, wxListCtrl): | |
252 | n, flags = ctrl.HitTest(event.GetPosition()) | |
253 | if n >= 0: | |
254 | if not ctrl.GetItemState(n, wxLIST_STATE_SELECTED): | |
255 | for i in range(ctrl.GetItemCount()): | |
256 | ctrl.SetItemState(i, 0, SEL_FOC) | |
257 | #for i in getListCtrlSelection(ctrl, SEL_FOC): | |
258 | # ctrl.SetItemState(i, 0, SEL_FOC) | |
259 | ctrl.SetItemState(n, SEL_FOC, SEL_FOC) | |
260 | ||
261 | def getListCtrlSelection(listctrl, state=wxLIST_STATE_SELECTED): | |
262 | """ Returns list of item indexes of given state (selected by defaults) """ | |
263 | res = [] | |
264 | idx = -1 | |
265 | while 1: | |
266 | idx = listctrl.GetNextItem(idx, wxLIST_NEXT_ALL, state) | |
267 | if idx == -1: | |
268 | break | |
269 | res.append(idx) | |
270 | return res | |
271 | ||
272 | class ListCtrlSelectionManagerMix: | |
273 | """Mixin that defines a platform independent selection policy | |
274 | ||
275 | As selection single and multi-select list return the item index or a | |
276 | list of item indexes respectively. | |
277 | """ | |
278 | wxEVT_DOPOPUPMENU = wxNewId() | |
279 | _menu = None | |
280 | ||
281 | def __init__(self): | |
282 | EVT_RIGHT_DOWN(self, self.OnLCSMRightDown) | |
283 | self.Connect(-1, -1, self.wxEVT_DOPOPUPMENU, self.OnLCSMDoPopup) | |
284 | ||
285 | def getPopupMenu(self): | |
286 | """ Override to implement dynamic menus (create) """ | |
287 | return self._menu | |
288 | ||
289 | def setPopupMenu(self, menu): | |
290 | """ Must be set for default behaviour """ | |
291 | self._menu = menu | |
292 | ||
293 | def afterPopupMenu(self, menu): | |
294 | """ Override to implement dynamic menus (destroy) """ | |
295 | pass | |
296 | ||
297 | def getSelection(self): | |
298 | res = getListCtrlSelection(self) | |
299 | if self.GetWindowStyleFlag() & wxLC_SINGLE_SEL: | |
300 | if res: | |
301 | return res[0] | |
302 | else: | |
303 | return -1 | |
304 | else: | |
305 | return res | |
306 | ||
307 | def OnLCSMRightDown(self, event): | |
308 | selectBeforePopup(event) | |
309 | event.Skip() | |
310 | menu = self.getPopupMenu() | |
311 | if menu: | |
312 | evt = wxPyEvent() | |
313 | evt.SetEventType(self.wxEVT_DOPOPUPMENU) | |
314 | evt.menu = menu | |
315 | evt.pos = event.GetPosition() | |
316 | wxPostEvent(self, evt) | |
317 | ||
318 | def OnLCSMDoPopup(self, event): | |
319 | self.PopupMenu(event.menu, event.pos) | |
320 | self.afterPopupMenu(event.menu) | |
321 | ||
322 | #---------------------------------------------------------------------- |