1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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: 8717 $"
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,
70 COL_TOOLTIP,
71 COL_FG,
72 COL_SAD) = range(10)
73
74 SAD_COLOR = "#FF0000"
75
76
80
81
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)
92 gsignal('show-popup-menu', int, int)
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
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 = {}
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
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,
122 str,
123 str,
124 str,
125 gtk.gdk.Pixbuf,
126 object,
127 int,
128 str,
129 str,
130 bool,
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
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
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
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
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
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
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
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
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
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
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
293 self._model.foreach(self._removeListenerForeach)
294
295 self.debug('updating components view')
296
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
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
352
353
354
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
384
396
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
410
415
417
418
419
420
421 workerName = componentState.get('workerName')
422 workerRequested = componentState.get('workerRequested')
423 if not workerName and not workerRequested:
424
425
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
439
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
456
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
468
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
479 states = self.getSelectedStates()
480
481 if not states:
482 self.debug(
483 'no component selected, emitting selection-changed None')
484
485
486
487
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
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
513
516
522
523
524 gobject.type_register(ComponentList)
525
526
527
528 if __name__ == '__main__':
529
530 from twisted.internet import reactor
531 from twisted.spread import jelly
532
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
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
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