1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 """
23 Base classes for component UI's using GTK+
24 """
25
26 import os
27 import time
28
29 import gtk
30 import gtk.glade
31
32 from twisted.python import util
33 from twisted.internet import defer
34 from zope.interface import implements
35
36 from flumotion.common import errors, log, common, messages
37 from flumotion.twisted import flavors
38
39 from flumotion.common.messages import N_
40 T_ = messages.gettexter('flumotion')
41
42 from gettext import gettext as _
43
45 """
46 I am a base class for all GTK+-based Admin views.
47 I am a view on one component's properties.
48
49 @type nodes: L{twisted.python.util.OrderedDict}
50 @ivar nodes: an ordered dict of name -> L{BaseAdminGtkNode}
51 """
52
53 logCategory = "admingtk"
54 gettext_domain = None
55
57 """
58 @param state: state of component this is a UI for
59 @type state: L{flumotion.common.planet.AdminComponentState}
60 @type admin: L{flumotion.admin.admin.AdminModel}
61 @param admin: the admin model that interfaces with the manager for us
62 """
63 self.state = state
64 self.name = state.get('name')
65 self.admin = admin
66 self.debug('creating admin gtk for state %r' % state)
67 self.uiState = None
68 self.nodes = util.OrderedDict()
69
70 d = admin.componentCallRemote(state, 'getUIState')
71 d.addCallback(self.setUIState)
72
79
88
89 - def callRemote(self, methodName, *args, **kwargs):
92
93
94
96 """
97 Set up the admin view so it can display nodes.
98 """
99 self.debug('BaseAdminGtk.setup()')
100
101 def fetchTranslations():
102 if not self.gettext_domain:
103 return defer.succeed(None)
104
105 def haveBundle(localedatadir):
106 localeDir = os.path.join(localedatadir, 'locale')
107 self.debug("Loading locales for %s from %s" % (
108 self.gettext_domain, localeDir))
109
110 import gettext
111 gettext.bindtextdomain(self.gettext_domain, localeDir)
112 gtk.glade.bindtextdomain(self.gettext_domain, localeDir)
113
114
115 lang = common.getLL()
116 self.debug("loading bundle for %s locales" % lang)
117 bundleName = '%s-locale-%s' % (self.gettext_domain, lang)
118 d = self.admin.bundleLoader.getBundleByName(bundleName)
119 d.addCallbacks(haveBundle, lambda _: None)
120 return d
121
122 def addPages(_):
123 config = self.state.get('config')
124
125 if config['feed']:
126 self.debug("Component has feeders, show Feeders node")
127 self.nodes['Feeders'] = FeedersAdminGtkNode(self.state, self.admin)
128
129 if 'eater' in config and config['eater']:
130 self.debug("Component has eaters, show Eaters node")
131 self.nodes['Eaters'] = EatersAdminGtkNode(self.state, self.admin)
132
133 d = fetchTranslations()
134 d.addCallback(addPages)
135 return
136
138 """
139 Return a dict of admin UI nodes.
140
141 @rtype: dict of str -> L{BaseAdminGtkNode}
142 @returns: dict of name (untranslated) -> admin node
143 """
144 return self.nodes
145
146
148 """
149 Render the GTK+ admin view for this component and return the
150 main widget for embedding.
151 """
152 raise NotImplementedError
153
159
160 - def stateSet(self, object, key, value):
162
165
168
170 """
171 I am a base class for all GTK+-based Admin UI nodes.
172 I am a view on a set of properties for a component.
173
174 @ivar widget: the main widget representing this node
175 @type widget: L{gtk.Widget}
176 @ivar wtree: the widget tree representation for this node
177 """
178
179 implements(flavors.IStateListener)
180
181 logCategory = "admingtk"
182 glade_file = None
183
184 gettext_domain = 'flumotion'
185
186 - def __init__(self, state, admin, title=None):
187 """
188 @param state: state of component this is a UI node for
189 @type state: L{flumotion.common.planet.AdminComponentState}
190 @param admin: the admin model that interfaces with the manager for us
191 @type admin: L{flumotion.admin.admin.AdminModel}
192 @param title: the (translated) title to show this node with
193 @type title: str
194 """
195 self.state = state
196 self.admin = admin
197 self.statusbar = None
198 self.title = title
199 self.nodes = util.OrderedDict()
200 self.wtree = None
201 self.widget = None
202 self.uiState = None
203 self._pendingUIState = None
204
205
206
207 self._gladefilepath = None
208
212
214 if self.statusbar:
215 return self.statusbar.push('notebook', str)
216
218 if self.statusbar:
219 return self.statusbar.remove('notebook', mid)
220
221 - def callRemote(self, methodName, *args, **kwargs):
224
225
227 """
228 Returns: a deferred returning the widget tree from the glade file.
229 """
230 def _getBundledFileCallback(result, gladeFile):
231 path = result
232 if not os.path.exists(path):
233 self.warning("Glade file %s not found in path %s" % (
234 gladeFile, path))
235 self.debug("loading widget tree from %s" % path)
236
237 old = gtk.glade.textdomain()
238 self.debug("Switching glade text domain from %s to %s" % (
239 old, domain))
240 self._gladefilepath = path
241 gtk.glade.textdomain(domain)
242
243 self.wtree = gtk.glade.XML(path)
244
245 self.debug("Switching glade text domain back from %s to %s" % (
246 domain, old))
247 gtk.glade.textdomain(old)
248 return self.wtree
249
250
251
252 self.debug("requesting bundle for glade file %s" % gladeFile)
253 d = self.admin.bundleLoader.getFile(gladeFile)
254 d.addCallback(_getBundledFileCallback, gladeFile)
255 return d
256
265
279
288
294
304
306 "Override me"
307 pass
308
310 "Override me"
311 pass
312
314 "Override me"
315 pass
316
318 "Override me"
319 pass
320
322 "Override me"
323 pass
324
326 """
327 Render the GTK+ admin view for this component.
328
329 Returns: a deferred returning the main widget for embedding
330 """
331
332 allmessages = self.state.get('messages', [])
333 for message in allmessages:
334 if message.id == 'render':
335 self.debug('Removing previous messages %r' % message)
336 self.state.observe_remove('messages', message)
337
338 def error(debug):
339
340
341 self.warning("error rendering component UI; debug %s", debug)
342 m = messages.Error(T_(N_(
343 "Internal error in component UI. "
344 "Please file a bug against the component.")),
345 debug=debug, id="render")
346 self.addMessage(m)
347
348 label = gtk.Label(_("Internal error.\nSee component error "
349 "message\nfor more details."))
350
351
352
353 self.widget = label
354
355 return label
356
357 def loadGladeFile():
358 if not self.glade_file:
359 return defer.succeed(None)
360
361 def haveWtree(wtree):
362 self.wtree = wtree
363 self.debug('render: calling haveWidgetTree')
364 try:
365 self.haveWidgetTree()
366 except Exception, e:
367 return error(log.getExceptionMessage(e))
368
369 self.debug('render: loading glade file %s in text domain %s',
370 self.glade_file, self.gettext_domain)
371
372 d = self.loadGladeFile(self.glade_file, self.gettext_domain)
373 d.addCallback(haveWtree)
374 return d
375
376 def loadGladeFileErrback(failure):
377 if failure.check(RuntimeError):
378 return error(
379 'Could not load glade file %s.' % self.glade_file)
380 if failure.check(errors.NoBundleError):
381 return error(
382 'No bundle found containing %s.' % self.glade_file)
383
384 return failure
385
386 def renderFinished(_):
387 if not self.widget:
388 self.debug('render: no self.widget, failing')
389 raise TypeError('no self.widget')
390
391 if self._pendingUIState:
392 self.debug('render: calling setUIState on the node')
393 self.setUIState(self._pendingUIState)
394
395 self.debug('renderFinished: returning widget %s', self.widget)
396 return self.widget
397
398 def renderFinishedErrback(failure):
399 return error(log.getFailureMessage(failure))
400
401 d = loadGladeFile()
402 d.addErrback(loadGladeFileErrback)
403 d.addCallback(renderFinished)
404 d.addErrback(renderFinishedErrback)
405 return d
406
408 """
409 Add a message to the component.
410 Since this is called in a component view and only relevant to the
411 component view, the message only exists in the view, and is not
412 replicated to the manager state.
413
414 The message will be displayed in the usual message view.
415
416 @type message: L{flumotion.common.messages.Message}
417 """
418 self.state.observe_append('messages', message)
419
420
421
423 - def __init__(self, state, setters, appenders, removers,
424 setitemers=None, delitemers=None):
440
442 if self.shown:
443 for k in self.setters:
444 self.onSet(self.state, k, None)
445 self.shown = False
446
448
449 if not self.shown:
450 self.shown = True
451 for k in self.setters:
452 self.onSet(self.state, k, self.state.get(k))
453
454 - def onSet(self, obj, k, v):
455 if self.shown and k in self.setters:
456 self.setters[k](self.state, v)
457
459 if k in self.appenders:
460 self.appenders[k](self.state, v)
461
463 if k in self.removers:
464 self.removers[k](self.state, v)
465
467 if self.shown and k in self.setitemers:
468 self.setitemers[k](self.state, sk, v)
469
471 if self.shown and k in self.setitemers:
472 self.setitemers[k](self.state, sk, v)
473
482
484 glade_file = os.path.join('flumotion', 'component', 'base', 'feeders.glade')
485
487 BaseAdminGtkNode.__init__(self, state, admin, title=_("Feeders"))
488
489
490
491 self.treemodel = None
492 self.treeview = None
493 self.selected = None
494 self.labels = {}
495 self._lastConnect = 0
496 self._lastDisconnect = 0
497
499 if self.selected:
500 self.selected.hide()
501 if watcher:
502 self.selected = watcher
503 self.selected.show()
504 else:
505 self.selected = None
506
508 self.labels['feeder-name'].set_markup(_('Feeder <b>%s</b>') % value)
509
516
518 if not value:
519 self.labels['eater-name'].set_markup(_('<i>select an eater</i>'))
520 return
521 value = self._mungeClientId(value)
522 self.labels['eater-name'].set_markup(_('<b>%s</b>')
523 % (value,))
524
530
538
542
544 if value is None:
545
546 value = _("Unknown")
547 self.labels['buffers-dropped-total'].set_text(str(value))
548
550 self.labels['connections-total'].set_text(str(value))
551
558
565
570
572 if value == None:
573
574 self._table_connected.hide()
575 self._table_disconnected.show()
576 else:
577 self._table_disconnected.hide()
578 self._table_connected.show()
579
580
582 if self._lastConnect:
583 text = common.formatTime(time.time() - self._lastConnect)
584 self.labels['connection-time'].set_text(text)
585
586
588 if self._lastDisconnect:
589 text = common.formatTime(time.time() - self._lastDisconnect)
590 self.labels['disconnection-time'].set_text(text)
591
593 """
594 @param uiState: the component's uiState
595 @param state: the feeder's uiState
596 """
597 feederName = state.get('feederName')
598 i = self.treemodel.append(None)
599 self.treemodel.set(i, 0, feederName, 1, state)
600 w = _StateWatcher(state,
601 {'feederName': self.setFeederName},
602 {'clients': self.addFeederClient},
603 {'clients': self.removeFeederClient})
604 self.treemodel.set(i, 2, w, 3, 'feeder')
605 self.treeview.expand_all()
606
608 """
609 @param state: the component's uiState
610 @param state: the feeder client's uiState
611 """
612
613 printableClientId = self._mungeClientId(state.get('clientId'))
614 for row in self.treemodel:
615 if self.treemodel.get_value(row.iter, 1) == feederState:
616 break
617 i = self.treemodel.append(row.iter)
618 self.treemodel.set(i, 0, printableClientId, 1, state)
619 w = _StateWatcher(state, {
620 'clientId': self.setFeederClientName,
621 'bytesReadCurrent': self.setFeederClientBytesReadCurrent,
622 'buffersDroppedCurrent': self.setFeederClientBuffersDroppedCurrent,
623 'bytesReadTotal': self.setFeederClientBytesReadTotal,
624 'buffersDroppedTotal': self.setFeederClientBuffersDroppedTotal,
625 'reconnects': self.setFeederClientReconnects,
626 'lastConnect': self.setFeederClientLastConnect,
627 'lastDisconnect': self.setFeederClientLastDisconnect,
628 'lastActivity': self.setFeederClientLastActivity,
629 'fd': self.setFeederClientFD,
630 }, {}, {})
631 self.treemodel.set(i, 2, w, 3, 'client')
632 self.treeview.expand_all()
633
635 for row in self.treemodel:
636 if self.treemodel.get_value(row.iter, 1) == feederState:
637 break
638 for row in row.iterchildren():
639 if self.treemodel.get_value(row.iter, 1) == state:
640 break
641 state, watcher = self.treemodel.get(row.iter, 1, 2)
642 if watcher == self.selected:
643 self.select(None)
644 watcher.unwatch()
645 self.treemodel.remove(row.iter)
646
656
679
680 sel.connect('changed', sel_changed)
681
682 def set_label(name):
683 self.labels[name] = self.wtree.get_widget('label-' + name)
684
685 self.labels[name].set_text('')
686
687 for type in ('feeder-name', 'eater-name',
688 'bytes-read-current', 'buffers-dropped-current',
689 'connected-since', 'connection-time',
690 'disconnected-since', 'disconnection-time',
691 'bytes-read-total', 'buffers-dropped-total',
692 'connections-total', 'last-activity'):
693 set_label(type)
694
695 self._table_connected = self.wtree.get_widget('table-current-connected')
696 self._table_disconnected = self.wtree.get_widget(
697 'table-current-disconnected')
698 self._table_feedclient = self.wtree.get_widget('table-feedclient')
699 self._table_connected.hide()
700 self._table_disconnected.hide()
701 self._table_feedclient.hide()
702 self.wtree.get_widget('box-right').hide()
703
704 return self.widget
705
707 glade_file = os.path.join('flumotion', 'component', 'base', 'eaters.glade')
708
710 BaseAdminGtkNode.__init__(self, state, admin, title=_("Eaters"))
711
712
713 self.treemodel = None
714 self.treeview = None
715 self._selected = None
716 self.labels = {}
717 self._lastConnect = 0
718 self._lastDisconnect = 0
719
721 if self._selected:
722 self._selected.hide()
723 if watcher:
724 self._selected = watcher
725 self._selected.show()
726 else:
727 self._selected = None
728
730 if value is None:
731 self._table_connected.hide()
732 self._table_disconnected.show()
733 else:
734 self._table_disconnected.hide()
735 self._table_connected.show()
736
738 self.labels['eater-name'].set_markup(_('Eater <b>%s</b>') % value)
739
745
747 if key == 'feedId':
748 self.labels['eating-from'].set_text(str(value))
749
750 elif key == 'countTimestampDiscont':
751 self.labels['timestamp-discont-count-current'].set_text(str(value))
752 if value > 0:
753 self._expander_discont_current.show()
754 elif key == 'timeTimestampDiscont':
755 text = common.formatTimeStamp(time.localtime(value))
756 self.labels['timestamp-discont-time-current'].set_text(text)
757 if value is not None:
758 self._vbox_timestamp_discont_current.show()
759 elif key == 'lastTimestampDiscont':
760 text = common.formatTime(value, fractional=9)
761 self.labels['timestamp-discont-last-current'].set_text(text)
762 if value > 0.0:
763 self._vbox_timestamp_discont_current.show()
764 elif key == 'totalTimestampDiscont':
765 text = common.formatTime(value, fractional=9)
766 self.labels['timestamp-discont-total-current'].set_text(text)
767 if value > 0.0:
768 self._vbox_timestamp_discont_current.show()
769 elif key == 'timestampTimestampDiscont':
770 if value is None:
771 return
772 text = common.formatTime(value, fractional=9)
773 self.labels['timestamp-discont-timestamp-current'].set_text(text)
774
775 elif key == 'countOffsetDiscont':
776 self.labels['offset-discont-count-current'].set_text(str(value))
777 if value > 0:
778 self._expander_discont_current.show()
779 elif key == 'timeOffsetDiscont':
780 text = common.formatTimeStamp(time.localtime(value))
781 self.labels['offset-discont-time-current'].set_text(text)
782 if value is not None:
783 self._vbox_offset_discont_current.show()
784 elif key == 'lastOffsetDiscont':
785 text = _("%d units") % value
786 self.labels['offset-discont-last-current'].set_text(text)
787 if value > 0:
788 self._vbox_offset_discont_current.show()
789 elif key == 'totalOffsetDiscont':
790 text = _("%d units") % value
791 self.labels['offset-discont-total-current'].set_text(text)
792 if value > 0:
793 self._vbox_offset_discont_current.show()
794 elif key == 'offsetOffsetDiscont':
795 if value is None:
796 return
797 text = _("%d units") % value
798 self.labels['offset-discont-offset-current'].set_text(text)
799 if value > 0:
800 self._vbox_offset_discont_current.show()
801
803 if value is None:
804 return
805 self.labels['timestamp-discont-count-total'].set_text(str(value))
806 if value > 0.0:
807 self._expander_discont_total.show()
808
816
818 if value is None:
819 return
820 self.labels['offset-discont-count-total'].set_text(str(value))
821 if value != 0:
822 self._expander_discont_total.show()
823
825 if value is None:
826 return
827 text = _("%d units") % value
828 self.labels['offset-discont-total'].set_text(text)
829 if value != 0:
830 self._vbox_offset_discont_total.show()
831
840
842 self.labels['connections-total'].set_text(str(value))
843
844
845
852
853
855 if self._lastConnect:
856 text = common.formatTime(time.time() - self._lastConnect)
857 self.labels['connection-time'].set_text(text)
858
859
861 if self._lastDisconnect:
862 text = common.formatTime(time.time() - self._lastDisconnect)
863 self.labels['disconnection-time'].set_text(text)
864
866 """
867 @param uiState: the component's uiState
868 @param state: the eater's uiState
869 """
870 eaterId = state.get('eaterAlias')
871 i = self.treemodel.append(None)
872 self.treemodel.set(i, 0, eaterId, 1, state)
873 w = _StateWatcher(state,
874 {
875 'fd': self._setEaterFD,
876 'eaterAlias': self._setEaterName,
877 'lastConnect': self._setEaterLastConnect,
878 'countTimestampDiscont': self._setEaterCountTimestampDiscont,
879 'totalTimestampDiscont': self._setEaterTotalTimestampDiscont,
880 'countOffsetDiscont': self._setEaterCountOffsetDiscont,
881 'totalOffsetDiscont': self._setEaterTotalOffsetDiscont,
882 'totalConnections': self._setEaterTotalConnections,
883
884
885 'connection': self._setEaterConnection,
886 },
887 {},
888 {},
889 setitemers={
890 'connection': self._setEaterConnectionItem,
891 },
892 delitemers={
893 }
894 )
895 self.treemodel.set(i, 2, w)
896
903
924
925 for type in (
926 'eater-name', 'connected-since', 'connection-time',
927 'eating-from', 'timestamp-discont-timestamp-current',
928 'offset-discont-offset-current',
929 'timestamp-discont-count-current', 'offset-discont-count-current',
930 'timestamp-discont-total-current', 'offset-discont-total-current',
931 'timestamp-discont-last-current', 'offset-discont-last-current',
932 'timestamp-discont-time-current', 'offset-discont-time-current',
933 'timestamp-discont-count-total', 'offset-discont-count-total',
934 'timestamp-discont-total', 'offset-discont-total',
935 'connections-total',
936 ):
937 set_label(type)
938
939
940 def sel_changed(sel):
941 model, i = sel.get_selected()
942 self.select(i and model.get_value(i, 2))
943 self.wtree.get_widget('box-right').show()
944
945 sel.connect('changed', sel_changed)
946
947
948 self._table_connected = self.wtree.get_widget('table-current-connected')
949 self._table_disconnected = self.wtree.get_widget(
950 'table-current-disconnected')
951 self._table_eater = self.wtree.get_widget('table-eater')
952 self._expander_discont_current = self.wtree.get_widget(
953 'expander-discont-current')
954 self._vbox_timestamp_discont_current = self.wtree.get_widget(
955 'vbox-timestamp-discont-current')
956 self._vbox_offset_discont_current = self.wtree.get_widget(
957 'vbox-offset-discont-current')
958
959 self._expander_discont_total = self.wtree.get_widget(
960 'expander-discont-total')
961 self._vbox_timestamp_discont_total = self.wtree.get_widget(
962 'vbox-timestamp-discont-total')
963 self._vbox_offset_discont_total = self.wtree.get_widget(
964 'vbox-offset-discont-total')
965
966
967 self.wtree.get_widget('scrolledwindow').show_all()
968
969
970 self._expander_discont_current.hide()
971 self._table_connected.hide()
972 self._table_disconnected.hide()
973 self._expander_discont_total.hide()
974
975
976 self.wtree.get_widget('box-right').hide()
977
978
979
980
981 self.widget.show()
982 return self.widget
983
985 """
986 I am a base class for all GTK+-based component effect Admin UI nodes.
987 I am a view on a set of properties for an effect on a component.
988 """
989 - def __init__(self, state, admin, effectName, title=None):
990 """
991 @param state: state of component this is a UI for
992 @type state: L{flumotion.common.planet.AdminComponentState}
993 @param admin: the admin model that interfaces with the manager for us
994 @type admin: L{flumotion.admin.admin.AdminModel}
995 """
996 BaseAdminGtkNode.__init__(self, state, admin, title)
997 self.effectName = effectName
998
1002