Package flumotion :: Package admin :: Package gtk :: Module componentlist
[hide private]

Source Code for Module flumotion.admin.gtk.componentlist

  1  # -*- Mode: Python; test-case-name: flumotion.test.test_parts -*- 
  2  # vi:si:et:sw=4:sts=4:ts=4 
  3   
  4  # Flumotion - a streaming media server 
  5  # Copyright (C) 2004,2005,2006,2007,2008,2009 Fluendo, S.L. 
  6  # Copyright (C) 2010,2011 Flumotion Services, S.A. 
  7  # All rights reserved. 
  8  # 
  9  # This file may be distributed and/or modified under the terms of 
 10  # the GNU Lesser General Public License version 2.1 as published by 
 11  # the Free Software Foundation. 
 12  # This file is distributed without any warranty; without even the implied 
 13  # warranty of merchantability or fitness for a particular purpose. 
 14  # See "LICENSE.LGPL" in the source distribution for more information. 
 15  # 
 16  # Headers in this file shall remain intact. 
 17   
 18  """widget to display a list of components. 
 19  This file contains a collection of widgets used to compose the list 
 20  of components used in the administration interface. 
 21  It contains: 
 22    - ComponentList: a treeview + treemodel abstraction 
 23    - ContextMenu: the menu which pops up when you right click 
 24  """ 
 25   
 26  import gettext 
 27  import operator 
 28  import os 
 29   
 30  import gobject 
 31  import gtk 
 32  from zope.interface import implements 
 33   
 34  from flumotion.configure import configure 
 35  from flumotion.common import log, planet 
 36  from flumotion.common.messages import ERROR, WARNING, INFO 
 37  from flumotion.common.planet import moods 
 38  from flumotion.common.pygobject import gsignal, gproperty 
 39  from flumotion.common.xmlwriter import cmpComponentType 
 40  from flumotion.twisted import flavors 
 41   
 42  __version__ = "$Rev$" 
 43  _ = gettext.gettext 
 44   
 45  _stock_icons = { 
 46      ERROR: gtk.STOCK_DIALOG_ERROR, 
 47      WARNING: gtk.STOCK_DIALOG_WARNING, 
 48      INFO: gtk.STOCK_DIALOG_INFO, 
 49      } 
 50   
 51  MOODS_INFO = { 
 52      moods.sad: _('Sad'), 
 53      moods.happy: _('Happy'), 
 54      moods.sleeping: _('Sleeping'), 
 55      moods.waking: _('Waking'), 
 56      moods.hungry: _('Hungry'), 
 57      moods.lost: _('Lost')} 
 58   
 59  (COL_MOOD, 
 60   COL_NAME, 
 61   COL_WORKER, 
 62   COL_PID, 
 63   COL_MSG, 
 64   COL_STATE, 
 65   COL_MOOD_VALUE, # to sort COL_MOOD 
 66   COL_TOOLTIP, 
 67   COL_FG, 
 68   COL_SAD) = range(10) 
 69   
 70  SAD_COLOR = "#FF0000" 
 71   
 72   
