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
35 from flumotion.common import errors, log, common
36 from flumotion.twisted import flavors
37 from flumotion.twisted.defer import defer_generator_method
38 from flumotion.twisted.compat import implements
39
40 from gettext import gettext as _
41
43 """
44 I am a base class for all GTK+-based Admin views.
45 I am a view on one component's properties.
46
47 @type nodes: L{twisted.python.util.OrderedDict}
48 @ivar nodes: an ordered dict of name -> L{BaseAdminGtkNode}
49 """
50
51 logCategory = "admingtk"
52
54 """
55 @param state: state of component this is a UI for
56 @type state: L{flumotion.common.planet.AdminComponentState}
57 @type admin: L{flumotion.admin.admin.AdminModel}
58 @param admin: the admin model that interfaces with the manager for us
59 """
60 self.state = state
61 self.name = state.get('name')
62 self.admin = admin
63 self.debug('creating admin gtk for state %r' % state)
64 self.uiState = None
65 self.nodes = util.OrderedDict()
66
67 d = admin.componentCallRemote(state, 'getUIState')
68 d.addCallback(self.setUIState)
69
78
84
86 """
87 Set the given property on the element with the given name.
88 """
89 d = self.admin.setProperty(self.state, elementName, propertyName, value)
90 d.addErrback(self.propertyErrback, self)
91 return d
92
94 """
95 Get the value of the given property of the element with the given name.
96
97 Returns: L{twisted.internet.defer.Deferred} returning the value.
98 """
99 d = self.admin.getProperty(self.state, elementName, propertyName)
100 d.addErrback(self.propertyErrback, self)
101 return d
102
103 - def callRemote(self, methodName, *args, **kwargs):
106
108 """
109 Override this method to be notified of component's properties that
110 have changed.
111
112 I am meant to be overridden.
113 """
114 self.debug("property %s changed to %r" % (name, value))
115
116
117
119 """
120 Set up the admin view so it can display nodes.
121 """
122 self.debug('BaseAdminGtk.setup()')
123
124 config = self.state.get('config')
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 'source' in config:
130 self.debug("Component has eaters, show Eaters node")
131 self.nodes['Eaters'] = EatersAdminGtkNode(self.state, self.admin)
132
133
134 if not hasattr(self, 'gettext_domain'):
135 yield None
136
137 lang = common.getLL()
138 self.debug("loading bundle for %s locales" % lang)
139 bundleName = '%s-locale-%s' % (self.gettext_domain, lang)
140 d = self.admin.bundleLoader.getBundleByName(bundleName)
141 yield d
142
143 try:
144 localedatadir = d.value()
145 except errors.NoBundleError:
146 self.debug("Failed to find locale bundle %s" % bundleName)
147 yield None
148
149 localeDir = os.path.join(localedatadir, 'locale')
150 self.debug("Loading locales for %s from %s" % (
151 self.gettext_domain, localeDir))
152 gettext.bindtextdomain(self.gettext_domain, localeDir)
153 gtk.glade.bindtextdomain(self.gettext_domain, localeDir)
154 yield None
155 setup = defer_generator_method(setup)
156
158 """
159 Return a dict of admin UI nodes.
160 """
161 return self.nodes
162
163
165 """
166 Render the GTK+ admin view for this component and return the
167 main widget for embedding.
168 """
169 raise NotImplementedError
170
176
177 - def stateSet(self, object, key, value):
179
182
185
187 """
188 I am a base class for all GTK+-based Admin UI nodes.
189 I am a view on a set of properties for a component.
190
191 @ivar widget: the main widget representing this node
192 @type widget: L{gtk.Widget}
193 @ivar wtree: the widget tree representation for this node
194 """
195
196 implements(flavors.IStateListener)
197
198 logCategory = "admingtk"
199 glade_file = None
200
201 gettext_domain = 'flumotion'
202
203 - def __init__(self, state, admin, title=None):
204 """
205 @param state: state of component this is a UI node for
206 @type state: L{flumotion.common.planet.AdminComponentState}
207 @param admin: the admin model that interfaces with the manager for us
208 @type admin: L{flumotion.admin.admin.AdminModel}
209 @param title: the (translated) title to show this node with
210 @type title: str
211 """
212 self.state = state
213 self.admin = admin
214 self.statusbar = None
215 self.title = title
216 self.nodes = util.OrderedDict()
217 self.wtree = None
218 self.widget = None
219 self.uiState = None
220
221
222 self._gladefilepath = None
223
224
226 if self.statusbar:
227 return self.statusbar.push('notebook', str)
228
230 if self.statusbar:
231 return self.statusbar.remove('notebook', mid)
232
238
240 """
241 Set the given property on the element with the given name.
242 """
243 d = self.admin.setProperty(self.state, elementName, propertyName, value)
244 d.addErrback(self.propertyErrback, self)
245 return d
246
248 """
249 Get the value of the given property of the element with the given name.
250
251 Returns: L{twisted.internet.defer.Deferred} returning the value.
252 """
253 d = self.admin.getProperty(self.state, elementName, propertyName)
254 d.addErrback(self.propertyErrback, self)
255 return d
256
257 - def callRemote(self, methodName, *args, **kwargs):
260
261
263 """
264 Returns: a deferred returning the widget tree from the glade file.
265 """
266 def _getBundledFileCallback(result, gladeFile):
267 path = result
268 if not os.path.exists(path):
269 self.warning("Glade file %s not found in path %s" % (
270 gladeFile, path))
271 self.debug("Switching glade text domain to %s" % domain)
272 self.debug("loading widget tree from %s" % path)
273 self._gladefilepath = path
274 old = gtk.glade.textdomain()
275 gtk.glade.textdomain(domain)
276 self.wtree = gtk.glade.XML(path)
277 self.debug("Switching glade text domain back to %s" % old)
278 gtk.glade.textdomain(old)
279 return self.wtree
280
281 self.debug("requesting bundle for glade file %s" % gladeFile)
282 d = self.admin.bundleLoader.getFile(gladeFile)
283 d.addCallback(_getBundledFileCallback, gladeFile)
284 return d
285
294
308
317
319 """
320 I am meant to be overridden.
321 """
322 self.debug("property %s changed to %r" % (name, value))
323
325 self.uiState = state
326 if self.widget:
327 self.setUIState(self.uiState)
328
338
340 "Override me"
341 pass
342
344 "Override me"
345 pass
346
348 "Override me"
349 pass
350
352 "Override me"
353 pass
354
356 "Override me"
357 pass
358
360 """
361 Render the GTK+ admin view for this component.
362
363 Returns: a deferred returning the main widget for embedding
364 """
365 if self.glade_file:
366 self.debug('render: loading glade file %s in text domain %s' % (
367 self.glade_file, self.gettext_domain))
368 dl = self.loadGladeFile(self.glade_file, self.gettext_domain)
369 yield dl
370
371 try:
372 self.wtree = dl.value()
373 except RuntimeError:
374 msg = 'Could not load glade file %s' % self.glade_file
375 self.warning(msg)
376 yield gtk.Label("%s. Kill the programmer." % msg)
377
378 self.debug('render: calling haveWidgetTree')
379 self.haveWidgetTree()
380
381 if not self.widget:
382 self.debug('render: no self.widget, failing')
383 yield defer.fail(IndexError)
384
385 if self.uiState:
386 self.debug('calling setUIState on the node')
387 self.setUIState(self.uiState)
388
389 self.debug('render: yielding widget %s' % self.widget)
390 yield self.widget
391 render = defer_generator_method(render)
392
393
394
396 - def __init__(self, state, setters, appenders, removers,
397 setitemers=None, delitemers=None):
413
415 if self.shown:
416 for k in self.setters:
417 self.onSet(self.state, k, None)
418 self.shown = False
419
421
422 if not self.shown:
423 self.shown = True
424 for k in self.setters:
425 self.onSet(self.state, k, self.state.get(k))
426
427 - def onSet(self, obj, k, v):
428 if self.shown and k in self.setters:
429 self.setters[k](self.state, v)
430
432 if k in self.appenders:
433 self.appenders[k](self.state, v)
434
436 if k in self.removers:
437 self.removers[k](self.state, v)
438
440 if self.shown and k in self.setitemers:
441 self.setitemers[k](self.state, sk, v)
442
444 if self.shown and k in self.setitemers:
445 self.setitemers[k](self.state, sk, v)
446
455
457 glade_file = os.path.join('flumotion', 'component', 'base', 'feeders.glade')
458
460 BaseAdminGtkNode.__init__(self, state, admin, title=_("Feeders"))
461
462
463
464 self.treemodel = None
465 self.treeview = None
466 self.selected = None
467 self.labels = {}
468 self._lastConnect = 0
469 self._lastDisconnect = 0
470
472 if self.selected:
473 self.selected.hide()
474 if watcher:
475 self.selected = watcher
476 self.selected.show()
477 else:
478 self.selected = None
479
481 self.labels['feeder-name'].set_markup(_('Feeder <b>%s</b>') % value)
482
484 self.labels['feeder-name'].set_markup(_('Feeding to <b>%s</b>')
485 % value)
486
492
500
504
506 if value is None:
507
508 value = _("Unknown")
509 self.labels['buffers-dropped-total'].set_text(str(value))
510
512 self.labels['connections-total'].set_text(str(value))
513
520
527
529 if value:
530 text = time.strftime("%c", time.localtime(value))
531 self.labels['last-activity'].set_text(text)
532
534 if value == None:
535
536 self._table_connected.hide()
537 self._table_disconnected.show()
538 else:
539 self._table_disconnected.hide()
540 self._table_connected.show()
541
542
544 if self._lastConnect:
545 text = common.formatTime(time.time() - self._lastConnect)
546 self.labels['connection-time'].set_text(text)
547
548
550 if self._lastDisconnect:
551 text = common.formatTime(time.time() - self._lastDisconnect)
552 self.labels['disconnection-time'].set_text(text)
553
555 """
556 @param uiState: the component's uiState
557 @param state: the feeder's uiState
558 """
559 feederId = state.get('feedId')
560 i = self.treemodel.append(None)
561 self.treemodel.set(i, 0, feederId, 1, state)
562 w = _StateWatcher(state,
563 {'feedId': self.setFeederName},
564 {'clients': self.addFeederClient},
565 {'clients': self.removeFeederClient})
566 self.treemodel.set(i, 2, w, 3, 'feeder')
567 self.treeview.expand_all()
568
570 """
571 @param uiState: the component's uiState
572 @param state: the feeder client's uiState
573 """
574
575 clientId = state.get('clientId')
576 for row in self.treemodel:
577 if self.treemodel.get_value(row.iter, 1) == feederState:
578 break
579 i = self.treemodel.append(row.iter)
580 self.treemodel.set(i, 0, clientId, 1, state)
581 w = _StateWatcher(state, {
582 'clientId': self.setFeederClientName,
583 'bytesReadCurrent': self.setFeederClientBytesReadCurrent,
584 'buffersDroppedCurrent': self.setFeederClientBuffersDroppedCurrent,
585 'bytesReadTotal': self.setFeederClientBytesReadTotal,
586 'buffersDroppedTotal': self.setFeederClientBuffersDroppedTotal,
587 'reconnects': self.setFeederClientReconnects,
588 'lastConnect': self.setFeederClientLastConnect,
589 'lastDisconnect': self.setFeederClientLastDisconnect,
590 'lastActivity': self.setFeederClientLastActivity,
591 'fd': self.setFeederClientFD,
592 }, {}, {})
593 self.treemodel.set(i, 2, w, 3, 'client')
594 self.treeview.expand_all()
595
597 for row in self.treemodel:
598 if self.treemodel.get_value(row.iter, 1) == feederState:
599 break
600 for row in row.iterchildren():
601 if self.treemodel.get_value(row.iter, 1) == state:
602 break
603 state, watcher = self.treemodel.get(row.iter, 1, 2)
604 if watcher == self.selected:
605 self.select(None)
606 watcher.unwatch()
607 self.treemodel.remove(row.iter)
608
615
635
636 sel.connect('changed', sel_changed)
637
638 def set_label(name):
639 self.labels[name] = self.wtree.get_widget('label-' + name)
640
641 self.labels[name].set_text('')
642
643 set_label('feeder-name')
644 for type in (
645 'bytes-read-current', 'buffers-dropped-current',
646 'connected-since', 'connection-time',
647 'disconnected-since', 'disconnection-time',
648 'bytes-read-total', 'buffers-dropped-total',
649 'connections-total', 'last-activity',
650 ):
651 set_label(type)
652
653 self._table_connected = self.wtree.get_widget('table-current-connected')
654 self._table_disconnected = self.wtree.get_widget(
655 'table-current-disconnected')
656 self._table_feedclient = self.wtree.get_widget('table-feedclient')
657 self._table_connected.hide()
658 self._table_disconnected.hide()
659 self._table_feedclient.hide()
660 self.wtree.get_widget('box-right').hide()
661
662
663
664
665 return self.widget
666
668 glade_file = os.path.join('flumotion', 'component', 'base', 'eaters.glade')
669
671 BaseAdminGtkNode.__init__(self, state, admin, title=_("Eaters"))
672
673
674 self.treemodel = None
675 self.treeview = None
676 self._selected = None
677 self.labels = {}
678 self._lastConnect = 0
679 self._lastDisconnect = 0
680
682 if self._selected:
683 self._selected.hide()
684 if watcher:
685 self._selected = watcher
686 self._selected.show()
687 else:
688 self._selected = None
689
691 if value is None:
692 self._table_connected.hide()
693 self._table_disconnected.show()
694 else:
695 self._table_disconnected.hide()
696 self._table_connected.show()
697
699 self.labels['eater-name'].set_markup(_('Eater <b>%s</b>') % value)
700
706
708
709 if key == 'countTimestampDiscont':
710 self.labels['timestamp-discont-count-current'].set_text(str(value))
711 if value > 0:
712 self._expander_discont_current.show()
713 elif key == 'timeTimestampDiscont':
714 text = time.strftime("%c", time.localtime(value))
715 self.labels['timestamp-discont-time-current'].set_text(text)
716 if value is not None:
717 self._vbox_timestamp_discont_current.show()
718 elif key == 'lastTimestampDiscont':
719 text = common.formatTime(value, fractional=9)
720 self.labels['timestamp-discont-last-current'].set_text(text)
721 if value > 0.0:
722 self._vbox_timestamp_discont_current.show()
723 elif key == 'totalTimestampDiscont':
724 text = common.formatTime(value, fractional=9)
725 self.labels['timestamp-discont-total-current'].set_text(text)
726 if value > 0.0:
727 self._vbox_timestamp_discont_current.show()
728 elif key == 'timestampTimestampDiscont':
729 if value is None:
730 return
731 text = common.formatTime(value, fractional=9)
732 self.labels['timestamp-discont-timestamp-current'].set_text(text)
733
734 elif key == 'countOffsetDiscont':
735 self.labels['offset-discont-count-current'].set_text(str(value))
736 if value > 0:
737 self._expander_discont_current.show()
738 elif key == 'timeOffsetDiscont':
739 text = time.strftime("%c", time.localtime(value))
740 self.labels['offset-discont-time-current'].set_text(text)
741 if value is not None:
742 self._vbox_offset_discont_current.show()
743 elif key == 'lastOffsetDiscont':
744 text = _("%d units") % value
745 self.labels['offset-discont-last-current'].set_text(text)
746 if value > 0:
747 self._vbox_offset_discont_current.show()
748 elif key == 'totalOffsetDiscont':
749 text = _("%d units") % value
750 self.labels['offset-discont-total-current'].set_text(text)
751 if value > 0:
752 self._vbox_offset_discont_current.show()
753 elif key == 'offsetOffsetDiscont':
754 if value is None:
755 return
756 text = _("%d units") % value
757 self.labels['offset-discont-offset-current'].set_text(text)
758 if value > 0:
759 self._vbox_offset_discont_current.show()
760
762 if value is None:
763 return
764 self.labels['timestamp-discont-count-total'].set_text(str(value))
765 if value > 0.0:
766 self._expander_discont_total.show()
767
775
777 if value is None:
778 return
779 self.labels['offset-discont-count-total'].set_text(str(value))
780 if value != 0:
781 self._expander_discont_total.show()
782
784 if value is None:
785 return
786 text = _("%d units") % value
787 self.labels['offset-discont-total'].set_text(text)
788 if value != 0:
789 self._vbox_offset_discont_total.show()
790
799
801 self.labels['connections-total'].set_text(str(value))
802
803
804
811
812
814 if self._lastConnect:
815 text = common.formatTime(time.time() - self._lastConnect)
816 self.labels['connection-time'].set_text(text)
817
818
820 if self._lastDisconnect:
821 text = common.formatTime(time.time() - self._lastDisconnect)
822 self.labels['disconnection-time'].set_text(text)
823
825 """
826 @param uiState: the component's uiState
827 @param state: the eater's uiState
828 """
829 eaterId = state.get('eaterId')
830 i = self.treemodel.append(None)
831 self.treemodel.set(i, 0, eaterId, 1, state)
832 w = _StateWatcher(state,
833 {
834 'fd': self._setEaterFD,
835 'eaterId': self._setEaterName,
836 'lastConnect': self._setEaterLastConnect,
837 'countTimestampDiscont': self._setEaterCountTimestampDiscont,
838 'totalTimestampDiscont': self._setEaterTotalTimestampDiscont,
839 'countOffsetDiscont': self._setEaterCountOffsetDiscont,
840 'totalOffsetDiscont': self._setEaterTotalOffsetDiscont,
841 'totalConnections': self._setEaterTotalConnections,
842
843
844 'connection': self._setEaterConnection,
845 },
846 {},
847 {},
848 setitemers={
849 'connection': self._setEaterConnectionItem,
850 },
851 delitemers={
852 }
853 )
854 self.treemodel.set(i, 2, w)
855
862
883
884 set_label('eater-name')
885 for type in (
886 'connected-since', 'connection-time',
887 'timestamp-discont-timestamp-current',
888 'offset-discont-offset-current',
889 'timestamp-discont-count-current', 'offset-discont-count-current',
890 'timestamp-discont-total-current', 'offset-discont-total-current',
891 'timestamp-discont-last-current', 'offset-discont-last-current',
892 'timestamp-discont-time-current', 'offset-discont-time-current',
893 'timestamp-discont-count-total', 'offset-discont-count-total',
894 'timestamp-discont-total', 'offset-discont-total',
895 'connections-total',
896 ):
897 set_label(type)
898
899
900 def sel_changed(sel):
901 model, i = sel.get_selected()
902 self.select(i and model.get_value(i, 2))
903 self.wtree.get_widget('box-right').show()
904
905 sel.connect('changed', sel_changed)
906
907
908 self._table_connected = self.wtree.get_widget('table-current-connected')
909 self._table_disconnected = self.wtree.get_widget(
910 'table-current-disconnected')
911 self._table_eater = self.wtree.get_widget('table-eater')
912 self._expander_discont_current = self.wtree.get_widget(
913 'expander-discont-current')
914 self._vbox_timestamp_discont_current = self.wtree.get_widget(
915 'vbox-timestamp-discont-current')
916 self._vbox_offset_discont_current = self.wtree.get_widget(
917 'vbox-offset-discont-current')
918
919 self._expander_discont_total = self.wtree.get_widget(
920 'expander-discont-total')
921 self._vbox_timestamp_discont_total = self.wtree.get_widget(
922 'vbox-timestamp-discont-total')
923 self._vbox_offset_discont_total = self.wtree.get_widget(
924 'vbox-offset-discont-total')
925
926
927 self.wtree.get_widget('scrolledwindow').show_all()
928
929
930 self._expander_discont_current.hide()
931 self._table_connected.hide()
932 self._table_disconnected.hide()
933 self._expander_discont_total.hide()
934
935
936 self.wtree.get_widget('box-right').hide()
937
938
939
940
941 self.widget.show()
942 return self.widget
943
945 """
946 I am a base class for all GTK+-based component effect Admin UI nodes.
947 I am a view on a set of properties for an effect on a component.
948 """
949 - def __init__(self, state, admin, effectName, title=None):
950 """
951 @param state: state of component this is a UI for
952 @type state: L{flumotion.common.planet.AdminComponentState}
953 @param admin: the admin model that interfaces with the manager for us
954 @type admin: L{flumotion.admin.admin.AdminModel}
955 """
956 BaseAdminGtkNode.__init__(self, state, admin, title)
957 self.effectName = effectName
958
962