1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,
66 COL_TOOLTIP,
67 COL_FG,
68 COL_SAD) = range(10)
69
70 SAD_COLOR = "#FF0000"
71
72
76
77
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)
88 gsignal('show-popup-menu', int, int)
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
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 = {}
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
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,
118 str,
119 str,
120 str,
121 gtk.gdk.Pixbuf,
122 object,
123 int,
124 str,
125 str,
126 bool,
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
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
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
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
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
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
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
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
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
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
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
289 self._model.foreach(self._removeListenerForeach)
290
291 self.debug('updating components view')
292
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
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
351
352
353
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
383
395
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
409
414
416
417
418
419
420 workerName = componentState.get('workerName')
421 workerRequested = componentState.get('workerRequested')
422 if not workerName and not workerRequested:
423
424
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
438
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
455
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
467
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
478 states = self.getSelectedStates()
479
480 if not states:
481 self.debug(
482 'no component selected, emitting selection-changed None')
483
484
485
486
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
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
512
515
521
522
523 gobject.type_register(ComponentList)
524
525
526
527 if __name__ == '__main__':
528
529 from twisted.internet import reactor
530 from twisted.spread import jelly
531
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
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
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