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 os 
 32   
 33  import gobject 
 34  import gtk 
 35  from zope.interface import implements 
 36   
 37  from flumotion.configure import configure 
 38  from flumotion.common import log, planet 
 39  from flumotion.common.planet import moods 
 40  from flumotion.common.pygobject import gsignal, gproperty 
 41  from flumotion.common.xmlwriter import cmpComponentType 
 42  from flumotion.twisted import flavors 
 43   
 44  __version__ = "$Rev: 8152 $" 
 45  _ = gettext.gettext 
 46  MOODS_INFO = { 
 47      moods.sad: _('Sad'), 
 48      moods.happy: _('Happy'), 
 49      moods.sleeping: _('Sleeping'), 
 50      moods.waking: _('Waking'), 
 51      moods.hungry: _('Hungry'), 
 52      moods.lost: _('Lost')} 
 53   
 54  (COL_MOOD, 
 55   COL_NAME, 
 56   COL_WORKER, 
 57   COL_PID, 
 58   COL_STATE, 
 59   COL_MOOD_VALUE, # to sort COL_MOOD 
 60   COL_CPU, 
 61   COL_TOOLTIP) = range(8) 
 62   
 63   
64 -def getComponentLabel(state):
65 config = state.get('config') 66 return config and config.get('label', config['name'])
67 68
69 -class ComponentList(log.Loggable, gobject.GObject):
70 """ 71 I present a view on the list of components logged in to the manager. 72 """ 73 74 implements(flavors.IStateListener) 75 76 logCategory = 'components' 77 78 gsignal('selection-changed', object) # state-or-None 79 gsignal('show-popup-menu', int, int) # button, click time 80 81 gproperty(bool, 'can-start-any', 'True if any component can be started', 82 False) 83 gproperty(bool, 'can-stop-any', 'True if any component can be stopped', 84 False) 85
86 - def __init__(self, treeView):
87 """ 88 @param treeView: the gtk.TreeView to put the view in. 89 """ 90 gobject.GObject.__init__(self) 91 self.set_property('can-start-any', False) 92 self.set_property('can-stop-any', False) 93 94 self._iters = {} # componentState -> model iter 95 self._lastStates = None 96 self._model = None 97 self._workers = [] 98 self._view = None 99 self._moodPixbufs = self._getMoodPixbufs() 100 self._createUI(treeView)
101
102 - def _createUI(self, treeView):
103 treeView.connect('button-press-event', 104 self._view_button_press_event_cb) 105 treeView.set_headers_visible(True) 106 107 treeModel = gtk.ListStore( 108 gtk.gdk.Pixbuf, # mood 109 str, # name 110 str, # worker 111 str, # pid 112 object, # state 113 int, # mood-value 114 str, # cpu 115 str, # tooltip 116 ) 117 treeView.set_model(treeModel) 118 119 treeSelection = treeView.get_selection() 120 treeSelection.set_mode(gtk.SELECTION_MULTIPLE) 121 treeSelection.connect('changed', self._view_cursor_changed_cb) 122 123 # put in all the columns 124 col = gtk.TreeViewColumn('', gtk.CellRendererPixbuf(), 125 pixbuf=COL_MOOD) 126 col.set_sort_column_id(COL_MOOD_VALUE) 127 treeView.append_column(col) 128 129 col = gtk.TreeViewColumn(_('Component'), gtk.CellRendererText(), 130 text=COL_NAME) 131 col.set_sort_column_id(COL_NAME) 132 treeView.append_column(col) 133 134 col = gtk.TreeViewColumn(_('Worker'), gtk.CellRendererText(), 135 markup=COL_WORKER) 136 col.set_sort_column_id(COL_WORKER) 137 treeView.append_column(col) 138 139 t = gtk.CellRendererText() 140 col = gtk.TreeViewColumn(_('PID'), t, text=COL_PID) 141 col.set_sort_column_id(COL_PID) 142 treeView.append_column(col) 143 if gtk.pygtk_version >= (2, 12): 144 treeView.set_tooltip_column(COL_TOOLTIP) 145 146 if hasattr(gtk.TreeView, 'set_rubber_banding'): 147 treeView.set_rubber_banding(False) 148 149 self._model = treeModel 150 self._view = treeView
151
152 - def getSelectedNames(self):
153 """ 154 Get the names of the currently selected component, or None. 155 156 @rtype: string 157 """ 158 return self._getSelected(COL_NAME)
159
160 - def getSelectedStates(self):
161 """ 162 Get the states of the currently selected component, or None. 163 164 @rtype: L{flumotion.common.component.AdminComponentState} 165 """ 166 return self._getSelected(COL_STATE)
167
168 - def getComponentNames(self):
169 """Fetches a list of all component names 170 @returns: component names 171 @rtype: list of strings 172 """ 173 names = [] 174 for row in self._model: 175 names.append(row[COL_NAME]) 176 return names
177
178 - def getComponentStates(self):
179 """Fetches a list of all component states 180 @returns: component states 181 @rtype: list of L{AdminComponentState} 182 """ 183 names = [] 184 for row in self._model: 185 names.append(row[COL_STATE]) 186 return names
187
188 - def canDelete(self):
189 """ 190 Get whether the selected components can be deleted. 191 192 Returns True if all components are sleeping. 193 194 Also returns False if no components are selected. 195 196 @rtype: bool 197 """ 198 states = self.getSelectedStates() 199 if not states: 200 return False 201 canDelete = True 202 for state in states: 203 moodname = moods.get(state.get('mood')).name 204 workerName = state.get('workerRequested') 205 canDelete = canDelete and moodname == 'sleeping' 206 return canDelete
207
208 - def canStart(self):
209 """ 210 Get whether the selected components can be started. 211 212 @rtype: bool 213 """ 214 # additionally to canDelete, the worker needs to be logged intoo 215 if not self.canDelete(): 216 return False 217 218 canStart = True 219 states = self.getSelectedStates() 220 for state in states: 221 workerName = state.get('workerRequested') 222 canStart = canStart and workerName in self._workers 223 224 return canStart
225
226 - def canStop(self):
227 """ 228 Get whether the selected components can be stoped. 229 230 @rtype: bool 231 """ 232 states = self.getSelectedStates() 233 if not states: 234 return False 235 canStop = True 236 for state in states: 237 moodname = moods.get(state.get('mood')).name 238 canStop = canStop and moodname != 'sleeping' 239 return canStop
240
241 - def clearAndRebuild(self, components, componentNameToSelect=None):
242 """ 243 Update the components view by removing all old components and 244 showing the new ones. 245 246 @param components: dictionary of name -> 247 L{flumotion.common.component.AdminComponentState} 248 @param componentNameToSelect: name of the component to select or None 249 """ 250 # remove all Listeners 251 self._model.foreach(self._removeListenerForeach) 252 253 self.debug('updating components view') 254 # clear and rebuild 255 self._model.clear() 256 self._iters = {} 257 258 # FIXME: When we can depend on Python 2.4, use 259 # sorted(components.values(), 260 # cmp=cmpComponentType, 261 # key=operator.attrgetter('type')) 262 # 263 264 def componentSort(a, b): 265 return cmpComponentType(a.get('type'), 266 b.get('type'))
267 componentsSorted = components.values() 268 componentsSorted.sort(cmp=componentSort) 269 270 for component in componentsSorted: 271 self.appendComponent(component, componentNameToSelect) 272 273 self.debug('updated components view')
274
275 - def appendComponent(self, component, componentNameToSelect):
276 self.debug('adding component %r to listview' % component) 277 component.addListener(self, set_=self.stateSet) 278 279 titer = self._model.append() 280 self._iters[component] = titer 281 282 mood = component.get('mood') 283 self.debug('component has mood %r' % mood) 284 messages = component.get('messages') 285 self.debug('component has messages %r' % messages) 286 287 if mood != None: 288 self._setMoodValue(titer, mood) 289 290 self._model.set(titer, COL_STATE, component) 291 componentName = getComponentLabel(component) 292 self._model.set(titer, COL_NAME, componentName) 293 294 pid = component.get('pid') 295 self._model.set(titer, COL_PID, (pid and str(pid)) or '') 296 297 self._updateWorker(titer, component) 298 selection = self._view.get_selection() 299 if (componentNameToSelect is not None and 300 componentName == componentNameToSelect and 301 not selection.get_selected_rows()[1]): 302 selection.select_iter(titer) 303 304 self._updateStartStop()
305
306 - def removeComponent(self, component):
307 self.debug('removing component %r to listview' % component) 308 309 titer = self._iters[component] 310 self._model.remove(titer) 311 del self._iters[component] 312 313 self._updateStartStop()
314 315 # IStateListener implementation 316
317 - def stateSet(self, state, key, value):
318 if not isinstance(state, planet.AdminComponentState): 319 self.warning('Got state change for unknown object %r' % state) 320 return 321 322 titer = self._iters[state] 323 self.log('stateSet: state %r, key %s, value %r' % (state, key, value)) 324 325 if key == 'mood': 326 self.debug('stateSet: mood of %r changed to %r' % (state, value)) 327 328 if value == moods.sleeping.value: 329 self.debug('sleeping, removing local messages on %r' % state) 330 for message in state.get('messages', []): 331 state.observe_remove('messages', message) 332 333 self._setMoodValue(titer, value) 334 self._updateWorker(titer, state) 335 elif key == 'name': 336 if value: 337 self._model.set(titer, COL_NAME, value) 338 elif key == 'workerName': 339 self._updateWorker(titer, state) 340 elif key == 'pid': 341 self._model.set(titer, COL_PID, (value and str(value) or ''))
342 343 # Private 344
345 - def _updateStartStop(self):
346 oldstop = self.get_property('can-stop-any') 347 oldstart = self.get_property('can-start-any') 348 moodnames = [moods.get(x[COL_MOOD_VALUE]).name for x in self._model] 349 canStop = bool([x for x in moodnames if (x!='sleeping')]) 350 canStart = bool([x for x in moodnames if (x=='sleeping')]) 351 if oldstop != canStop: 352 self.set_property('can-stop-any', canStop) 353 if oldstart != canStart: 354 self.set_property('can-start-any', canStart)
355
356 - def workerAppend(self, name):
357 self._workers.append(name)
358
359 - def workerRemove(self, name):
360 self._workers.remove(name) 361 for state, titer in self._iters.items(): 362 self._updateWorker(titer, state)
363
364 - def _updateWorker(self, titer, componentState):
365 # update the worker name: 366 # - italic if workerName and workerRequested are not running 367 # - normal if running 368 369 workerName = componentState.get('workerName') 370 workerRequested = componentState.get('workerRequested') 371 if not workerName and not workerRequested: 372 #FIXME: Should we raise an error here? 373 # It's an impossible situation. 374 workerName = _("[any worker]") 375 376 markup = workerName or workerRequested 377 if markup not in self._workers: 378 self._model.set(titer, COL_TOOLTIP, 379 _("<b>Worker %s is not connected</b>") % markup) 380 markup = "<i>%s</i>" % markup 381 self._model.set(titer, COL_WORKER, markup)
382
383 - def _removeListenerForeach(self, model, path, titer):
384 # remove the listener for each state object 385 state = model.get(titer, COL_STATE)[0] 386 state.removeListener(self)
387
388 - def _setMoodValue(self, titer, value):
389 """ 390 Set the mood value on the given component name. 391 392 @type value: int 393 """ 394 self._model.set(titer, COL_MOOD, self._moodPixbufs[value]) 395 self._model.set(titer, COL_MOOD_VALUE, value) 396 mood = moods.get(value) 397 self._model.set(titer, COL_TOOLTIP, 398 _("<b>Component is %s</b>") % (MOODS_INFO[mood].lower(), )) 399 400 self._updateStartStop()
401
402 - def _getSelected(self, col_name):
403 selection = self._view.get_selection() 404 if not selection: 405 return None 406 model, selected_tree_rows = selection.get_selected_rows() 407 selected = [] 408 for tree_row in selected_tree_rows: 409 component_state = model[tree_row][col_name] 410 selected.append(component_state) 411 return selected
412
413 - def _getMoodPixbufs(self):
414 # load all pixbufs for the moods 415 pixbufs = {} 416 for i in range(0, len(moods)): 417 name = moods.get(i).name 418 pixbufs[i] = gtk.gdk.pixbuf_new_from_file_at_size( 419 os.path.join(configure.imagedir, 'mood-%s.png' % name), 420 24, 24) 421 422 return pixbufs
423
424 - def _selectionChanged(self):
425 states = self.getSelectedStates() 426 427 if not states: 428 self.debug( 429 'no component selected, emitting selection-changed None') 430 # Emit this in an idle, since popups will not be shown 431 # before this has completed, and it might possibly take a long 432 # time to finish all the callbacks connected to selection-changed 433 # This is not the proper fix, but makes the popups show up faster 434 gobject.idle_add(self.emit, 'selection-changed', []) 435 return 436 437 if states == self._lastStates: 438 self.debug('no new components selected, no emitting signal') 439 return 440 441 self.debug('components selected, emitting selection-changed') 442 self.emit('selection-changed', states) 443 self._lastStates = states
444
445 - def _showPopupMenu(self, event):
446 selection = self._view.get_selection() 447 retval = self._view.get_path_at_pos(int(event.x), int(event.y)) 448 if retval is None: 449 selection.unselect_all() 450 return 451 clicked_path = retval[0] 452 selected_path = selection.get_selected_rows()[1] 453 if clicked_path not in selected_path: 454 selection.unselect_all() 455 selection.select_path(clicked_path) 456 self.emit('show-popup-menu', event.button, event.time)
457 458 # Callbacks 459
460 - def _view_cursor_changed_cb(self, *args):
461 self._selectionChanged()
462
463 - def _view_button_press_event_cb(self, treeview, event):
464 if event.button == 3: 465 self._showPopupMenu(event) 466 return True 467 return False
468 469 470 gobject.type_register(ComponentList) 471