Package elisa :: Package plugins :: Package poblesec :: Package base :: Module list

Source Code for Module elisa.plugins.poblesec.base.list

  1  # -*- coding: utf-8 -*- 
  2  # Moovida - Home multimedia server 
  3  # Copyright (C) 2006-2009 Fluendo Embedded S.L. (www.fluendo.com). 
  4  # All rights reserved. 
  5  # 
  6  # This file is available under one of two license agreements. 
  7  # 
  8  # This file is licensed under the GPL version 3. 
  9  # See "LICENSE.GPL" in the root of this distribution including a special 
 10  # exception to use Moovida with Fluendo's plugins. 
 11  # 
 12  # The GPL part of Moovida is also available under a commercial licensing 
 13  # agreement from Fluendo. 
 14  # See "LICENSE.Moovida" in the root directory of this distribution package 
 15  # for details on that license. 
 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  # Here for backward compatibility, all deferreds in Elisa should be cancellable 
 29  # (and thus use elisa.core.utils.defer). 
 30  from twisted.internet import defer as twisted_defer 
 31   
 32   
 33  _ = install_translation('poblesec') 
 34   
 35   
36 -class GenericListViewMode(Component):
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
51 - def get_label(self, item):
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
67 - def get_sublabel(self, item):
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
83 - def get_default_image(self, item):
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
97 - def get_image(self, item, theme):
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
119 - def get_preview_image(self, item, theme):
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
138 - def get_contextual_background(self, item):
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
157 -class BaseListController(HierarchyController):
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
188 - def __init__(self):
189 super(BaseListController, self).__init__() 190 self.model = notifying_list.List() 191 self.shortcuts = notifying_list.List() 192 self.nodes = None 193 self.fastscroller = None 194 self._default_action = None 195 self._contextual_actions = [] 196 self._empty_alert_widget = None
197
198 - def initialize(self):
199 dfr = super(BaseListController, self).initialize() 200 201 # Contextual background's call later 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 # FIXME: make self._view_mode public 216 # (and rename view_mode to view_mode_cls) 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 # Respect initialize()'s API that requires to return self 229 dfr.addCallback(lambda result: self) 230 return dfr 231
232 - def clean(self):
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
245 - def _populate(self):
246 # Populate the model and the shortcuts accordingly if needed. 247 248 def model_populated(model): 249 self.model.extend(model)
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
276 - def reload(self):
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
293 - def populate_model(self):
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
306 - def create_actions(self):
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
321 - def set_frontend(self, frontend):
322 super(BaseListController, self).set_frontend(frontend) 323 324 # Create and render the list widget. 325 self.nodes_setup() 326 self.nodes.focus_before_activate = True 327 self.nodes.connect('item-activated', self._item_activated_cb) 328 self.nodes.connect('selected-item-changed', self._selected_item_cb) 329 self.nodes.set_model(self.model) 330 self.nodes.set_renderer(self.node_renderer) 331 332 # By default the focus is forwarded to the list widget 333 self.widget.set_focus_proxy(self.nodes) 334 335 if self.shortcuts: 336 self.fastscroller_setup() 337 self.fastscroller.connect('item-activated', 338 self._shortcut_activated) 339 self.fastscroller.set_model(self.shortcuts) 340 self.fastscroller.set_renderer(self.shortcut_renderer) 341 self.fastscroller.connect('state-changed', 342 self._fastscroller_stated_changed) 343 344 # layout components (fastscroller and nodes) 345 self.layout_components()
346
347 - def ready(self):
348 self._start_monitoring_model()
349
350 - def _monitor_model(self, *args):
351 if self.empty_label: 352 if len(self.model) == 0: 353 self.display_empty_alert(self.empty_label) 354 else: 355 self.hide_empty_alert()
356
357 - def _start_monitoring_model(self):
358 notifier = self.model.notifier 359 notifier.connect('items-deleted', self._monitor_model) 360 notifier.connect('items-inserted', self._monitor_model) 361 # Initial check. 362 self._monitor_model()
363
364 - def _stop_monitoring_model(self):
365 notifier = self.model.notifier 366 notifier.disconnect_by_func(self._monitor_model) 367 notifier.disconnect_by_func(self._monitor_model)
368
369 - def nodes_setup(self):
370 """ 371 Create the list widget. 372 373 This method should be overridden by subclasses. 374 """ 375 raise NotImplementedError()
376
377 - def fastscroller_setup(self):
378 """ 379 Create fastscroller. 380 381 This method should be overridden by subclasses. 382 """ 383 raise NotImplementedError()
384
385 - def layout_components(self):
386 """ 387 Layout fastscroller and nodes. 388 389 This method should be overridden by subclasses. 390 """ 391 pass
392
393 - def _build_shortcuts(self):
394 shortcuts = [] 395 396 def iterate_model(shortcuts): 397 for item in self.model: 398 shortcut = self.get_shortcut_for_item(item) 399 # we assume that self.model is sorted by shortcut 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
408 - def get_shortcut_for_item(self, item):
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
416 - def shortcut_renderer(self, shortcut, widget):
417 """ 418 DOCME 419 420 This method should be overridden by subclasses. 421 """ 422 raise NotImplementedError()
423
424 - def _get_item_index_for_shortcut(self, shortcut):
425 # needs to be optimised caching {shortcut -> item index} 426 for index, item in enumerate(self.model): 427 if self.get_shortcut_for_item(item) == shortcut: 428 return index
429
430 - def _shortcut_activated(self, widget, shortcut):
431 item_index = self._get_item_index_for_shortcut(shortcut) 432 self.nodes.selected_item_index = item_index
433
434 - def _get_shortcut_index_for_item_index(self, item_index):
435 item = self.model[item_index] 436 shortcut = self.get_shortcut_for_item(item) 437 return self.shortcuts.index(shortcut)
438
439 - def _fastscroller_stated_changed(self, fastscroller, previous_state):
440 if self.fastscroller.state == STATE_SELECTED: 441 item_index = self.nodes.selected_item_index 442 shortcut_index = self._get_shortcut_index_for_item_index(item_index) 443 self.fastscroller.selected_item_index = shortcut_index
444
445 - def item_activated(self, item):
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 # Handle gracefully the switch to the new API: fallback to the old API 469 # if needed. 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 # Catch any type of exception and wrap it in a deferred. 477 return defer.fail(error) 478 else: 479 if isinstance(result, twisted_defer.Deferred): 480 return result 481 else: 482 # Artificially wrap the result (if any) in a deferred 483 return defer.succeed(result)
484
485 - def _do_item_activated(self, item):
486 # TODO: cancel a potential previous call from another item 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
498 - def _item_activated_cb(self, widget, item):
499 self._do_item_activated(item)
500
501 - def node_clicked(self, widget, item):
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
517 - def _node_clicked_proxy(self, widget, item):
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
537 - def stop_loading_animation(self):
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
547 - def sensitive_set(self, value):
548 self._sensitive = value
549
550 - def sensitive_get(self):
551 return self._sensitive
552 553 sensitive = property(fget=sensitive_get, fset=sensitive_set) 554
555 - def display_empty_alert(self, label):
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 # There is already an empty alert displayed. 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
583 - def hide_empty_alert(self):
584 """ 585 Hide the alert widget that informs that the model is empty. 586 """ 587 if self._empty_alert_widget is None: 588 # The empty alert is not displayed. 589 return 590 591 self.widget.remove(self._empty_alert_widget) 592 self._empty_alert_widget = None
593
594 - def node_renderer(self, item, widget):
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
608 - def node_selected(self, widget, item, previous_item):
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
624 - def handle_input(self, manager, event):
625 """ 626 Specialisation that allows for key presses to the letter keys 627 to trigger a fast-scroll to the first label with a first letter 628 equal to the pressed letter. 629 """ 630 value_string = str(event.value) 631 if value_string in input_event.key_values: 632 key = value_string.lstrip("KEY_").upper() 633 if key in self.shortcuts: 634 item_index = self._get_item_index_for_shortcut(key) 635 self.nodes.selected_item_index = item_index 636 # Prevent other screens from handling the input 637 return True
638
639 - def _selected_item_cb(self, widget, item, previous_item):
640 self.node_selected(widget, item, previous_item) 641 self._schedule_load_background() 642 self._update_logo(item)
643
644 - def _update_logo(self, selected_item):
645 try: 646 source_icon = selected_item.source_icon 647 except AttributeError: 648 pass 649 else: 650 browser = self.frontend.retrieve_controllers('/poblesec/browser')[0] 651 browser.set_source_logo(source_icon)
652
653 - def _schedule_load_background(self):
654 # plan on loading the contextual background after a certain delay 655 # reset it if it was already counting down 656 657 # FIXME: hardcoded delay 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
665 - def _load_background(self):
666 # load a visual representation of the currently selected item 667 selected_item = self.model[self.nodes.selected_item_index] 668 669 def contextual_background_retrieved(background_path): 670 if background_path != None: 671 main = self.frontend.retrieve_controllers('/poblesec')[0] 672 main.background.load_file(background_path)
673 674 dfr = self._view_mode.get_contextual_background(selected_item) 675 dfr.addCallback(contextual_background_retrieved) 676