]>
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 | |
49e0b673 RD |
40 | Version: 0.9 |
41 | Date: September 6, 2006 | |
4f708f05 RD |
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 | ||
49e0b673 | 153 | class BasePopupFrame(wx.Frame): |
4f708f05 RD |
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) | |
49e0b673 RD |
453 | string = self._tree.GetItemText(item) |
454 | if self._text.GetValue() != string: | |
455 | self._text.SetValue(string) | |
4f708f05 RD |
456 | return True |
457 | else: | |
458 | return False | |
459 | ||
460 | # The following methods are all part of the ComboBox API (actually | |
461 | # the ControlWithItems API) and have been adapted to take TreeItemIds | |
462 | # as parameter and return TreeItemIds, rather than indices. | |
463 | ||
464 | def Append(self, itemText, parent=None, clientData=None): | |
465 | """ | |
466 | Append(self, String itemText, TreeItemId parent=None, PyObject | |
467 | clientData=None) -> TreeItemId | |
468 | ||
469 | Adds the itemText to the control, associating the given clientData | |
470 | with the item if not None. If parent is None, itemText is added | |
471 | as a root item, else itemText is added as a child item of | |
472 | parent. The return value is the TreeItemId of the newly added | |
473 | item. """ | |
474 | if parent is None: | |
475 | parent = self._tree.GetRootItem() | |
476 | item = self._tree.AppendItem(parent, itemText, | |
477 | data=wx.TreeItemData(clientData)) | |
478 | if self._sort: | |
479 | self._tree.SortChildren(parent) | |
480 | return item | |
481 | ||
482 | def Clear(self): | |
483 | """ | |
484 | Clear(self) | |
485 | ||
486 | Removes all items from the control. | |
487 | """ | |
488 | return self._tree.DeleteAllItems() | |
489 | ||
490 | def Delete(self, item): | |
491 | """ | |
492 | Delete(self, TreeItemId item) | |
493 | ||
494 | Deletes the item from the control. | |
495 | """ | |
496 | return self._tree.Delete(item) | |
497 | ||
498 | def FindString(self, string, parent=None): | |
499 | """ | |
500 | FindString(self, String string, TreeItemId parent=None) -> TreeItemId | |
501 | ||
502 | Finds the *first* item in the tree with a label equal to the | |
503 | given string. If no such item exists, an invalid item is | |
504 | returned. | |
505 | """ | |
506 | parent = parent or self._tree.GetRootItem() | |
507 | child, cookie = self._tree.GetFirstChild(parent) | |
508 | while child: | |
509 | if self._tree.GetItemText(child) == string: | |
510 | return child | |
511 | else: | |
512 | result = self.FindString(string, child) | |
513 | if result: | |
514 | return result | |
515 | child, cookie = self._tree.GetNextChild(parent, cookie) | |
516 | return child | |
517 | ||
518 | def GetSelection(self): | |
519 | """ | |
520 | GetSelection(self) -> TreeItemId | |
521 | ||
522 | Returns the TreeItemId of the selected item or an invalid item | |
523 | if no item is selected. | |
524 | """ | |
525 | selectedItem = self._tree.GetSelection() | |
526 | if selectedItem and selectedItem != self._tree.GetRootItem(): | |
527 | return selectedItem | |
528 | else: | |
529 | return self.FindString(self.GetValue()) | |
530 | ||
531 | def GetString(self, item): | |
532 | """ | |
533 | GetString(self, TreeItemId item) -> String | |
534 | ||
535 | Returns the label of the given item. | |
536 | """ | |
537 | if item: | |
538 | return self._tree.GetItemText(item) | |
539 | else: | |
540 | return '' | |
541 | ||
542 | def GetStringSelection(self): | |
543 | """ | |
544 | GetStringSelection(self) -> String | |
545 | ||
546 | Returns the label of the selected item or an empty string if no item | |
547 | is selected. | |
548 | """ | |
549 | return self.GetValue() | |
550 | ||
551 | def Insert(self, itemText, previous=None, parent=None, clientData=None): | |
552 | """ | |
553 | Insert(self, String itemText, TreeItemId previous=None, TreeItemId | |
554 | parent=None, PyObject clientData=None) -> TreeItemId | |
555 | ||
556 | Insert an item into the control before the ``previous`` item | |
557 | and/or as child of the ``parent`` item. The itemText is associated | |
558 | with clientData when not None. | |
559 | """ | |
560 | data = wx.TreeItemData(clientData) | |
561 | if parent is None: | |
562 | parent = self._tree.GetRootItem() | |
563 | if previous is None: | |
564 | item = self._tree.InsertItemBefore(parent, 0, itemText, data=data) | |
565 | else: | |
566 | item = self._tree.InsertItem(parent, previous, itemText, data=data) | |
567 | if self._sort: | |
568 | self._tree.SortChildren(parent) | |
569 | return item | |
570 | ||
571 | def IsEmpty(self): | |
572 | """ | |
573 | IsEmpty(self) -> bool | |
574 | ||
575 | Returns True if the control is empty or False if it has some items. | |
576 | """ | |
577 | return self.GetCount() == 0 | |
578 | ||
579 | def GetCount(self): | |
580 | """ | |
581 | GetCount(self) -> int | |
582 | ||
583 | Returns the number of items in the control. | |
584 | """ | |
585 | # Note: We don't need to substract 1 for the hidden root item, | |
586 | # because the TreeCtrl does that for us | |
587 | return self._tree.GetCount() | |
588 | ||
589 | def SetSelection(self, item): | |
590 | """ | |
591 | SetSelection(self, TreeItemId item) | |
592 | ||
593 | Sets the provided item to be the selected item. | |
594 | """ | |
595 | self._tree.SelectItem(item) | |
596 | self._text.SetValue(self._tree.GetItemText(item)) | |
597 | ||
598 | Select = SetSelection | |
599 | ||
600 | def SetString(self, item, string): | |
601 | """ | |
602 | SetString(self, TreeItemId item, String string) | |
603 | ||
604 | Sets the label for the provided item. | |
605 | """ | |
606 | self._tree.SetItemText(item, string) | |
607 | if self._sort: | |
608 | self._tree.SortChildren(self._tree.GetItemParent(item)) | |
609 | ||
610 | def SetStringSelection(self, string): | |
611 | """ | |
612 | SetStringSelection(self, String string) -> bool | |
613 | ||
614 | Selects the item with the provided string in the control. | |
615 | Returns True if the provided string has been selected, False if | |
616 | it wasn't found in the control. | |
617 | """ | |
618 | item = self.FindString(string) | |
619 | if item: | |
620 | if self._text.GetValue() != string: | |
621 | self._text.SetValue(string) | |
622 | self._tree.SelectItem(item) | |
623 | return True | |
624 | else: | |
625 | return False | |
626 | ||
627 | def GetClientData(self, item): | |
628 | """ | |
629 | GetClientData(self, TreeItemId item) -> PyObject | |
630 | ||
631 | Returns the client data associated with the given item, if any. | |
632 | """ | |
633 | return self._tree.GetItemPyData(item) | |
634 | ||
635 | def SetClientData(self, item, clientData): | |
636 | """ | |
637 | SetClientData(self, TreeItemId item, PyObject clientData) | |
638 | ||
639 | Associate the given client data with the provided item. | |
640 | """ | |
641 | self._tree.SetItemPyData(item, clientData) | |
642 | ||
643 | def GetValue(self): | |
644 | """ | |
645 | GetValue(self) -> String | |
646 | ||
647 | Returns the current value in the combobox text field. | |
648 | """ | |
649 | if self._text == self: | |
650 | return super(BaseComboTreeBox, self).GetValue() | |
651 | else: | |
652 | return self._text.GetValue() | |
653 | ||
654 | def SetValue(self, value): | |
655 | """ | |
656 | SetValue(self, String value) | |
657 | ||
658 | Sets the text for the combobox text field. | |
659 | ||
660 | NB: For a combobox with wxCB_READONLY style the string must be | |
661 | in the combobox choices list, otherwise the call to SetValue() | |
662 | is ignored. | |
663 | """ | |
664 | item = self._tree.GetSelection() | |
665 | if not item or self._tree.GetItemText(item) != value: | |
666 | item = self.FindString(value) | |
667 | if self._readOnly and not item: | |
668 | return | |
669 | if self._text == self: | |
670 | super(BaseComboTreeBox, self).SetValue(value) | |
671 | else: | |
672 | self._text.SetValue(value) | |
673 | if item: | |
674 | if self._tree.GetSelection() != item: | |
675 | self._tree.SelectItem(item) | |
676 | else: | |
677 | self._tree.Unselect() | |
678 | ||
679 | ||
680 | class NativeComboTreeBox(BaseComboTreeBox, wx.ComboBox): | |
681 | """ NativeComboTreeBox, and any subclass, uses the native ComboBox as | |
682 | basis, but prevent it from popping up its drop down list and | |
683 | instead pops up a PopupFrame containing a tree of items. """ | |
684 | ||
685 | def _eventsToBind(self): | |
686 | events = super(NativeComboTreeBox, self)._eventsToBind() | |
687 | # Bind all mouse click events to self.OnMouseClick so we can | |
688 | # intercept those events and prevent the native Combobox from | |
689 | # popping up its list of choices. | |
690 | for eventType in (wx.EVT_LEFT_DOWN, wx.EVT_LEFT_DCLICK, | |
691 | wx.EVT_MIDDLE_DOWN, wx.EVT_MIDDLE_DCLICK, | |
692 | wx.EVT_RIGHT_DOWN, wx.EVT_RIGHT_DCLICK): | |
693 | events.append((self._button, eventType, self.OnMouseClick)) | |
694 | if self._readOnly: | |
695 | events.append((self, wx.EVT_CHAR, self.OnChar)) | |
696 | return events | |
697 | ||
698 | def OnChar(self, event): | |
699 | # OnChar is only called when in read only mode. We don't call | |
700 | # event.Skip() on purpose, to prevent the characters from being | |
701 | # displayed in the text field. | |
702 | pass | |
703 | ||
704 | ||
705 | class MSWComboTreeBox(NativeComboTreeBox): | |
706 | """ MSWComboTreeBox adds one piece of functionality as compared to | |
707 | NativeComboTreeBox: when the user browses through the tree, the | |
708 | ComboTreeBox's text field is continuously updated to show the | |
709 | currently selected item in the tree. If the user cancels | |
710 | selecting a new item from the tree, e.g. by hitting escape, the | |
711 | previous value (the one that was selected before the PopupFrame | |
712 | was popped up) is restored. """ | |
713 | ||
714 | def _createPopupFrame(self): | |
715 | return MSWPopupFrame(self) | |
716 | ||
717 | def _eventsToBind(self): | |
718 | events = super(MSWComboTreeBox, self)._eventsToBind() | |
719 | events.append((self._tree, wx.EVT_TREE_SEL_CHANGED, | |
720 | self.OnSelectionChangedInTree)) | |
721 | return events | |
722 | ||
723 | def OnSelectionChangedInTree(self, event): | |
49e0b673 RD |
724 | if self.IsBeingDeleted(): |
725 | return | |
4f708f05 RD |
726 | item = event.GetItem() |
727 | if item: | |
728 | selectedValue = self._tree.GetItemText(item) | |
729 | if self.GetValue() != selectedValue: | |
730 | self.SetValue(selectedValue) | |
731 | event.Skip() | |
732 | ||
733 | def _keyShouldPopUpTree(self, keyEvent): | |
734 | return super(MSWComboTreeBox, self)._keyShouldPopUpTree(keyEvent) or \ | |
735 | (keyEvent.GetKeyCode() == wx.WXK_F4) or \ | |
736 | ((keyEvent.AltDown() or keyEvent.MetaDown()) and \ | |
737 | keyEvent.GetKeyCode() == wx.WXK_UP) | |
738 | ||
739 | def SetValue(self, value): | |
740 | """ Extend SetValue to also select the text in the | |
741 | ComboTreeBox's text field. """ | |
742 | super(MSWComboTreeBox, self).SetValue(value) | |
743 | # We select the text in the ComboTreeBox's text field. | |
744 | # There is a slight complication, however. When the control is | |
745 | # deleted, SetValue is called. But if we call SetMark at that | |
746 | # time, wxPython will crash. We can prevent this by comparing the | |
747 | # result of GetLastPosition and the length of the value. If they | |
748 | # match, all is fine. If they don't match, we don't call SetMark. | |
749 | if self._text.GetLastPosition() == len(value): | |
750 | self._text.SetMark(0, self._text.GetLastPosition()) | |
751 | ||
752 | def Popup(self, *args, **kwargs): | |
753 | """ Extend Popup to store a copy of the current value, so we can | |
754 | restore it later (in NotifyNoItemSelected). This is necessary | |
755 | because MSWComboTreeBox will change the value as the user | |
756 | browses through the items in the popped up tree. """ | |
757 | self._previousValue = self.GetValue() | |
758 | super(MSWComboTreeBox, self).Popup(*args, **kwargs) | |
759 | ||
760 | def NotifyNoItemSelected(self, *args, **kwargs): | |
761 | """ Restore the value copied previously, because the user has | |
762 | not selected a new value. """ | |
763 | self.SetValue(self._previousValue) | |
764 | super(MSWComboTreeBox, self).NotifyNoItemSelected(*args, **kwargs) | |
765 | ||
766 | ||
767 | class MACComboTreeBox(NativeComboTreeBox): | |
768 | def _createPopupFrame(self): | |
769 | return MACPopupFrame(self) | |
770 | ||
771 | def _createButton(self): | |
772 | return self.GetChildren()[0] # The choice button | |
773 | ||
774 | def _keyShouldNavigate(self, keyEvent): | |
775 | return False # No navigation with up and down on wxMac | |
776 | ||
777 | def _keyShouldPopUpTree(self, keyEvent): | |
778 | return super(MACComboTreeBox, self)._keyShouldPopUpTree(keyEvent) or \ | |
779 | keyEvent.GetKeyCode() == wx.WXK_DOWN | |
780 | ||
781 | ||
782 | class GTKComboTreeBox(BaseComboTreeBox, wx.Panel): | |
783 | """ The ComboTreeBox widget for wxGTK. This is actually a work | |
784 | around because on wxGTK, there doesn't seem to be a way to intercept | |
785 | mouse events sent to the Combobox. Intercepting those events is | |
786 | necessary to prevent the Combobox from popping up the list and pop up | |
787 | the tree instead. So, until wxPython makes intercepting those events | |
788 | possible we build a poor man's Combobox ourselves using a TextCtrl and | |
789 | a BitmapButton. """ | |
790 | ||
791 | def _createPopupFrame(self): | |
792 | return GTKPopupFrame(self) | |
793 | ||
794 | def _createTextCtrl(self): | |
795 | if self._readOnly: | |
796 | style = wx.TE_READONLY | |
797 | else: | |
798 | style = 0 | |
799 | return wx.TextCtrl(self, style=style) | |
800 | ||
801 | def _createButton(self): | |
802 | bitmap = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, client=wx.ART_BUTTON) | |
803 | return wx.BitmapButton(self, bitmap=bitmap) | |
804 | ||
805 | def _layoutInterior(self): | |
806 | panelSizer = wx.BoxSizer(wx.HORIZONTAL) | |
807 | panelSizer.Add(self._text, flag=wx.EXPAND, proportion=1) | |
808 | panelSizer.Add(self._button) | |
809 | self.SetSizerAndFit(panelSizer) | |
810 | ||
811 | ||
812 | # --------------------------------------------------------------------------- | |
813 | ||
814 | ||
815 | def ComboTreeBox(*args, **kwargs): | |
816 | """ Factory function to create the right ComboTreeBox depending on | |
817 | platform. You may force a specific class, e.g. for testing | |
818 | purposes, by setting the keyword argument 'platform', e.g. | |
819 | 'platform=GTK' or 'platform=MSW' or platform='MAC'. """ | |
820 | ||
821 | platform = kwargs.pop('platform', None) or wx.PlatformInfo[0][4:7] | |
822 | ComboTreeBoxClassName = '%sComboTreeBox' % platform | |
823 | ComboTreeBoxClass = globals()[ComboTreeBoxClassName] | |
824 | return ComboTreeBoxClass(*args, **kwargs) | |
825 |