73 -def getComponentLabel(state):
74 config = state.get('config') 75 return config and config.get('label', config['name'])
76 77
78 -class ComponentList(log.Loggable, gobject.GObject):
79 """ 80 I present a view on the list of components logged in to the manager. 81 """ 82 83 implements(flavors.IStateListener) 84 85 logCategory = 'components' 86 87 gsignal('selection-changed', object) # state-or-None 88 gsignal('show-popup-menu', int, int) # button, click time 89 90 gproperty(bool, 'can-start-any', 'True if any component can be started', 91 False) 92 gproperty(bool, 'can-stop-any', 'True if any component can be stopped', 93 False) 94
95 - def __init__(self, treeView):
96 """ 97 @param treeView: the gtk.TreeView to put the view in. 98 """ 99 gobject.GObject.__init__(self) 100 self.set_property('can-start-any', False) 101 self.set_property('can-stop-any', False) 102 103 self._iters = {} # componentState -> model iter 104 self._lastStates = None 105 self._model = None 106 self._workers = [] 107 self._view = None 108 self._moodPixbufs = self._getMoodPixbufs() 109 self._createUI(treeView)
110
111 - def _createUI(self, treeView):
112 treeView.connect('button-press-event', 113 self._view_button_press_event_cb) 114 treeView.set_headers_visible(True) 115 116 treeModel = gtk.ListStore( 117 gtk.gdk.Pixbuf, # mood 118 str, # name 119 str, # worker 120 str, # pid 121 gtk.gdk.Pixbuf, # message level 122 object, # state 123 int, # mood-value 124 str, # tooltip 125 str, # color 126 bool, # colorize 127 ) 128 treeView.set_model(treeModel) 129 130 treeSelection = treeView.get_selection() 131 treeSelection.set_mode(gtk.SELECTION_MULTIPLE) 132 treeSelection.connect('changed', self._view_cursor_changed_cb) 133 134 # put in all the columns 135 col = gtk.TreeViewColumn('', gtk.CellRendererPixbuf(), 136 pixbuf=COL_MOOD) 137 col.set_sort_column_id(COL_MOOD_VALUE) 138 treeView.append_column(col) 139 140 col = gtk.TreeViewColumn(_('Component'), gtk.CellRendererText(), 141 text=COL_NAME, 142 foreground=COL_FG, 143 foreground_set=COL_SAD) 144 col.set_sort_column_id(COL_NAME) 145 treeView.append_column(col) 146 147 col = gtk.TreeViewColumn(_('Worker'), gtk.CellRendererText(), 148 markup=COL_WORKER, 149 foreground=COL_FG, 150 foreground_set=COL_SAD) 151 col.set_sort_column_id(COL_WORKER) 152 treeView.append_column(col) 153 154 t = gtk.CellRendererText() 155 col = gtk.TreeViewColumn(_('PID'), t, markup=COL_PID, 156 foreground=COL_FG, 157 foreground_set=COL_SAD) 158 col.set_sort_column_id(COL_PID) 159 treeView.append_column(col) 160 161 col = gtk.TreeViewColumn('', gtk.CellRendererPixbuf(), 162 pixbuf=COL_MSG) 163 treeView.append_column(col) 164 165 166 if gtk.pygtk_version >= (2, 12): 167 treeView.set_tooltip_column(COL_TOOLTIP) 168 169 if hasattr(gtk.TreeView, 'set_rubber_banding'): 170 treeView.set_rubber_banding(False) 171 172 self._model = treeModel 173 self._view = treeView
174
175 - def getSelectedNames(self):
176 """ 177 Get the names of the currently selected components, or None if none 178 are selected. 179 180 @rtype: list of str or None 181 """ 182 return self._getSelected(COL_NAME)
183
184 - def getSelectedStates(self):
185 """ 186 Get the states of the currently selected components, or None if none 187 are selected. 188 189 @rtype: list of L{flumotion.common.component.AdminComponentState} 190 or None 191 """ 192 return self._getSelected(COL_STATE)
193
194 - def getComponentNames(self):
195 """ 196 Fetches a list of all component names. 197 198 @returns: component names 199 @rtype: list of str 200 """ 201 names = [] 202 for row in self._model: 203 names.append(row[COL_NAME]) 204 return names
205
206 - def getComponentStates(self):
207 """ 208 Fetches a list of all component states 209 210 @returns: component states 211 @rtype: list of L{AdminComponentState} 212 """ 213 names = [] 214 for row in self._model: 215 names.append(row[COL_STATE]) 216 return names
217
218 - def canDelete(self):
219 """ 220 Get whether the selected components can be deleted. 221 222 Returns True if all components are sleeping. 223 224 Also returns False if no components are selected. 225 226 @rtype: bool 227 """ 228 states = self.getSelectedStates() 229 if not states: 230 return False 231 canDelete = True 232 for state in states: 233 moodname = moods.get(state.get('mood')).name 234 canDelete = canDelete and moodname == 'sleeping' 235 return canDelete
236
237 - def canStart(self):
238 """ 239 Get whether the selected components can be started. 240 241 Returns True if all components are sleeping and their worked has 242 logged in. 243 244 Also returns False if no components are selected. 245 246 @rtype: bool 247 """ 248 # additionally to canDelete, the worker needs to be logged intoo 249 if not self.canDelete(): 250 return False 251 252 canStart = True 253 states = self.getSelectedStates() 254 for state in states: 255 workerName = state.get('workerRequested') 256 canStart = canStart and workerName in self._workers 257 258 return canStart
259
260 - def canStop(self):
261 """ 262 Get whether the selected components can be stopped. 263 264 Returns True if none of the components are sleeping. 265 266 Also returns False if no components are selected. 267 268 @rtype: bool 269 """ 270 states = self.getSelectedStates() 271 if not states: 272 return False 273 canStop = True 274 for state in states: 275 moodname = moods.get(state.get('mood')).name 276 canStop = canStop and moodname != 'sleeping' 277 return canStop
278
279 - def clearAndRebuild(self, components, componentNameToSelect=None):
280 """ 281 Update the components view by removing all old components and 282 showing the new ones. 283 284 @param components: dictionary of name -> 285 L{flumotion.common.component.AdminComponentState} 286 @param componentNameToSelect: name of the component to select or None 287 """ 288 # remove all Listeners 289 self._model.foreach(self._removeListenerForeach) 290 291 self.debug('updating components view') 292 # clear and rebuild 293 self._view.get_selection().unselect_all() 294 self._model.clear() 295 self._iters = {} 296 297 components = sorted(components.values(), 298 cmp=cmpComponentType, 299 key=operator.itemgetter('type')) 300 301 for component in components: 302 self.appendComponent(component, componentNameToSelect) 303 304 self.debug('updated components view')
305
306 - def appendComponent(self, component, componentNameToSelect):
307 self.debug('adding component %r to listview' % component) 308 component.addListener(self, set_=self.stateSet, append=self.stateSet, 309 remove=self.stateSet) 310 311 titer = self._model.append() 312 self._iters[component] = titer 313 314 mood = component.get('mood') 315 self.debug('component has mood %r' % mood) 316 messages = component.get('messages') 317 self.debug('component has messages %r' % messages) 318 self._setMsgLevel(titer, messages) 319 320 if mood != None: 321 self._setMoodValue(titer, mood) 322 323 self._model.set(titer, COL_FG, SAD_COLOR) 324 self._model.set(titer, COL_STATE, component) 325 componentName = getComponentLabel(component) 326 self._model.set(titer, COL_NAME, componentName) 327 328 pid = component.get('pid') 329 if not pid and component.hasKey('lastKnownPid'): 330 if component.get('lastKnownPid'): 331 pid = "<i>%d</i>" % component.get('lastKnownPid') 332 self._model.set(titer, COL_PID, (pid and str(pid)) or '') 333 334 self._updateWorker(titer, component) 335 selection = self._view.get_selection() 336 if (componentNameToSelect is not None and 337 componentName == componentNameToSelect and 338 not selection.get_selected_rows()[1]): 339 selection.select_iter(titer) 340 341 self._updateStartStop()
342
343 - def removeComponent(self, component):
344 self.debug('removing component %r to listview' % component) 345 346 titer = self._iters[component] 347 self._model.remove(titer) 348 del self._iters[component] 349 350 self._updateStartStop()
351 352 # IStateListener implementation 353
354 - def stateSet(self, state, key, value):
355 if not isinstance(state, planet.AdminComponentState): 356 self.warning('Got state change for unknown object %r' % state) 357 return 358 359 titer = self._iters[state] 360 self.log('stateSet: state %r, key %s, value %r' % (state, key, value)) 361 362 if key == 'mood': 363 self.debug('stateSet: mood of %r changed to %r' % (state, value)) 364 365 if value == moods.sleeping.value: 366 self.debug('sleeping, removing local messages on %r' % state) 367 for message in state.get('messages', []): 368 state.observe_remove('messages', message) 369 370 self._setMoodValue(titer, value) 371 self._updateWorker(titer, state) 372 elif key == 'name': 373 if value: 374 self._model.set(titer, COL_NAME, value) 375 elif key == 'workerName': 376 self._updateWorker(titer, state) 377 elif key == 'pid': 378 self._model.set(titer, COL_PID, (value and str(value) or '')) 379 elif key =='messages': 380 self._setMsgLevel(titer, state.get('messages'))
381 382 # Private 383
384 - def _setMsgLevel(self, titer, messages):
385 icon = None 386 387 if messages: 388 messages = sorted(messages, cmp=lambda x, y: x.level - y.level) 389 level = messages[0].level 390 st = _stock_icons.get(level, gtk.STOCK_MISSING_IMAGE) 391 w = gtk.Invisible() 392 icon = w.render_icon(st, gtk.ICON_SIZE_MENU) 393 394 self._model.set(titer, COL_MSG, icon)
395
396 - def _updateStartStop(self):
397 oldstop = self.get_property('can-stop-any') 398 oldstart = self.get_property('can-start-any') 399 moodnames = [moods.get(x[COL_MOOD_VALUE]).name for x in self._model] 400 canStop = bool([x for x in moodnames if (x!='sleeping')]) 401 canStart = bool([x for x in moodnames if (x=='sleeping')]) 402 if oldstop != canStop: 403 self.set_property('can-stop-any', canStop) 404 if oldstart != canStart: 405 self.set_property('can-start-any', canStart)
406
407 - def workerAppend(self, name):
408 self._workers.append(name)
409
410 - def workerRemove(self, name):
411 self._workers.remove(name) 412 for state, titer in self._iters.items(): 413 self._updateWorker(titer, state)
414
415 - def _updateWorker(self, titer, componentState):
416 # update the worker name: 417 # - italic if workerName and workerRequested are not running 418 # - normal if running 419 420 workerName = componentState.get('workerName') 421 workerRequested = componentState.get('workerRequested') 422 if not workerName and not workerRequested: 423 #FIXME: Should we raise an error here? 424 # It's an impossible situation. 425 workerName = _("[any worker]") 426 427 markup = workerName or workerRequested 428 if markup not in self._workers: 429 self._model.set(titer, COL_TOOLTIP, 430 _("<b>Worker %s is not connected</b>") % markup) 431 markup = "<i>%s</i>" % markup 432 self._model.set(titer, COL_WORKER, markup)
433
434 - def _removeListenerForeach(self, model, path, titer):
435 # remove the listener for each state object 436 state = model.get(titer, COL_STATE)[0] 437 state.removeListener(self)
438
439 - def _setMoodValue(self, titer, value):
440 """ 441 Set the mood value on the given component name. 442 443 @type value: int 444 """ 445 self._model.set(titer, COL_MOOD, self._moodPixbufs[value]) 446 self._model.set(titer, COL_MOOD_VALUE, value) 447 self._model.set(titer, COL_SAD, moods.sad.value == value) 448 mood = moods.get(value) 449 self._model.set(titer, COL_TOOLTIP, 450 _("<b>Component is %s</b>") % (MOODS_INFO[mood].lower(), )) 451 452 self._updateStartStop()
453
454 - def _getSelected(self, col_name):
455 # returns None if no components are selected, a list otherwise 456 selection = self._view.get_selection() 457 if not selection: 458 return None 459 model, selected_tree_rows = selection.get_selected_rows() 460 selected = [] 461 for tree_row in selected_tree_rows: 462 component_state = model[tree_row][col_name] 463 selected.append(component_state) 464 return selected
465
466 - def _getMoodPixbufs(self):
467 # load all pixbufs for the moods 468 pixbufs = {} 469 for i in range(0, len(moods)): 470 name = moods.get(i).name 471 pixbufs[i] = gtk.gdk.pixbuf_new_from_file_at_size( 472 os.path.join(configure.imagedir, 'mood-%s.png' % name), 473 24, 24) 474 475 return pixbufs
476
477 - def _selectionChanged(self):
478 states = self.getSelectedStates() 479 480 if not states: 481 self.debug( 482 'no component selected, emitting selection-changed None') 483 # Emit this in an idle, since popups will not be shown 484 # before this has completed, and it might possibly take a long 485 # time to finish all the callbacks connected to selection-changed 486 # This is not the proper fix, but makes the popups show up faster 487 gobject.idle_add(self.emit, 'selection-changed', []) 488 return 489 490 if states == self._lastStates: 491 self.debug('no new components selected, no emitting signal') 492 return 493 494 self.debug('components selected, emitting selection-changed') 495 self.emit('selection-changed', states) 496 self._lastStates = states
497
498 - def _showPopupMenu(self, event):
499 selection = self._view.get_selection() 500 retval = self._view.get_path_at_pos(int(event.x), int(event.y)) 501 if retval is None: 502 selection.unselect_all() 503 return 504 clicked_path = retval[0] 505 selected_path = selection.get_selected_rows()[1] 506 if clicked_path not in selected_path: 507 selection.unselect_all() 508 selection.select_path(clicked_path) 509 self.emit('show-popup-menu', event.button, event.time)
510 511 # Callbacks 512
513 - def _view_cursor_changed_cb(self, *args):
514 self._selectionChanged()
515
516 - def _view_button_press_event_cb(self, treeview, event):
517 if event.button == 3: 518 self._showPopupMenu(event) 519 return True 520 return False
521 522 523 gobject.type_register(ComponentList) 524 525 526 # this file can be run to test ComponentList 527 if __name__ == '__main__': 528 529 from twisted.internet import reactor 530 from twisted.spread import jelly 531
532 - class Main:
533
534 - def __init__(self):
535 self.window = gtk.Window() 536 self.widget = gtk.TreeView() 537 self.window.add(self.widget) 538 self.window.show_all() 539 self.view = ComponentList(self.widget) 540 self.view.connect('selection-changed', self._selection_changed_cb) 541 self.view.connect('show-popup-menu', self._show_popup_menu_cb) 542 self.window.connect('destroy', gtk.main_quit)
543
544 - def _createComponent(self, dict):
545 mstate = planet.ManagerComponentState() 546 for key in dict.keys(): 547 mstate.set(key, dict[key]) 548 astate = jelly.unjelly(jelly.jelly(mstate)) 549 return astate
550
551 - def tearDown(self):
552 self.window.destroy()
553
554 - def update(self):
555 components = {} 556 c = self._createComponent( 557 {'config': {'name': 'one'}, 558 'mood': moods.happy.value, 559 'workerName': 'R2D2', 'pid': 1, 'type': 'dummy'}) 560 components['one'] = c 561 c = self._createComponent( 562 {'config': {'name': 'two'}, 563 'mood': moods.sad.value, 564 'workerName': 'R2D2', 'pid': 2, 'type': 'dummy'}) 565 components['two'] = c 566 c = self._createComponent( 567 {'config': {'name': 'three'}, 568 'mood': moods.hungry.value, 569 'workerName': 'C3PO', 'pid': 3, 'type': 'dummy'}) 570 components['three'] = c 571 c = self._createComponent( 572 {'config': {'name': 'four'}, 573 'mood': moods.sleeping.value, 574 'workerName': 'C3PO', 'pid': None, 'type': 'dummy'}) 575 components['four'] = c 576 self.view.clearAndRebuild(components)
577
578 - def _selection_changed_cb(self, view, states):
579 # states: list of AdminComponentState 580 print "Selected component(s) %s" % ", ".join( 581 [s.get('config')['name'] for s in states])
582
583 - def _show_popup_menu_cb(self, view, button, time):
584 print "Pressed button %r at time %r" % (button, time)
585 586 587 app = Main() 588 589 app.update() 590 591 gtk.main() 592