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