]>
Commit | Line | Data |
---|---|---|
4f708f05 RD |
1 | """ |
2 | ComboTreeBox provides a ComboBox that pops up a tree instead of a list. | |
3 | ||
4 | ComboTreeBox tries to provide the same interface as ComboBox as much as | |
5 | possible. However, whereas the ComboBox widget uses indices to access | |
6 | items in the list of choices, ComboTreeBox uses TreeItemId's instead. If | |
7 | you add an item to the ComboTreeBox (using Append or Insert), the | |
8 | TreeItemId associated with the added item is returned. You can then use | |
9 | that TreeItemId to add items as children of that first item. For | |
10 | example: | |
11 | ||
12 | >>> from wx.lib.combotreebox import ComboTreeBox | |
13 | >>> combo = ComboTreeBox(parent) | |
14 | >>> item1 = combo.Append('Item 1') # Add a root item | |
15 | >>> item1a = combo.Append('Item 1a', parent=item1) # Add a child to item1 | |
16 | ||
17 | You can also add client data to each of the items like this: | |
18 | >>> item1 = combo.Append('Item 1', clientData=somePythonObject) | |
19 | >>> item1a = combo.Append('Item 1a', parent=item1, | |
20 | ... clientData=someOtherPythonObject) | |
21 | ||
22 | And later fetch the client data like this: | |
23 | >>> somePythonObject = combo.GetClientData(item1) | |
24 | ||
25 | To get the client data of the currently selected item (if any): | |
26 | >>> currentItem = combo.GetSelection() | |
27 | >>> if currentItem: | |
28 | >>> somePythonObject = combo.GetClientData(currentItem) | |
29 | ||
30 | Supported styles are the same as for ComboBox, i.e. wx.CB_READONLY and | |
31 | wx.CB_SORT. Provide them as usual: | |
32 | >>> combo = ComboTreeBox(parent, style=wx.CB_READONLY|wx.CB_SORT) | |
33 | ||
34 | Supported platforms: wxMSW and wxMAC natively, wxGTK by means of a | |
35 | workaround. | |
36 | ||
37 | Author: Frank Niessink <frank@niessink.com> | |
38 | Copyright 2006, Frank Niessink | |
39 | License: wxWidgets license | |
40 | Version: 0.8 | |
41 | Date: August 21, 2006 | |
42 | """ | |
43 | ||
44 | import wx | |
45 | ||
46 | __all__ = ['ComboTreeBox'] # Export only the ComboTreeBox widget | |
47 | ||
48 | ||
49 | # --------------------------------------------------------------------------- | |
50 | ||
51 | ||
52 | class IterableTreeCtrl(wx.TreeCtrl): | |
53 | """ | |
54 | TreeCtrl is the same as wx.TreeCtrl, with a few convenience methods | |
55 | added for easier navigation of items. """ | |
56 | ||
57 | def GetPreviousItem(self, item): | |
58 | """ | |
59 | GetPreviousItem(self, TreeItemId item) -> TreeItemId | |
60 | ||
61 | Returns the item that is on the line immediately above item | |
62 | (as is displayed when the tree is fully expanded). The returned | |
63 | item is invalid if item is the first item in the tree. | |
64 | """ | |
65 | previousSibling = self.GetPrevSibling(item) | |
66 | if previousSibling: | |
67 | return self.GetLastChildRecursively(previousSibling) | |
68 | else: | |
69 | parent = self.GetItemParent(item) | |
70 | if parent == self.GetRootItem() and \ | |
71 | (self.GetWindowStyle() & wx.TR_HIDE_ROOT): | |
72 | # Return an invalid item, because the root item is hidden | |
73 | return previousSibling | |
74 | else: | |
75 | return parent | |
76 | ||
77 | def GetNextItem(self, item): | |
78 | """ | |
79 | GetNextItem(self, TreeItemId item) -> TreeItemId | |
80 | ||
81 | Returns the item that is on the line immediately below item | |
82 | (as is displayed when the tree is fully expanded). The returned | |
83 | item is invalid if item is the last item in the tree. | |
84 | """ | |
85 | if self.ItemHasChildren(item): | |
86 | firstChild, cookie = self.GetFirstChild(item) | |
87 | return firstChild | |
88 | else: | |
89 | return self.GetNextSiblingRecursively(item) | |
90 | ||
91 | def GetFirstItem(self): | |
92 | """ | |
93 | GetFirstItem(self) -> TreeItemId | |
94 | ||
95 | Returns the very first item in the tree. This is the root item | |
96 | unless the root item is hidden. In that case the first child of | |
97 | the root item is returned, if any. If the tree is empty, an | |
98 | invalid tree item is returned. | |
99 | """ | |
100 | rootItem = self.GetRootItem() | |
101 | if rootItem and (self.GetWindowStyle() & wx.TR_HIDE_ROOT): | |
102 | firstChild, cookie = self.GetFirstChild(rootItem) | |
103 | return firstChild | |
104 | else: | |
105 | return rootItem | |
106 | ||
107 | def GetLastChildRecursively(self, item): | |
108 | """ | |
109 | GetLastChildRecursively(self, TreeItemId item) -> TreeItemId | |
110 | ||
111 | Returns the last child of the last child ... of item. If item | |
112 | has no children, item itself is returned. So the returned item | |
113 | is always valid, assuming a valid item has been passed. | |
114 | """ | |
115 | lastChild = item | |
116 | while self.ItemHasChildren(lastChild): | |
117 | lastChild = self.GetLastChild(lastChild) | |
118 | return lastChild | |
119 | ||
120 | def GetNextSiblingRecursively(self, item): | |
121 | """ | |
122 | GetNextSiblingRecursively(self, TreeItemId item) -> TreeItemId | |
123 | ||
124 | Returns the next sibling of item if it has one. If item has no | |
125 | next sibling the next sibling of the parent of item is returned. | |
126 | If the parent has no next sibling the next sibling of the parent | |
127 | of the parent is returned, etc. If none of the ancestors of item | |
128 | has a next sibling, an invalid item is returned. | |
129 | """ | |
130 | if item == self.GetRootItem(): | |
131 | return wx.TreeItemId() # Return an invalid TreeItemId | |
132 | nextSibling = self.GetNextSibling(item) | |
133 | if nextSibling: | |
134 | return nextSibling | |
135 | else: | |
136 | parent = self.GetItemParent(item) | |
137 | return self.GetNextSiblingRecursively(parent) | |
138 | ||
139 | def GetSelection(self): | |
140 | """ Extend GetSelection to never return the root item if the | |
141 | root item is hidden. """ | |
142 | selection = super(IterableTreeCtrl, self).GetSelection() | |
143 | if selection == self.GetRootItem() and \ | |
144 | (self.GetWindowStyle() & wx.TR_HIDE_ROOT): | |
145 | return wx.TreeItemId() # Return an invalid TreeItemId | |
146 | else: | |
147 | return selection | |
148 | ||
149 | ||
150 | # --------------------------------------------------------------------------- | |
151 | ||
152 | ||
153 | class BasePopupFrame(wx.MiniFrame): | |
154 | """ | |
155 | BasePopupFrame is the base class for platform specific | |
156 | versions of the PopupFrame. The PopupFrame is the frame that | |
157 | is popped up by ComboTreeBox. It contains the tree of items | |
158 | that the user can select one item from. Upon selection, or | |
159 | when focus is lost, the frame is hidden. """ | |
160 | ||
161 | def __init__(self, parent): | |
162 | super(BasePopupFrame, self).__init__(parent, | |
163 | style=wx.DEFAULT_FRAME_STYLE & wx.FRAME_FLOAT_ON_PARENT & | |
164 | ~(wx.RESIZE_BORDER | wx.CAPTION)) | |
165 | self._createInterior() | |
166 | self._layoutInterior() | |
167 | self._bindEventHandlers() | |
168 | ||
169 | def _createInterior(self): | |
170 | self._tree = IterableTreeCtrl(self, | |
171 | style=wx.TR_HIDE_ROOT|wx.TR_LINES_AT_ROOT|wx.TR_HAS_BUTTONS) | |
172 | self._tree.AddRoot('Hidden root node') | |
173 | ||
174 | def _layoutInterior(self): | |
175 | frameSizer = wx.BoxSizer(wx.HORIZONTAL) | |
176 | frameSizer.Add(self._tree, flag=wx.EXPAND, proportion=1) | |
177 | self.SetSizerAndFit(frameSizer) | |
178 | ||
179 | def _bindEventHandlers(self): | |
180 | self._tree.Bind(wx.EVT_CHAR, self.OnChar) | |
181 | self._tree.Bind(wx.EVT_TREE_ITEM_ACTIVATED, self.OnItemActivated) | |
182 | self._tree.Bind(wx.EVT_LEFT_DOWN, self.OnMouseClick) | |
183 | ||
184 | def _bindKillFocus(self): | |
185 | self._tree.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus) | |
186 | ||
187 | def _unbindKillFocus(self): | |
188 | self._tree.Unbind(wx.EVT_KILL_FOCUS) | |
189 | ||
190 | def OnKillFocus(self, event): | |
191 | # We hide the frame rather than destroy it, so it can be | |
192 | # popped up again later: | |
193 | self.Hide() | |
194 | self.GetParent().NotifyNoItemSelected() | |
195 | event.Skip() | |
196 | ||
197 | def OnChar(self, keyEvent): | |
198 | if self._keyShouldHidePopup(keyEvent): | |
199 | self.Hide() | |
200 | self.GetParent().NotifyNoItemSelected() | |
201 | keyEvent.Skip() | |
202 | ||
203 | def _keyShouldHidePopup(self, keyEvent): | |
204 | return keyEvent.GetKeyCode() == wx.WXK_ESCAPE | |
205 | ||
206 | def OnMouseClick(self, event): | |
207 | item, flags = self._tree.HitTest(event.GetPosition()) | |
208 | if item and (flags & wx.TREE_HITTEST_ONITEMLABEL): | |
209 | self._tree.SelectItem(item) | |
210 | self.Hide() | |
211 | self.GetParent().NotifyItemSelected(self._tree.GetItemText(item)) | |
212 | else: | |
213 | event.Skip() | |
214 | ||
215 | def OnItemActivated(self, event): | |
216 | item = event.GetItem() | |
217 | self.Hide() | |
218 | self.GetParent().NotifyItemSelected(self._tree.GetItemText(item)) | |
219 | ||
220 | def Show(self): | |
221 | self._bindKillFocus() | |
222 | wx.CallAfter(self._tree.SetFocus) | |
223 | super(BasePopupFrame, self).Show() | |
224 | ||
225 | def Hide(self): | |
226 | self._unbindKillFocus() | |
227 | super(BasePopupFrame, self).Hide() | |
228 | ||
229 | def GetTree(self): | |
230 | return self._tree | |
231 | ||
232 | ||
233 | class MSWPopupFrame(BasePopupFrame): | |
234 | def Show(self): | |
235 | # Comply with the MS Windows Combobox behaviour: if the text in | |
236 | # the text field is not in the tree, the first item in the tree | |
237 | # is selected. | |
238 | if not self._tree.GetSelection(): | |
239 | self._tree.SelectItem(self._tree.GetFirstItem()) | |
240 | super(MSWPopupFrame, self).Show() | |
241 | ||
242 | ||
243 | class MACPopupFrame(BasePopupFrame): | |
244 | def _bindKillFocus(self): | |
245 | # On wxMac, the kill focus event doesn't work, but the | |
246 | # deactivate event does: | |
247 | self.Bind(wx.EVT_ACTIVATE, self.OnKillFocus) | |
248 | ||
249 | def _unbindKillFocus(self): | |
250 | self.Unbind(wx.EVT_ACTIVATE) | |
251 | ||
252 | def OnKillFocus(self, event): | |
253 | if not event.GetActive(): # We received a deactivate event | |
254 | self.Hide() | |
255 | wx.CallAfter(self.GetParent().NotifyNoItemSelected) | |
256 | event.Skip() | |
257 | ||
258 | ||
259 | class GTKPopupFrame(BasePopupFrame): | |
260 | def _keyShouldHidePopup(self, keyEvent): | |
261 | # On wxGTK, Alt-Up also closes the popup: | |
262 | return super(GTKPopupFrame, self)._keyShouldHidePopup(keyEvent) or \ | |
263 | (keyEvent.AltDown() and keyEvent.GetKeyCode() == wx.WXK_UP) | |
264 | ||
265 | ||
266 | # --------------------------------------------------------------------------- | |
267 | ||
268 | ||
269 | class BaseComboTreeBox(object): | |
270 | """ BaseComboTreeBox is the base class for platform specific | |
271 | versions of the ComboTreeBox. """ | |
272 | ||
273 | def __init__(self, *args, **kwargs): | |
274 | style = kwargs.pop('style', 0) | |
275 | if style & wx.CB_READONLY: | |
276 | style &= ~wx.CB_READONLY # We manage readonlyness ourselves | |
277 | self._readOnly = True | |
278 | else: | |
279 | self._readOnly = False | |
280 | if style & wx.CB_SORT: | |
281 | style &= ~wx.CB_SORT # We manage sorting ourselves | |
282 | self._sort = True | |
283 | else: | |
284 | self._sort = False | |
285 | super(BaseComboTreeBox, self).__init__(style=style, *args, **kwargs) | |
286 | self._createInterior() | |
287 | self._layoutInterior() | |
288 | self._bindEventHandlers() | |
289 | ||
290 | # Methods to construct the widget. | |
291 | ||
292 | def _createInterior(self): | |
293 | self._popupFrame = self._createPopupFrame() | |
294 | self._text = self._createTextCtrl() | |
295 | self._button = self._createButton() | |
296 | self._tree = self._popupFrame.GetTree() | |
297 | ||
298 | def _createTextCtrl(self): | |
299 | return self # By default, the text control is the control itself. | |
300 | ||
301 | def _createButton(self): | |
302 | return self # By default, the dropdown button is the control itself. | |
303 | ||
304 | def _createPopupFrame(self): | |
305 | # It is a subclass responsibility to provide the right PopupFrame, | |
306 | # depending on platform: | |
307 | raise NotImplementedError | |
308 | ||
309 | def _layoutInterior(self): | |
310 | pass # By default, there is no layout to be done. | |
311 | ||
312 | def _bindEventHandlers(self): | |
313 | for eventSource, eventType, eventHandler in self._eventsToBind(): | |
314 | eventSource.Bind(eventType, eventHandler) | |
315 | ||
316 | def _eventsToBind(self): | |
317 | """ | |
318 | _eventsToBind(self) -> [(eventSource, eventType, eventHandler), ...] | |
319 | ||
320 | _eventsToBind returns a list of eventSource, eventType, | |
321 | eventHandlers tuples that will be bound. This method can be | |
322 | extended to bind additional events. In that case, don't | |
323 | forget to call _eventsToBind on the super class. """ | |
324 | return [(self._text, wx.EVT_KEY_DOWN, self.OnKeyDown), | |
325 | (self._text, wx.EVT_TEXT, self.OnText), | |
326 | (self._button, wx.EVT_BUTTON, self.OnMouseClick)] | |
327 | ||
328 | # Event handlers | |
329 | ||
330 | def OnMouseClick(self, event): | |
331 | self.Popup() | |
332 | # Note that we don't call event.Skip() to prevent popping up the | |
333 | # ComboBox's own box. | |
334 | ||
335 | def OnKeyDown(self, keyEvent): | |
336 | if self._keyShouldNavigate(keyEvent): | |
337 | self._navigateUpOrDown(keyEvent) | |
338 | elif self._keyShouldPopUpTree(keyEvent): | |
339 | self.Popup() | |
340 | else: | |
341 | keyEvent.Skip() | |
342 | ||
343 | def _keyShouldPopUpTree(self, keyEvent): | |
344 | return (keyEvent.AltDown() or keyEvent.MetaDown()) and \ | |
345 | keyEvent.GetKeyCode() == wx.WXK_DOWN | |
346 | ||
347 | def _keyShouldNavigate(self, keyEvent): | |
348 | return keyEvent.GetKeyCode() in (wx.WXK_DOWN, wx.WXK_UP) and not \ | |
349 | self._keyShouldPopUpTree(keyEvent) | |
350 | ||
351 | def _navigateUpOrDown(self, keyEvent): | |
352 | item = self.GetSelection() | |
353 | if item: | |
354 | navigationMethods = {wx.WXK_DOWN: self._tree.GetNextItem, | |
355 | wx.WXK_UP: self._tree.GetPreviousItem} | |
356 | getNextItem = navigationMethods[keyEvent.GetKeyCode()] | |
357 | nextItem = getNextItem(item) | |
358 | else: | |
359 | nextItem = self._tree.GetFirstItem() | |
360 | if nextItem: | |
361 | self.SetSelection(nextItem) | |
362 | ||
363 | def OnText(self, event): | |
364 | event.Skip() | |
365 | item = self.FindString(self._text.GetValue()) | |
366 | if item: | |
367 | if self._tree.GetSelection() != item: | |
368 | self._tree.SelectItem(item) | |
369 | else: | |
370 | self._tree.Unselect() | |
371 | ||
372 | # Methods called by the PopupFrame, to let the ComboTreeBox know | |
373 | # about what the user did. | |
374 | ||
375 | def NotifyItemSelected(self, text): | |
376 | """ Simulate selection of an item by the user. This is meant to | |
377 | be called by the PopupFrame when the user selects an item. """ | |
378 | self._text.SetValue(text) | |
379 | self._postComboBoxSelectedEvent(text) | |
380 | self.SetFocus() | |
381 | ||
382 | def _postComboBoxSelectedEvent(self, text): | |
383 | """ Simulate a selection event. """ | |
384 | event = wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED, | |
385 | self.GetId()) | |
386 | event.SetString(text) | |
387 | self.GetEventHandler().ProcessEvent(event) | |
388 | ||
389 | def NotifyNoItemSelected(self): | |
390 | """ This is called by the PopupFrame when the user closes the | |
391 | PopupFrame, without selecting an item. """ | |
392 | self.SetFocus() | |
393 | ||
394 | # Misc methods, not part of the ComboBox API. | |
395 | ||
396 | def Popup(self): | |
397 | """ | |
398 | Popup(self) | |
399 | ||
400 | Pops up the frame with the tree. | |
401 | """ | |
402 | comboBoxSize = self.GetSize() | |
403 | x, y = self.GetParent().ClientToScreen(self.GetPosition()) | |
404 | y += comboBoxSize[1] | |
405 | width = comboBoxSize[0] | |
406 | height = 300 | |
407 | self._popupFrame.SetDimensions(x, y, width, height) | |
408 | # On wxGTK, when the Combobox width has been increased a call | |
409 | # to SetMinSize is needed to force a resize of the popupFrame: | |
410 | self._popupFrame.SetMinSize((width, height)) | |
411 | self._popupFrame.Show() | |
412 | ||
413 | def GetTree(self): | |
414 | """ | |
415 | GetTree(self) -> wx.TreeCtrl | |
416 | ||
417 | Returns the tree control that is popped up. | |
418 | """ | |
419 | return self._popupFrame.GetTree() | |
420 | ||
421 | def FindClientData(self, clientData, parent=None): | |
422 | """ | |
423 | FindClientData(self, PyObject clientData, TreeItemId parent=None) | |
424 | -> TreeItemId | |
425 | ||
426 | Finds the *first* item in the tree with client data equal to the | |
427 | given clientData. If no such item exists, an invalid item is | |
428 | returned. | |
429 | """ | |
430 | parent = parent or self._tree.GetRootItem() | |
431 | child, cookie = self._tree.GetFirstChild(parent) | |
432 | while child: | |
433 | if self.GetClientData(child) == clientData: | |
434 | return child | |
435 | else: | |
436 | result = self.FindClientData(clientData, child) | |
437 | if result: | |
438 | return result | |
439 | child, cookie = self._tree.GetNextChild(parent, cookie) | |
440 | return child | |
441 | ||
442 | def SetClientDataSelection(self, clientData): | |
443 | """ | |
444 | SetClientDataSelection(self, PyObject clientData) -> bool | |
445 | ||
446 | Selects the item with the provided clientData in the control. | |
447 | Returns True if the item belonging to the clientData has been | |
448 | selected, False if it wasn't found in the control. | |
449 | """ | |
450 | item = self.FindClientData(clientData) | |
451 | if item: | |
452 | self._tree.SelectItem(item) | |
453 | return True | |
454 | else: | |
455 | return False | |
456 | ||
457 | # The following methods are all part of the ComboBox API (actually | |
458 | # the ControlWithItems API) and have been adapted to take TreeItemIds | |
459 | # as parameter and return TreeItemIds, rather than indices. | |
460 | ||
461 | def Append(self, itemText, parent=None, clientData=None): | |
462 | """ | |
463 | Append(self, String itemText, TreeItemId parent=None, PyObject | |
464 | clientData=None) -> TreeItemId | |
465 | ||
466 | Adds the itemText to the control, associating the given clientData | |
467 | with the item if not None. If parent is None, itemText is added | |
468 | as a root item, else itemText is added as a child item of | |
469 | parent. The return value is the TreeItemId of the newly added | |
470 | item. """ | |
471 | if parent is None: | |
472 | parent = self._tree.GetRootItem() | |
473 | item = self._tree.AppendItem(parent, itemText, | |
474 | data=wx.TreeItemData(clientData)) | |
475 | if self._sort: | |
476 | self._tree.SortChildren(parent) | |
477 | return item | |
478 | ||
479 | def Clear(self): | |
480 | """ | |
481 | Clear(self) | |
482 | ||
483 | Removes all items from the control. | |
484 | """ | |
485 | return self._tree.DeleteAllItems() | |
486 | ||
487 | def Delete(self, item): | |
488 | """ | |
489 | Delete(self, TreeItemId item) | |
490 | ||
491 | Deletes the item from the control. | |
492 | """ | |
493 | return self._tree.Delete(item) | |
494 | ||
495 | def FindString(self, string, parent=None): | |
496 | """ | |
497 | FindString(self, String string, TreeItemId parent=None) -> TreeItemId | |
498 | ||
499 | Finds the *first* item in the tree with a label equal to the | |
500 | given string. If no such item exists, an invalid item is | |
501 | returned. | |
502 | """ | |
503 | parent = parent or self._tree.GetRootItem() | |
504 | child, cookie = self._tree.GetFirstChild(parent) | |
505 | while child: | |
506 | if self._tree.GetItemText(child) == string: | |
507 | return child | |
508 | else: | |
509 | result = self.FindString(string, child) | |
510 | if result: | |
511 | return result | |
512 | child, cookie = self._tree.GetNextChild(parent, cookie) | |
513 | return child | |
514 | ||
515 | def GetSelection(self): | |
516 | """ | |
517 | GetSelection(self) -> TreeItemId | |
518 | ||
519 | Returns the TreeItemId of the selected item or an invalid item | |
520 | if no item is selected. | |
521 | """ | |
522 | selectedItem = self._tree.GetSelection() | |
523 | if selectedItem and selectedItem != self._tree.GetRootItem(): | |
524 | return selectedItem | |
525 | else: | |
526 | return self.FindString(self.GetValue()) | |
527 | ||
528 | def GetString(self, item): | |
529 | """ | |
530 | GetString(self, TreeItemId item) -> String | |
531 | ||
532 | Returns the label of the given item. | |
533 | """ | |
534 | if item: | |
535 | return self._tree.GetItemText(item) | |
536 | else: | |
537 | return '' | |
538 | ||
539 | def GetStringSelection(self): | |
540 | """ | |
541 | GetStringSelection(self) -> String | |
542 | ||
543 | Returns the label of the selected item or an empty string if no item | |
544 | is selected. | |
545 | """ | |
546 | return self.GetValue() | |
547 | ||
548 | def Insert(self, itemText, previous=None, parent=None, clientData=None): | |
549 | """ | |
550 | Insert(self, String itemText, TreeItemId previous=None, TreeItemId | |
551 | parent=None, PyObject clientData=None) -> TreeItemId | |
552 | ||
553 | Insert an item into the control before the ``previous`` item | |
554 | and/or as child of the ``parent`` item. The itemText is associated | |
555 | with clientData when not None. | |
556 | """ | |
557 | data = wx.TreeItemData(clientData) | |
558 | if parent is None: | |
559 | parent = self._tree.GetRootItem() | |
560 | if previous is None: | |
561 | item = self._tree.InsertItemBefore(parent, 0, itemText, data=data) | |
562 | else: | |
563 | item = self._tree.InsertItem(parent, previous, itemText, data=data) | |
564 | if self._sort: | |
565 | self._tree.SortChildren(parent) | |
566 | return item | |
567 | ||
568 | def IsEmpty(self): | |
569 | """ | |
570 | IsEmpty(self) -> bool | |
571 | ||
572 | Returns True if the control is empty or False if it has some items. | |
573 | """ | |
574 | return self.GetCount() == 0 | |
575 | ||
576 | def GetCount(self): | |
577 | """ | |
578 | GetCount(self) -> int | |
579 | ||
580 | Returns the number of items in the control. | |
581 | """ | |
582 | # Note: We don't need to substract 1 for the hidden root item, | |
583 | # because the TreeCtrl does that for us | |
584 | return self._tree.GetCount() | |
585 | ||
586 | def SetSelection(self, item): | |
587 | """ | |
588 | SetSelection(self, TreeItemId item) | |
589 | ||
590 | Sets the provided item to be the selected item. | |
591 | """ | |
592 | self._tree.SelectItem(item) | |
593 | self._text.SetValue(self._tree.GetItemText(item)) | |
594 | ||
595 | Select = SetSelection | |
596 | ||
597 | def SetString(self, item, string): | |
598 | """ | |
599 | SetString(self, TreeItemId item, String string) | |
600 | ||
601 | Sets the label for the provided item. | |
602 | """ | |
603 | self._tree.SetItemText(item, string) | |
604 | if self._sort: | |
605 | self._tree.SortChildren(self._tree.GetItemParent(item)) | |
606 | ||
607 | def SetStringSelection(self, string): | |
608 | """ | |
609 | SetStringSelection(self, String string) -> bool | |
610 | ||
611 | Selects the item with the provided string in the control. | |
612 | Returns True if the provided string has been selected, False if | |
613 | it wasn't found in the control. | |
614 | """ | |
615 | item = self.FindString(string) | |
616 | if item: | |
617 | if self._text.GetValue() != string: | |
618 | self._text.SetValue(string) | |
619 | self._tree.SelectItem(item) | |
620 | return True | |
621 | else: | |
622 | return False | |
623 | ||
624 | def GetClientData(self, item): | |
625 | """ | |
626 | GetClientData(self, TreeItemId item) -> PyObject | |
627 | ||
628 | Returns the client data associated with the given item, if any. | |
629 | """ | |
630 | return self._tree.GetItemPyData(item) | |
631 | ||
632 | def SetClientData(self, item, clientData): | |
633 | """ | |
634 | SetClientData(self, TreeItemId item, PyObject clientData) | |
635 | ||
636 | Associate the given client data with the provided item. | |
637 | """ | |
638 | self._tree.SetItemPyData(item, clientData) | |
639 | ||
640 | def GetValue(self): | |
641 | """ | |
642 | GetValue(self) -> String | |
643 | ||
644 | Returns the current value in the combobox text field. | |
645 | """ | |
646 | if self._text == self: | |
647 | return super(BaseComboTreeBox, self).GetValue() | |
648 | else: | |
649 | return self._text.GetValue() | |
650 | ||
651 | def SetValue(self, value): | |
652 | """ | |
653 | SetValue(self, String value) | |
654 | ||
655 | Sets the text for the combobox text field. | |
656 | ||
657 | NB: For a combobox with wxCB_READONLY style the string must be | |
658 | in the combobox choices list, otherwise the call to SetValue() | |
659 | is ignored. | |
660 | """ | |
661 | item = self._tree.GetSelection() | |
662 | if not item or self._tree.GetItemText(item) != value: | |
663 | item = self.FindString(value) | |
664 | if self._readOnly and not item: | |
665 | return | |
666 | if self._text == self: | |
667 | super(BaseComboTreeBox, self).SetValue(value) | |
668 | else: | |
669 | self._text.SetValue(value) | |
670 | if item: | |
671 | if self._tree.GetSelection() != item: | |
672 | self._tree.SelectItem(item) | |
673 | else: | |
674 | self._tree.Unselect() | |
675 | ||
676 | ||
677 | class NativeComboTreeBox(BaseComboTreeBox, wx.ComboBox): | |
678 | """ NativeComboTreeBox, and any subclass, uses the native ComboBox as | |
679 | basis, but prevent it from popping up its drop down list and | |
680 | instead pops up a PopupFrame containing a tree of items. """ | |
681 | ||
682 | def _eventsToBind(self): | |
683 | events = super(NativeComboTreeBox, self)._eventsToBind() | |
684 | # Bind all mouse click events to self.OnMouseClick so we can | |
685 | # intercept those events and prevent the native Combobox from | |
686 | # popping up its list of choices. | |
687 | for eventType in (wx.EVT_LEFT_DOWN, wx.EVT_LEFT_DCLICK, | |
688 | wx.EVT_MIDDLE_DOWN, wx.EVT_MIDDLE_DCLICK, | |
689 | wx.EVT_RIGHT_DOWN, wx.EVT_RIGHT_DCLICK): | |
690 | events.append((self._button, eventType, self.OnMouseClick)) | |
691 | if self._readOnly: | |
692 | events.append((self, wx.EVT_CHAR, self.OnChar)) | |
693 | return events | |
694 | ||
695 | def OnChar(self, event): | |
696 | # OnChar is only called when in read only mode. We don't call | |
697 | # event.Skip() on purpose, to prevent the characters from being | |
698 | # displayed in the text field. | |
699 | pass | |
700 | ||
701 | ||
702 | class MSWComboTreeBox(NativeComboTreeBox): | |
703 | """ MSWComboTreeBox adds one piece of functionality as compared to | |
704 | NativeComboTreeBox: when the user browses through the tree, the | |
705 | ComboTreeBox's text field is continuously updated to show the | |
706 | currently selected item in the tree. If the user cancels | |
707 | selecting a new item from the tree, e.g. by hitting escape, the | |
708 | previous value (the one that was selected before the PopupFrame | |
709 | was popped up) is restored. """ | |
710 | ||
711 | def _createPopupFrame(self): | |
712 | return MSWPopupFrame(self) | |
713 | ||
714 | def _eventsToBind(self): | |
715 | events = super(MSWComboTreeBox, self)._eventsToBind() | |
716 | events.append((self._tree, wx.EVT_TREE_SEL_CHANGED, | |
717 | self.OnSelectionChangedInTree)) | |
718 | return events | |
719 | ||
720 | def OnSelectionChangedInTree(self, event): | |
721 | item = event.GetItem() | |
722 | if item: | |
723 | selectedValue = self._tree.GetItemText(item) | |
724 | if self.GetValue() != selectedValue: | |
725 | self.SetValue(selectedValue) | |
726 | event.Skip() | |
727 | ||
728 | def _keyShouldPopUpTree(self, keyEvent): | |
729 | return super(MSWComboTreeBox, self)._keyShouldPopUpTree(keyEvent) or \ | |
730 | (keyEvent.GetKeyCode() == wx.WXK_F4) or \ | |
731 | ((keyEvent.AltDown() or keyEvent.MetaDown()) and \ | |
732 | keyEvent.GetKeyCode() == wx.WXK_UP) | |
733 | ||
734 | def SetValue(self, value): | |
735 | """ Extend SetValue to also select the text in the | |
736 | ComboTreeBox's text field. """ | |
737 | super(MSWComboTreeBox, self).SetValue(value) | |
738 | # We select the text in the ComboTreeBox's text field. | |
739 | # There is a slight complication, however. When the control is | |
740 | # deleted, SetValue is called. But if we call SetMark at that | |
741 | # time, wxPython will crash. We can prevent this by comparing the | |
742 | # result of GetLastPosition and the length of the value. If they | |
743 | # match, all is fine. If they don't match, we don't call SetMark. | |
744 | if self._text.GetLastPosition() == len(value): | |
745 | self._text.SetMark(0, self._text.GetLastPosition()) | |
746 | ||
747 | def Popup(self, *args, **kwargs): | |
748 | """ Extend Popup to store a copy of the current value, so we can | |
749 | restore it later (in NotifyNoItemSelected). This is necessary | |
750 | because MSWComboTreeBox will change the value as the user | |
751 | browses through the items in the popped up tree. """ | |
752 | self._previousValue = self.GetValue() | |
753 | super(MSWComboTreeBox, self).Popup(*args, **kwargs) | |
754 | ||
755 | def NotifyNoItemSelected(self, *args, **kwargs): | |
756 | """ Restore the value copied previously, because the user has | |
757 | not selected a new value. """ | |
758 | self.SetValue(self._previousValue) | |
759 | super(MSWComboTreeBox, self).NotifyNoItemSelected(*args, **kwargs) | |
760 | ||
761 | ||
762 | class MACComboTreeBox(NativeComboTreeBox): | |
763 | def _createPopupFrame(self): | |
764 | return MACPopupFrame(self) | |
765 | ||
766 | def _createButton(self): | |
767 | return self.GetChildren()[0] # The choice button | |
768 | ||
769 | def _keyShouldNavigate(self, keyEvent): | |
770 | return False # No navigation with up and down on wxMac | |
771 | ||
772 | def _keyShouldPopUpTree(self, keyEvent): | |
773 | return super(MACComboTreeBox, self)._keyShouldPopUpTree(keyEvent) or \ | |
774 | keyEvent.GetKeyCode() == wx.WXK_DOWN | |
775 | ||
776 | ||
777 | class GTKComboTreeBox(BaseComboTreeBox, wx.Panel): | |
778 | """ The ComboTreeBox widget for wxGTK. This is actually a work | |
779 | around because on wxGTK, there doesn't seem to be a way to intercept | |
780 | mouse events sent to the Combobox. Intercepting those events is | |
781 | necessary to prevent the Combobox from popping up the list and pop up | |
782 | the tree instead. So, until wxPython makes intercepting those events | |
783 | possible we build a poor man's Combobox ourselves using a TextCtrl and | |
784 | a BitmapButton. """ | |
785 | ||
786 | def _createPopupFrame(self): | |
787 | return GTKPopupFrame(self) | |
788 | ||
789 | def _createTextCtrl(self): | |
790 | if self._readOnly: | |
791 | style = wx.TE_READONLY | |
792 | else: | |
793 | style = 0 | |
794 | return wx.TextCtrl(self, style=style) | |
795 | ||
796 | def _createButton(self): | |
797 | bitmap = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, client=wx.ART_BUTTON) | |
798 | return wx.BitmapButton(self, bitmap=bitmap) | |
799 | ||
800 | def _layoutInterior(self): | |
801 | panelSizer = wx.BoxSizer(wx.HORIZONTAL) | |
802 | panelSizer.Add(self._text, flag=wx.EXPAND, proportion=1) | |
803 | panelSizer.Add(self._button) | |
804 | self.SetSizerAndFit(panelSizer) | |
805 | ||
806 | ||
807 | # --------------------------------------------------------------------------- | |
808 | ||
809 | ||
810 | def ComboTreeBox(*args, **kwargs): | |
811 | """ Factory function to create the right ComboTreeBox depending on | |
812 | platform. You may force a specific class, e.g. for testing | |
813 | purposes, by setting the keyword argument 'platform', e.g. | |
814 | 'platform=GTK' or 'platform=MSW' or platform='MAC'. """ | |
815 | ||
816 | platform = kwargs.pop('platform', None) or wx.PlatformInfo[0][4:7] | |
817 | ComboTreeBoxClassName = '%sComboTreeBox' % platform | |
818 | ComboTreeBoxClass = globals()[ComboTreeBoxClassName] | |
819 | return ComboTreeBoxClass(*args, **kwargs) | |
820 |