1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 from elisa.core.component import Component
18 from elisa.core.utils.i18n import install_translation
19 from elisa.core.utils import defer, notifying_list
20 from elisa.core import input_event
21
22 from elisa.plugins.poblesec.base.hierarchy import HierarchyController
23 from elisa.plugins.poblesec.modal_popup import Popup
24 from elisa.plugins.pigment.widgets.const import STATE_SELECTED, STATE_LOADING
25
26 from twisted.internet import task, reactor
27
28
29
30 from twisted.internet import defer as twisted_defer
31
32
33 _ = install_translation('poblesec')
34
35
37
38 """
39 Generic view mode API.
40
41 It defines a common API for clients. All one has to do is inherit from this
42 class and implement the following methods:
43
44 - C{get_label(item)}
45 - C{get_sublabel(item)}
46 - C{get_default_image(item)}
47 - C{get_image(item, theme)}
48 - C{get_preview_image(item, theme)}
49 """
50
52 """
53 Return a text to display in a label to represent an item.
54
55 This call is asynchronous, it should return a
56 L{elisa.core.utils.cancellable_defer.CancellableDeferred} that, when
57 triggered, returns the text of the label.
58
59 @param item: a list item
60 @type item: a subclass of L{elisa.core.components.model.Model}
61
62 @return: a cancellable deferred
63 @rtype: L{elisa.core.utils.cancellable_defer.CancellableDeferred}
64 """
65 return defer.fail(NotImplementedError())
66
68 """
69 Return a text to display in a sublabel to represent an item.
70
71 This call is asynchronous, it should return a
72 L{elisa.core.utils.cancellable_defer.CancellableDeferred} that, when
73 triggered, returns the text of the sublabel.
74
75 @param item: a list item
76 @type item: a subclass of L{elisa.core.components.model.Model}
77
78 @return: a cancellable deferred
79 @rtype: L{elisa.core.utils.cancellable_defer.CancellableDeferred}
80 """
81 return defer.fail(NotImplementedError())
82
84 """
85 Return the path of a theme resource to display as a default image for
86 an item.
87
88 @param item: a list item
89 @type item: a subclass of L{elisa.core.components.model.Model}
90
91 @return: the path of a theme resource to display as a default
92 image for the item
93 @rtype: C{str}
94 """
95 raise NotImplementedError()
96
98 """
99 Return the path to an image file to display as an image for an item.
100
101 This call is asynchronous, it should return a
102 L{elisa.core.utils.cancellable_defer.CancellableDeferred} that, when
103 triggered, returns the path to an image file on disk (downloaded and
104 cached if necessary).
105
106 If no other image than the default one is necessary/available, this
107 method should return C{None}.
108
109 @param item: a list item
110 @type item: a subclass of L{elisa.core.components.model.Model}
111 @param theme: the frontend's current theme
112 @type theme: L{elisa.plugins.pigment.widgets.theme.Theme}
113
114 @return: a cancellable deferred or C{None}
115 @rtype: L{elisa.core.utils.cancellable_defer.CancellableDeferred}
116 """
117 return defer.fail(NotImplementedError())
118
120 """
121 Return the path to an image file to display as a preview image for an
122 item.
123
124 This call is synchronous, if no preview image is available yet for the
125 item or if no other image than the default one is necessary, it should
126 return C{None}.
127
128 @param item: a list item
129 @type item: a subclass of L{elisa.core.components.model.Model}
130 @param theme: the frontend's current theme
131 @type theme: L{elisa.plugins.pigment.widgets.theme.Theme}
132
133 @return: the path to an image file on disk or C{None}
134 @rtype: C{str} or C{None}
135 """
136 raise NotImplementedError()
137
139 """
140 Return the path to an image file to display as a contextual background
141 for an item.
142
143 This call is asynchronous, it should return a
144 L{elisa.core.utils.cancellable_defer.CancellableDeferred} that, when
145 triggered, returns the path to an image file on disk (downloaded and
146 cached if necessary) or C{None} if not availadble.
147
148 @param item: a list item
149 @type item: a subclass of L{elisa.core.components.model.Model}
150
151 @return: a cancellable deferred
152 @rtype: L{elisa.core.utils.cancellable_defer.CancellableDeferred}
153 """
154 return defer.succeed(None)
155
156
158
159 """
160 Base list controller with a common API for all list-like controllers.
161
162 @ivar model: the data model
163 @type model: L{elisa.core.utils.notifying_list.List}
164 @ivar nodes: the list widget
165 @type nodes: L{elisa.plugins.pigment.widgets.list.List}
166 @ivar fastscroller: DOCME
167 @type fastscroller: L{elisa.plugins.pigment.widgets.list.List}
168 @ivar shortcuts: the fastscroller's model
169 @type shortcuts: L{elisa.core.utils.notifying_list.List}
170
171 @cvar fastscroller_enabled: whether to show a fastscroller
172 (C{False} by default)
173 @type fastscroller_enabled: C{bool}
174 @cvar fastscroller_threshold: DOCME
175 @type fastscroller_threshold: C{int}
176 @cvar view_mode: DOCME
177 @type view_mode: DOCME
178 @cvar empty_label: text shown when there no models are to
179 be displayed
180 @type empty_label: str
181 """
182
183 fastscroller_enabled = False
184 fastscroller_threshold = 20
185 view_mode = GenericListViewMode
186 empty_label = None
187
197
199 dfr = super(BaseListController, self).initialize()
200
201
202 self._delayed_loading = None
203
204 def populate(result):
205 return self._populate()
206
207 def create_actions(result):
208 self._default_action, self._contextual_actions = \
209 self.create_actions()
210
211 def create_view_mode(result):
212 return self.view_mode.create()
213
214 def view_mode_created(view_mode):
215
216
217 self._view_mode = view_mode
218
219 def error_creating_view_mode(failure):
220 self.warning('Error creating view mode: %s' % \
221 failure.getErrorMessage())
222 return failure
223
224 dfr.addCallback(populate)
225 dfr.addCallback(create_actions)
226 dfr.addCallback(create_view_mode)
227 dfr.addCallbacks(view_mode_created, error_creating_view_mode)
228
229 dfr.addCallback(lambda result: self)
230 return dfr
231
233 if self._delayed_loading != None and self._delayed_loading.active():
234 self._delayed_loading.cancel()
235 self._delayed_loading = None
236 if self.fastscroller is not None:
237 self.fastscroller.disconnect_by_func(self._fastscroller_stated_changed)
238 self.fastscroller.disconnect_by_func(self._shortcut_activated)
239 self.nodes.disconnect_by_func(self._selected_item_cb)
240 self.nodes.disconnect_by_func(self._item_activated_cb)
241 self._stop_monitoring_model()
242 self.model[:] = []
243 return super(BaseListController, self).clean()
244
250
251 def error_populating_model(failure):
252 self.warning('Error populating model: %s' % \
253 failure.getErrorMessage())
254 return failure
255
256 def populate_shortcuts(result):
257 if self.fastscroller_enabled and \
258 len(self.model) > self.fastscroller_threshold:
259 return self._build_shortcuts()
260
261 def shortcuts_populated(shortcuts):
262 if shortcuts:
263 self.shortcuts.extend(shortcuts)
264
265 def error_populating_shortcuts(failure):
266 self.warning('Error populating shortcuts: %s' % \
267 failure.getErrorMessage())
268 return failure
269
270 dfr = self.populate_model()
271 dfr.addCallbacks(model_populated, error_populating_model)
272 dfr.addCallback(populate_shortcuts)
273 dfr.addCallbacks(shortcuts_populated, error_populating_shortcuts)
274 return dfr
275
277 """
278 Reload the model as it is done at initialization time.
279 Re-build accordingly the shortcuts.
280
281 @return: a deferred fired when the reload is complete
282 @rtype: L{elisa.core.utils.defer.Deferred}
283 """
284 self._stop_monitoring_model()
285 if self.shortcuts:
286 self.shortcuts[:] = []
287 if self.model:
288 self.model[:] = []
289 dfr = self._populate()
290 dfr.addCallback(lambda result: self._start_monitoring_model())
291 return dfr
292
294 """
295 Initial population of the data model (C{self.model}).
296
297 This method should be overridden by subclasses. The default
298 implementation returns an empty list.
299
300 @return: a deferred fired when the population of the model is complete,
301 with the resulting model (of type C{list})
302 @rtype: L{elisa.core.utils.defer.Deferred}
303 """
304 return defer.succeed([])
305
307 """
308 Create the default action and the contextual actions associated to the
309 type of item the controller will be presenting.
310
311 The default implementation does create any action, subclasses should
312 override this method.
313
314 @return: a 2-tuple containing the default action and a list of
315 contextual actions
316 @rtype: (L{elisa.core.action.ContextualAction}, C{list}
317 of L{elisa.core.action.ContextualAction})
318 """
319 return None, []
320
346
348 self._start_monitoring_model()
349
356
358 notifier = self.model.notifier
359 notifier.connect('items-deleted', self._monitor_model)
360 notifier.connect('items-inserted', self._monitor_model)
361
362 self._monitor_model()
363
365 notifier = self.model.notifier
366 notifier.disconnect_by_func(self._monitor_model)
367 notifier.disconnect_by_func(self._monitor_model)
368
370 """
371 Create the list widget.
372
373 This method should be overridden by subclasses.
374 """
375 raise NotImplementedError()
376
384
386 """
387 Layout fastscroller and nodes.
388
389 This method should be overridden by subclasses.
390 """
391 pass
392
394 shortcuts = []
395
396 def iterate_model(shortcuts):
397 for item in self.model:
398 shortcut = self.get_shortcut_for_item(item)
399
400 if shortcut is not None and shortcut not in shortcuts:
401 shortcuts.append(shortcut)
402 yield None
403
404 dfr = task.coiterate(iterate_model(shortcuts))
405 dfr.addCallback(lambda result: shortcuts)
406 return dfr
407
409 """
410 @return: a shortcut for the item, or C{None}
411
412 This method should be overridden by subclasses.
413 """
414 raise NotImplementedError()
415
417 """
418 DOCME
419
420 This method should be overridden by subclasses.
421 """
422 raise NotImplementedError()
423
429
431 item_index = self._get_item_index_for_shortcut(shortcut)
432 self.nodes.selected_item_index = item_index
433
438
444
446 """
447 Callback invoked when an item is activated.
448
449 The default implementation executes the default action associated with
450 the item.
451
452 This method should be overriden by subclasses that do not make use of
453 contextual actions.
454
455 @param item: the item that was activated
456 @type item: L{elisa.core.components.model.Model}
457
458 @return: a deferred fired when the action taken is complete
459 @rtype: L{elisa.core.utils.defer.Deferred}
460 """
461 default_action = self._default_action
462 if hasattr(item, 'default_action') and item.default_action is not None:
463 default_action = item.default_action
464
465 if default_action:
466 return default_action.execute(item)
467
468
469
470 try:
471 result = self._node_clicked_proxy(None, item)
472 except DeprecationWarning, error:
473 self.warning(error.message)
474 return defer.fail(item)
475 except BaseException, error:
476
477 return defer.fail(error)
478 else:
479 if isinstance(result, twisted_defer.Deferred):
480 return result
481 else:
482
483 return defer.succeed(result)
484
486
487 widget = self.nodes._widget_from_item_index(self.model.index(item))
488 previous_state = widget.state
489 widget.state = STATE_LOADING
490
491 def reset_widget_state(result_or_failure, widget, previous_state):
492 widget.state = previous_state
493
494 dfr = self.item_activated(item)
495 dfr.addBoth(reset_widget_state, widget, previous_state)
496 return dfr
497
499 self._do_item_activated(item)
500
502 """
503 [DEPRECATED] Callback invoked when an item of the list representing a
504 given level of the hierarchy is clicked.
505
506 @deprecated: implement C{item_activated} instead
507
508 @param widget: the selected list item widget in the view
509 @type widget: L{elisa.plugins.pigment.widgets.widget.Widget}
510 @param item: the selected list item in the controller's model
511 @type item: L{elisa.core.components.model.Model}
512 """
513 msg = 'This interface is deprecated, ' \
514 'please implement item_activated instead'
515 raise DeprecationWarning(msg)
516
518 """
519 [DEPRECATED] This method will be removed once we manage to completely
520 unify the way items are activated (mouse, keyboard, remote control)
521 using the item-activated signal.
522
523 This method is triggered by the widget item-clicked signal. It figures
524 out the widget that was really clicked, checks if it is still in a
525 previous_clicked mode or not sensitive to clicks. If we should react it
526 calls self.node_clicked (the public method) with the selected widget as
527 the first parameter and the item as the second parameter.
528 """
529 if not self.sensitive:
530 return
531
532 selected_widget = \
533 self.nodes._widget_from_item_index(self.model.index(item))
534
535 return self.node_clicked(selected_widget, item)
536
538 """
539 [DEPRECATED] This method can be safely removed once the transition from
540 using node_clicked to implementing item_activated is complete.
541 """
542 msg = 'This method is deprecated. ' \
543 'The base list controller now handles ' \
544 'the loading state of widgets on its own.'
545 raise DeprecationWarning(msg)
546
548 self._sensitive = value
549
551 return self._sensitive
552
553 sensitive = property(fget=sensitive_get, fset=sensitive_set)
554
556 """
557 Display an alert widget to inform that the model is empty.
558
559 @param label: the text of the alert
560 @type label: C{unicode}
561 """
562 if self._empty_alert_widget is not None:
563
564 return
565
566 def go_back():
567 browser = self.frontend.retrieve_controllers('/poblesec/browser')[0]
568 browser.history.go_back()
569
570 title = _('EMPTY SECTION')
571 subtitle = label
572 text = ''
573 buttons = [(_('Back'), go_back)]
574
575 self._empty_alert_widget = Popup(title, subtitle, text, buttons)
576 self._empty_alert_widget.set_name('empty_alert')
577 self._empty_alert_widget.visible = True
578 self.widget.add(self._empty_alert_widget)
579 self.widget.set_focus_proxy(self._empty_alert_widget)
580 if self.widget.focus:
581 self._empty_alert_widget.set_focus()
582
584 """
585 Hide the alert widget that informs that the model is empty.
586 """
587 if self._empty_alert_widget is None:
588
589 return
590
591 self.widget.remove(self._empty_alert_widget)
592 self._empty_alert_widget = None
593
595 """
596 Render an item in a list widget.
597
598 The default implementation does nothing. Subclasses should override
599 this method to control how items are visually rendered.
600
601 @param item: the item to render
602 @type item: L{elisa.core.components.model.Model}
603 @param widget: the widget in which to render the item
604 @type widget: L{elisa.plugins.pigment.widgets.widget.Widget}
605 """
606 pass
607
609 """
610 Callback invoked when an item is selected.
611
612 The default implementation does nothing.
613 Subclasses should override if an action is to be taken.
614
615 @param widget: the list widget
616 @type widget: L{elisa.plugins.pigment.widgets.list.List}
617 @param item: the newly selected item
618 @type item: L{elisa.core.components.model.Model}
619 @param previous_item: the previously selected item
620 @type previous_item: L{elisa.core.components.model.Model}
621 """
622 pass
623
638
640 self.node_selected(widget, item, previous_item)
641 self._schedule_load_background()
642 self._update_logo(item)
643
652
654
655
656
657
658 delay = 1.0
659 if self._delayed_loading != None and self._delayed_loading.active():
660 self._delayed_loading.reset(delay)
661 else:
662 self._delayed_loading = reactor.callLater(delay,
663 self._load_background)
664
673
674 dfr = self._view_mode.get_contextual_background(selected_item)
675 dfr.addCallback(contextual_background_retrieved)
676