]>
Commit | Line | Data |
---|---|---|
1 | #---------------------------------------------------------------------------- | |
2 | # Name: OutlineService.py | |
3 | # Purpose: Outline View Service for pydocview | |
4 | # | |
5 | # Author: Morgan Hua | |
6 | # | |
7 | # Created: 8/3/04 | |
8 | # CVS-ID: $Id$ | |
9 | # Copyright: (c) 2004-2005 ActiveGrid, Inc. | |
10 | # License: wxWindows License | |
11 | #---------------------------------------------------------------------------- | |
12 | ||
13 | import wx | |
14 | import wx.lib.docview | |
15 | import wx.lib.pydocview | |
16 | import Service | |
17 | _ = wx.GetTranslation | |
18 | ||
19 | ||
20 | #---------------------------------------------------------------------------- | |
21 | # Constants | |
22 | #---------------------------------------------------------------------------- | |
23 | SORT_NONE = 0 | |
24 | SORT_ASC = 1 | |
25 | SORT_DESC = 2 | |
26 | ||
27 | class OutlineView(Service.ServiceView): | |
28 | """ Reusable Outline View for any document. | |
29 | As a default, it uses a modified tree control (OutlineTreeCtrl) that allows sorting. | |
30 | Subclass OutlineTreeCtrl to customize the tree control and call SetTreeCtrl to install a customized tree control. | |
31 | When an item is selected, the document view is called back (with DoSelectCallback) to highlight and display the corresponding item in the document view. | |
32 | """ | |
33 | ||
34 | #---------------------------------------------------------------------------- | |
35 | # Overridden methods | |
36 | #---------------------------------------------------------------------------- | |
37 | ||
38 | def __init__(self, service): | |
39 | Service.ServiceView.__init__(self, service) | |
40 | self._actionOnSelect = True | |
41 | ||
42 | ||
43 | def _CreateControl(self, parent, id): | |
44 | treeCtrl = OutlineTreeCtrl(parent, id) | |
45 | wx.EVT_TREE_SEL_CHANGED(treeCtrl, treeCtrl.GetId(), self.DoSelection) | |
46 | wx.EVT_SET_FOCUS(treeCtrl, self.DoSelection) | |
47 | wx.EVT_ENTER_WINDOW(treeCtrl, treeCtrl.CallDoLoadOutlineCallback) | |
48 | wx.EVT_RIGHT_DOWN(treeCtrl, self.OnRightClick) | |
49 | ||
50 | return treeCtrl | |
51 | ||
52 | ||
53 | #---------------------------------------------------------------------------- | |
54 | # Service specific methods | |
55 | #---------------------------------------------------------------------------- | |
56 | ||
57 | def OnRightClick(self, event): | |
58 | menu = wx.Menu() | |
59 | ||
60 | menu.AppendRadioItem(OutlineService.SORT_NONE, _("Unsorted"), _("Display items in original order")) | |
61 | menu.AppendRadioItem(OutlineService.SORT_ASC, _("Sort A-Z"), _("Display items in ascending order")) | |
62 | menu.AppendRadioItem(OutlineService.SORT_DESC, _("Sort Z-A"), _("Display items in descending order")) | |
63 | ||
64 | config = wx.ConfigBase_Get() | |
65 | sort = config.ReadInt("OutlineSort", SORT_NONE) | |
66 | if sort == SORT_NONE: | |
67 | menu.Check(OutlineService.SORT_NONE, True) | |
68 | elif sort == SORT_ASC: | |
69 | menu.Check(OutlineService.SORT_ASC, True) | |
70 | elif sort == SORT_DESC: | |
71 | menu.Check(OutlineService.SORT_DESC, True) | |
72 | ||
73 | self.GetControl().PopupMenu(menu, event.GetPosition()) | |
74 | menu.Destroy() | |
75 | ||
76 | ||
77 | #---------------------------------------------------------------------------- | |
78 | # Tree Methods | |
79 | #---------------------------------------------------------------------------- | |
80 | ||
81 | def DoSelection(self, event): | |
82 | if not self._actionOnSelect: | |
83 | return | |
84 | item = self.GetControl().GetSelection() | |
85 | if item: | |
86 | self.GetControl().CallDoSelectCallback(item) | |
87 | ||
88 | ||
89 | def ResumeActionOnSelect(self): | |
90 | self._actionOnSelect = True | |
91 | ||
92 | ||
93 | def StopActionOnSelect(self): | |
94 | self._actionOnSelect = False | |
95 | ||
96 | ||
97 | def SetTreeCtrl(self, tree): | |
98 | self.SetControl(tree) | |
99 | wx.EVT_TREE_SEL_CHANGED(self.GetControl(), self.GetControl().GetId(), self.DoSelection) | |
100 | wx.EVT_ENTER_WINDOW(self.GetControl(), treeCtrl.CallDoLoadOutlineCallback) | |
101 | wx.EVT_RIGHT_DOWN(self.GetControl(), self.OnRightClick) | |
102 | ||
103 | ||
104 | def GetTreeCtrl(self): | |
105 | return self.GetControl() | |
106 | ||
107 | ||
108 | def OnSort(self, sortOrder): | |
109 | treeCtrl = self.GetControl() | |
110 | treeCtrl.SetSortOrder(sortOrder) | |
111 | treeCtrl.SortAllChildren(treeCtrl.GetRootItem()) | |
112 | ||
113 | ||
114 | def ClearTreeCtrl(self): | |
115 | if self.GetControl(): | |
116 | self.GetControl().DeleteAllItems() | |
117 | ||
118 | ||
119 | def GetExpansionState(self): | |
120 | expanded = [] | |
121 | ||
122 | treeCtrl = self.GetControl() | |
123 | if not treeCtrl: | |
124 | return expanded | |
125 | ||
126 | parentItem = treeCtrl.GetRootItem() | |
127 | ||
128 | if not parentItem: | |
129 | return expanded | |
130 | ||
131 | if not treeCtrl.IsExpanded(parentItem): | |
132 | return expanded | |
133 | ||
134 | expanded.append(treeCtrl.GetItemText(parentItem)) | |
135 | ||
136 | (child, cookie) = treeCtrl.GetFirstChild(parentItem) | |
137 | while child.IsOk(): | |
138 | if treeCtrl.IsExpanded(child): | |
139 | expanded.append(treeCtrl.GetItemText(child)) | |
140 | (child, cookie) = treeCtrl.GetNextChild(parentItem, cookie) | |
141 | return expanded | |
142 | ||
143 | ||
144 | def SetExpansionState(self, expanded): | |
145 | if not expanded or len(expanded) == 0: | |
146 | return | |
147 | ||
148 | treeCtrl = self.GetControl() | |
149 | parentItem = treeCtrl.GetRootItem() | |
150 | if expanded[0] != treeCtrl.GetItemText(parentItem): | |
151 | return | |
152 | ||
153 | (child, cookie) = treeCtrl.GetFirstChild(parentItem) | |
154 | while child.IsOk(): | |
155 | if treeCtrl.GetItemText(child) in expanded: | |
156 | treeCtrl.Expand(child) | |
157 | (child, cookie) = treeCtrl.GetNextChild(parentItem, cookie) | |
158 | ||
159 | # wxBug: This causes a crash, tried using ScrollTo which crashed as well. Then tried calling it with wx.CallAfter and that crashed as well, with both EnsureVisible and ScrollTo | |
160 | # self.GetControl().EnsureVisible(self.GetControl().GetRootItem()) | |
161 | # So doing the following massive hack which forces the treectrl to scroll up to the top item | |
162 | treeCtrl.Collapse(parentItem) | |
163 | treeCtrl.Expand(parentItem) | |
164 | ||
165 | ||
166 | class OutlineTreeCtrl(wx.TreeCtrl): | |
167 | """ Default Tree Control Class for OutlineView. | |
168 | This class has the added functionality of sorting by the labels | |
169 | """ | |
170 | ||
171 | ||
172 | #---------------------------------------------------------------------------- | |
173 | # Constants | |
174 | #---------------------------------------------------------------------------- | |
175 | ORIG_ORDER = 0 | |
176 | VIEW = 1 | |
177 | CALLBACKDATA = 2 | |
178 | ||
179 | ||
180 | #---------------------------------------------------------------------------- | |
181 | # Overridden Methods | |
182 | #---------------------------------------------------------------------------- | |
183 | ||
184 | def __init__(self, parent, id, style=wx.TR_HAS_BUTTONS|wx.TR_DEFAULT_STYLE): | |
185 | wx.TreeCtrl.__init__(self, parent, id, style = style) | |
186 | self._origOrderIndex = 0 | |
187 | self._sortOrder = SORT_NONE | |
188 | ||
189 | ||
190 | def DeleteAllItems(self): | |
191 | self._origOrderIndex = 0 | |
192 | wx.TreeCtrl.DeleteAllItems(self) | |
193 | ||
194 | ||
195 | #---------------------------------------------------------------------------- | |
196 | # Sort Methods | |
197 | #---------------------------------------------------------------------------- | |
198 | ||
199 | def SetSortOrder(self, sortOrder = SORT_NONE): | |
200 | """ Sort Order constants are defined at top of file """ | |
201 | self._sortOrder = sortOrder | |
202 | ||
203 | ||
204 | def OnCompareItems(self, item1, item2): | |
205 | if self._sortOrder == SORT_ASC: | |
206 | return cmp(self.GetItemText(item1).lower(), self.GetItemText(item2).lower()) # sort A-Z | |
207 | elif self._sortOrder == SORT_DESC: | |
208 | return cmp(self.GetItemText(item2).lower(), self.GetItemText(item1).lower()) # sort Z-A | |
209 | else: | |
210 | return (self.GetPyData(item1)[self.ORIG_ORDER] > self.GetPyData(item2)[self.ORIG_ORDER]) # unsorted | |
211 | ||
212 | ||
213 | def SortAllChildren(self, parentItem): | |
214 | if parentItem and self.GetChildrenCount(parentItem, False): | |
215 | self.SortChildren(parentItem) | |
216 | (child, cookie) = self.GetFirstChild(parentItem) | |
217 | while child.IsOk(): | |
218 | self.SortAllChildren(child) | |
219 | (child, cookie) = self.GetNextChild(parentItem, cookie) | |
220 | ||
221 | ||
222 | #---------------------------------------------------------------------------- | |
223 | # Select Callback Methods | |
224 | #---------------------------------------------------------------------------- | |
225 | ||
226 | def CallDoSelectCallback(self, item): | |
227 | """ Invoke the DoSelectCallback of the given view to highlight text in the document view | |
228 | """ | |
229 | data = self.GetPyData(item) | |
230 | if not data: | |
231 | return | |
232 | ||
233 | view = data[self.VIEW] | |
234 | cbdata = data[self.CALLBACKDATA] | |
235 | if view: | |
236 | view.DoSelectCallback(cbdata) | |
237 | ||
238 | ||
239 | def SelectClosestItem(self, position): | |
240 | tree = self | |
241 | distances = [] | |
242 | items = [] | |
243 | self.FindDistanceToTreeItems(tree.GetRootItem(), position, distances, items) | |
244 | mindist = 1000000 | |
245 | mindex = -1 | |
246 | for index in range(0, len(distances)): | |
247 | if distances[index] <= mindist: | |
248 | mindist = distances[index] | |
249 | mindex = index | |
250 | if mindex != -1: | |
251 | item = items[mindex] | |
252 | self.EnsureVisible(item) | |
253 | os_view = wx.GetApp().GetService(OutlineService).GetView() | |
254 | if os_view: | |
255 | os_view.StopActionOnSelect() | |
256 | self.SelectItem(item) | |
257 | if os_view: | |
258 | os_view.ResumeActionOnSelect() | |
259 | ||
260 | ||
261 | def FindDistanceToTreeItems(self, item, position, distances, items): | |
262 | data = self.GetPyData(item) | |
263 | this_dist = 1000000 | |
264 | if data and data[2]: | |
265 | positionTuple = data[2] | |
266 | if position >= positionTuple[1]: | |
267 | items.append(item) | |
268 | distances.append(position - positionTuple[1]) | |
269 | ||
270 | if self.ItemHasChildren(item): | |
271 | child, cookie = self.GetFirstChild(item) | |
272 | while child and child.IsOk(): | |
273 | self.FindDistanceToTreeItems(child, position, distances, items) | |
274 | child, cookie = self.GetNextChild(item, cookie) | |
275 | return False | |
276 | ||
277 | ||
278 | def SetDoSelectCallback(self, item, view, callbackdata): | |
279 | """ When an item in the outline view is selected, | |
280 | a method is called to select the respective text in the document view. | |
281 | The view must define the method DoSelectCallback(self, data) in order for this to work | |
282 | """ | |
283 | self.SetPyData(item, (self._origOrderIndex, view, callbackdata)) | |
284 | self._origOrderIndex = self._origOrderIndex + 1 | |
285 | ||
286 | ||
287 | def CallDoLoadOutlineCallback(self, event): | |
288 | """ Invoke the DoLoadOutlineCallback | |
289 | """ | |
290 | rootItem = self.GetRootItem() | |
291 | if rootItem: | |
292 | data = self.GetPyData(rootItem) | |
293 | if data: | |
294 | view = data[self.VIEW] | |
295 | if view and view.DoLoadOutlineCallback(): | |
296 | self.SortAllChildren(self.GetRootItem()) | |
297 | ||
298 | ||
299 | def GetCallbackView(self): | |
300 | rootItem = self.GetRootItem() | |
301 | if rootItem: | |
302 | return self.GetPyData(rootItem)[self.VIEW] | |
303 | else: | |
304 | return None | |
305 | ||
306 | ||
307 | class OutlineService(Service.Service): | |
308 | ||
309 | ||
310 | #---------------------------------------------------------------------------- | |
311 | # Constants | |
312 | #---------------------------------------------------------------------------- | |
313 | SHOW_WINDOW = wx.NewId() # keep this line for each subclass, need unique ID for each Service | |
314 | SORT = wx.NewId() | |
315 | SORT_ASC = wx.NewId() | |
316 | SORT_DESC = wx.NewId() | |
317 | SORT_NONE = wx.NewId() | |
318 | ||
319 | ||
320 | #---------------------------------------------------------------------------- | |
321 | # Overridden methods | |
322 | #---------------------------------------------------------------------------- | |
323 | ||
324 | def __init__(self, serviceName, embeddedWindowLocation = wx.lib.pydocview.EMBEDDED_WINDOW_BOTTOM): | |
325 | Service.Service.__init__(self, serviceName, embeddedWindowLocation) | |
326 | self._validTemplates = [] | |
327 | ||
328 | ||
329 | def _CreateView(self): | |
330 | return OutlineView(self) | |
331 | ||
332 | ||
333 | def InstallControls(self, frame, menuBar = None, toolBar = None, statusBar = None, document = None): | |
334 | Service.Service.InstallControls(self, frame, menuBar, toolBar, statusBar, document) | |
335 | ||
336 | wx.EVT_MENU(frame, OutlineService.SORT_ASC, frame.ProcessEvent) | |
337 | wx.EVT_UPDATE_UI(frame, OutlineService.SORT_ASC, frame.ProcessUpdateUIEvent) | |
338 | wx.EVT_MENU(frame, OutlineService.SORT_DESC, frame.ProcessEvent) | |
339 | wx.EVT_UPDATE_UI(frame, OutlineService.SORT_DESC, frame.ProcessUpdateUIEvent) | |
340 | wx.EVT_MENU(frame, OutlineService.SORT_NONE, frame.ProcessEvent) | |
341 | wx.EVT_UPDATE_UI(frame, OutlineService.SORT_NONE, frame.ProcessUpdateUIEvent) | |
342 | ||
343 | ||
344 | if wx.GetApp().GetDocumentManager().GetFlags() & wx.lib.docview.DOC_SDI: | |
345 | return True | |
346 | ||
347 | viewMenu = menuBar.GetMenu(menuBar.FindMenu(_("&View"))) | |
348 | self._outlineSortMenu = wx.Menu() | |
349 | self._outlineSortMenu.AppendRadioItem(OutlineService.SORT_NONE, _("Unsorted"), _("Display items in original order")) | |
350 | self._outlineSortMenu.AppendRadioItem(OutlineService.SORT_ASC, _("Sort A-Z"), _("Display items in ascending order")) | |
351 | self._outlineSortMenu.AppendRadioItem(OutlineService.SORT_DESC, _("Sort Z-A"), _("Display items in descending order")) | |
352 | viewMenu.AppendMenu(wx.NewId(), _("Outline Sort"), self._outlineSortMenu) | |
353 | ||
354 | return True | |
355 | ||
356 | ||
357 | #---------------------------------------------------------------------------- | |
358 | # Event Processing Methods | |
359 | #---------------------------------------------------------------------------- | |
360 | ||
361 | def ProcessEvent(self, event): | |
362 | if Service.Service.ProcessEvent(self, event): | |
363 | return True | |
364 | ||
365 | id = event.GetId() | |
366 | if id == OutlineService.SORT_ASC: | |
367 | self.OnSort(event) | |
368 | return True | |
369 | elif id == OutlineService.SORT_DESC: | |
370 | self.OnSort(event) | |
371 | return True | |
372 | elif id == OutlineService.SORT_NONE: | |
373 | self.OnSort(event) | |
374 | return True | |
375 | else: | |
376 | return False | |
377 | ||
378 | ||
379 | def ProcessUpdateUIEvent(self, event): | |
380 | if Service.Service.ProcessUpdateUIEvent(self, event): | |
381 | return True | |
382 | ||
383 | id = event.GetId() | |
384 | if id == OutlineService.SORT_ASC: | |
385 | event.Enable(True) | |
386 | ||
387 | config = wx.ConfigBase_Get() | |
388 | sort = config.ReadInt("OutlineSort", SORT_NONE) | |
389 | if sort == SORT_ASC: | |
390 | self._outlineSortMenu.Check(OutlineService.SORT_ASC, True) | |
391 | else: | |
392 | self._outlineSortMenu.Check(OutlineService.SORT_ASC, False) | |
393 | ||
394 | return True | |
395 | elif id == OutlineService.SORT_DESC: | |
396 | event.Enable(True) | |
397 | ||
398 | config = wx.ConfigBase_Get() | |
399 | sort = config.ReadInt("OutlineSort", SORT_NONE) | |
400 | if sort == SORT_DESC: | |
401 | self._outlineSortMenu.Check(OutlineService.SORT_DESC, True) | |
402 | else: | |
403 | self._outlineSortMenu.Check(OutlineService.SORT_DESC, False) | |
404 | ||
405 | return True | |
406 | elif id == OutlineService.SORT_NONE: | |
407 | event.Enable(True) | |
408 | ||
409 | config = wx.ConfigBase_Get() | |
410 | sort = config.ReadInt("OutlineSort", SORT_NONE) | |
411 | if sort == SORT_NONE: | |
412 | self._outlineSortMenu.Check(OutlineService.SORT_NONE, True) | |
413 | else: | |
414 | self._outlineSortMenu.Check(OutlineService.SORT_NONE, False) | |
415 | ||
416 | return True | |
417 | else: | |
418 | return False | |
419 | ||
420 | ||
421 | def OnSort(self, event): | |
422 | id = event.GetId() | |
423 | if id == OutlineService.SORT_ASC: | |
424 | wx.ConfigBase_Get().WriteInt("OutlineSort", SORT_ASC) | |
425 | self.GetView().OnSort(SORT_ASC) | |
426 | return True | |
427 | elif id == OutlineService.SORT_DESC: | |
428 | wx.ConfigBase_Get().WriteInt("OutlineSort", SORT_DESC) | |
429 | self.GetView().OnSort(SORT_DESC) | |
430 | return True | |
431 | elif id == OutlineService.SORT_NONE: | |
432 | wx.ConfigBase_Get().WriteInt("OutlineSort", SORT_NONE) | |
433 | self.GetView().OnSort(SORT_NONE) | |
434 | return True | |
435 | ||
436 | ||
437 | #---------------------------------------------------------------------------- | |
438 | # Service specific methods | |
439 | #---------------------------------------------------------------------------- | |
440 | ||
441 | def LoadOutline(self, view, position=-1, force=False): | |
442 | if not self.GetView(): | |
443 | return | |
444 | ||
445 | self.SaveExpansionState() | |
446 | if view.DoLoadOutlineCallback(force=force): | |
447 | self.GetView().OnSort(wx.ConfigBase_Get().ReadInt("OutlineSort", SORT_NONE)) | |
448 | self.LoadExpansionState() | |
449 | if position >= 0: | |
450 | self.SyncToPosition(position) | |
451 | ||
452 | ||
453 | def SyncToPosition(self, position): | |
454 | if not self.GetView(): | |
455 | return | |
456 | self.GetView().GetTreeCtrl().SelectClosestItem(position) | |
457 | ||
458 | ||
459 | def OnCloseFrame(self, event): | |
460 | Service.Service.OnCloseFrame(self, event) | |
461 | self.SaveExpansionState(clear = True) | |
462 | ||
463 | return True | |
464 | ||
465 | ||
466 | def SaveExpansionState(self, clear = False): | |
467 | if clear: | |
468 | expanded = [] | |
469 | elif self.GetView(): | |
470 | expanded = self.GetView().GetExpansionState() | |
471 | wx.ConfigBase_Get().Write("OutlineLastExpanded", expanded.__repr__()) | |
472 | ||
473 | ||
474 | def LoadExpansionState(self): | |
475 | expanded = wx.ConfigBase_Get().Read("OutlineLastExpanded") | |
476 | if expanded: | |
477 | self.GetView().SetExpansionState(eval(expanded)) | |
478 | ||
479 | ||
480 | #---------------------------------------------------------------------------- | |
481 | # Timer Methods | |
482 | #---------------------------------------------------------------------------- | |
483 | ||
484 | def StartBackgroundTimer(self): | |
485 | self._timer = wx.PyTimer(self.DoBackgroundRefresh) | |
486 | self._timer.Start(250) | |
487 | ||
488 | ||
489 | def DoBackgroundRefresh(self): | |
490 | """ Refresh the outline view periodically """ | |
491 | self._timer.Stop() | |
492 | ||
493 | foundRegisteredView = False | |
494 | if self.GetView(): | |
495 | currView = wx.GetApp().GetDocumentManager().GetCurrentView() | |
496 | if currView: | |
497 | for template in self._validTemplates: | |
498 | type = template.GetViewType() | |
499 | if isinstance(currView, type): | |
500 | self.LoadOutline(currView) | |
501 | foundRegisteredView = True | |
502 | break | |
503 | ||
504 | if not foundRegisteredView: | |
505 | self.GetView().ClearTreeCtrl() | |
506 | ||
507 | self._timer.Start(1000) # 1 second interval | |
508 | ||
509 | ||
510 | def AddTemplateForBackgroundHandler(self, template): | |
511 | self._validTemplates.append(template) | |
512 | ||
513 | ||
514 | def GetTemplatesForBackgroundHandler(self): | |
515 | return self._validTemplates | |
516 | ||
517 | ||
518 | def RemoveTemplateForBackgroundHandler(self, template): | |
519 | self._validTemplates.remove(template) | |
520 |