1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 import os
24 import os.path
25 import sys
26
27 from gettext import gettext as _
28
29 import gobject
30 from gtk import gdk
31 import gtk
32 import gtk.glade
33
34 from twisted.internet import reactor, defer
35 from twisted.python import rebuild
36
37 from flumotion.admin.admin import AdminModel
38 from flumotion.admin import connections
39 from flumotion.admin.gtk import dialogs, parts, message
40 from flumotion.admin.gtk import connections as gtkconnections
41 from flumotion.configure import configure
42 from flumotion.common import errors, log, worker, planet, common, pygobject
43 from flumotion.manager import admin
44 from flumotion.twisted import flavors, reflect
45 from flumotion.twisted.compat import implements
46 from flumotion.ui import icons, trayicon
47
48 from flumotion.common.planet import moods
49 from flumotion.common.pygobject import gsignal
50
51 from flumotion.common import messages
52 from flumotion.common.messages import N_
53 T_ = messages.gettexter('flumotion')
54
55 -class Window(log.Loggable, gobject.GObject):
56 '''
57 Creates the GtkWindow for the user interface.
58 Also connects to the manager on the given host and port.
59 '''
60
61 implements(flavors.IStateListener)
62
63 logCategory = 'adminview'
64 gsignal('connected')
65
67 self.__gobject_init__()
68
69 self.widgets = {}
70 self.debug('creating UI')
71 self._trayicon = None
72
73
74
75 self.current_component = None
76 self.current_component_state = None
77
78 self._create_ui()
79
80 self._append_recent_connections()
81
82 self._disconnected_dialog = None
83
84
85 self._planetState = None
86 self._components = None
87
88 self.debug('setting model')
89 self.admin = None
90 self.wizard = None
91 self._setAdminModel(model)
92
117
118
120 self.warning('Errback: unhandled failure: %s' %
121 failure.getErrorMessage())
122 return failure
123
125
126
127 wtree = gtk.glade.XML(os.path.join(configure.gladedir, 'admin.glade'))
128 wtree.signal_autoconnect(self)
129
130 for widget in wtree.get_widget_prefix(''):
131 self.widgets[widget.get_name()] = widget
132 widgets = self.widgets
133
134 window = self.window = widgets['main_window']
135
136 def set_icon(proc, size, name):
137 i = gtk.Image()
138 i.set_from_stock('flumotion-'+name, size)
139 proc(i)
140 i.show()
141
142 def make_menu_proc(m):
143 return lambda f: m.set_property('image', f)
144 def menu_set_icon(m, name):
145 set_icon(make_menu_proc(m), gtk.ICON_SIZE_MENU, name)
146 m.show()
147
148 def tool_set_icon(m, name):
149 set_icon(m.set_icon_widget, gtk.ICON_SIZE_SMALL_TOOLBAR, name)
150
151 menu_set_icon(widgets['menuitem_manage_run_wizard'], 'wizard')
152 tool_set_icon(widgets['toolbutton_wizard'], 'wizard')
153 menu_set_icon(widgets['menuitem_manage_start_component'], 'play')
154 tool_set_icon(widgets['toolbutton_start_component'], 'play')
155 menu_set_icon(widgets['menuitem_manage_stop_component'], 'pause')
156 tool_set_icon(widgets['toolbutton_stop_component'], 'pause')
157
158 self._trayicon = trayicon.FluTrayIcon(self)
159 self._trayicon.set_tooltip(_('Not connected'))
160
161
162 self._component_view = widgets['component_view']
163 self._component_view_clear()
164
165 window.connect('delete-event', self.close)
166
167 self.components_view = parts.ComponentsView(widgets['components_view'])
168 self.components_view.connect('has-selection',
169 self._components_view_has_selection_cb)
170 self.components_view.connect('activated',
171 self._components_view_activated_cb)
172 self.statusbar = parts.AdminStatusbar(widgets['statusbar'])
173 self._set_stop_start_component_sensitive()
174 self.components_view.connect('notify::can-start-any',
175 self.start_stop_notify_cb)
176 self.components_view.connect('notify::can-stop-any',
177 self.start_stop_notify_cb)
178 self.start_stop_notify_cb()
179
180 self._messages_view = widgets['messages_view']
181 self._messages_view.hide()
182
183 return window
184
196
197 def refused(failure):
198 if failure.check(errors.ConnectionRefusedError):
199 d = dialogs.connection_refused_message(i.host,
200 self.window)
201 else:
202 d = dialogs.connection_failed_message(i.host,
203 self.window)
204 d.addCallback(lambda _: self.window.set_sensitive(True))
205
206 d.addCallbacks(connected, refused)
207 self.window.set_sensitive(False)
208
211
213 menu = self.widgets['connection_menu'].get_submenu()
214
215
216 kids = menu.get_children()
217 while True:
218 w = kids.pop()
219 if w.get_name() == 'file_quit':
220 break
221 else:
222 menu.remove(w)
223
224 clist = connections.get_recent_connections()
225 if not clist:
226 return
227
228 def append(i):
229 i.show()
230 gtk.MenuShell.append(menu, i)
231 def append_txt(c, n):
232 i = gtk.MenuItem(c['name'])
233 i.connect('activate', self.on_recent_activate, c['info'])
234 append(i)
235
236 append(gtk.SeparatorMenuItem())
237 map(append_txt, clist[:4], range(1,len(clist[:4])+1))
238
239
241 if not parent:
242 parent = self.window
243 d = dialogs.ErrorDialog(message, parent, close_on_response)
244 d.show_all()
245 return d
246
247
248
249
250
251
252
253 - def show_component(self, state, entryPath, fileName, methodName, data):
254 """
255 Show the user interface for this component.
256 Searches data for the given methodName global,
257 then instantiates an object from that class,
258 and calls the render() method.
259
260 @type state: L{flumotion.common.planet.AdminComponentState}
261 @param entryPath: absolute path to the cached base directory
262 @param fileName: path to the file with the entry point, under
263 entryPath
264 @param methodName: name of the method to instantiate the
265 L{flumotion.component.base.admin_gtk.BaseAdminGtk}
266 UI view
267 @param data: the python code to load
268 """
269
270
271 instance = None
272
273 name = state.get('name')
274 self.statusbar.set('main', _("Loading UI for %s ...") % name)
275
276 moduleName = common.pathToModuleName(fileName)
277 statement = 'import %s' % moduleName
278 self.debug('running %s' % statement)
279 try:
280 exec(statement)
281 except SyntaxError, e:
282
283 where = getattr(e, 'filename', "<entry file>")
284 lineno = getattr(e, 'lineno', 0)
285 msg = "Syntax Error at %s:%d while executing %s" % (
286 where, lineno, fileName)
287 self.warning(msg)
288 raise errors.EntrySyntaxError(msg)
289 except NameError, e:
290 msg = "NameError at while executing %s: %s" % (
291 fileName, " ".join(e.args))
292 raise
293 self.warning(msg)
294 raise errors.EntrySyntaxError(msg)
295 except ImportError, e:
296 msg = "ImportError while executing %s: %s" % (fileName,
297 " ".join(e.args))
298 self.warning(msg)
299 raise errors.EntrySyntaxError(msg)
300
301
302 module = reflect.namedAny(moduleName)
303 rebuild.rebuild(module)
304
305
306 if not hasattr(module, methodName):
307 msg = 'method %s not found in file %s' % (
308 methodName, fileName)
309 self.warning(msg)
310
311 m = messages.Error(T_(
312 N_("This component has a UI bug.")),
313 debug=msg,
314 id=methodName)
315 self._messages_view.add_message(m)
316
317
318 raise errors.FlumotionError(msg)
319 klass = getattr(module, methodName)
320
321
322 instance = klass(state, self.admin)
323 self.debug("Created entry instance %r" % instance)
324 self._instanceSetup(instance, klass, name)
325
327 self.debug('Setting up instance %r' % instance)
328 msg = None
329 d = None
330 try:
331 d = instance.setup()
332 except Exception, e:
333 msg = log.getExceptionMessage(e)
334 self.debug('Setup instance %r' % instance)
335 if not d:
336 msg = "%r.setup() should return a deferred" % klass
337
338 if msg:
339 self.warning('Component UI bug: %s' % msg)
340 m = messages.Error(T_(
341 N_("This component has a UI bug.")),
342 debug=msg,
343 id=name)
344 self._messages_view.add_message(m)
345 return
346
347 d.addCallback(self._setupCallback, name, instance)
348 d.addErrback(self._setupErrback, name)
349
351 notebook = gtk.Notebook()
352 nodeWidgets = {}
353 nodes = instance.getNodes()
354 self.statusbar.clear('main')
355
356
357 for node in nodes.values():
358 self.debug("Creating node for %s" % node.title)
359 label = gtk.Label(_('Loading UI for %s ...') % node.title)
360 table = gtk.Table(1, 1)
361 table.add(label)
362 nodeWidgets[node.title] = table
363
364 notebook.append_page(table, gtk.Label(node.title))
365
366
367 self._component_view_set_widget(notebook)
368
369
370 d = defer.Deferred()
371
372 for node in nodes.values():
373 mid = self.statusbar.push('notebook',
374 _("Loading tab %s for %s ...") % (node.title, name))
375 node.statusbar = self.statusbar
376 self.debug('adding callback for %s node.render()' % node.title)
377 d.addCallback(lambda _, n: n.render(), node)
378 d.addCallback(self._nodeRenderCallback, node.title,
379 nodeWidgets, mid)
380 d.addErrback(self._nodeRenderErrback, node.title)
381
382 d.addCallback(self._setCurrentComponentCallback, instance)
383
384 d.callback(None)
385 return d
386
397
398
400
401 self.debug("Got sub widget %r" % widget)
402 self.statusbar.remove('notebook', mid)
403
404
405 table = nodeWidgets[nodeName]
406 for w in table.get_children():
407 table.remove(w)
408
409 if not widget:
410 self.warning(".render() did not return an object")
411 widget = gtk.Label(_('%s does not have a UI yet') % nodeName)
412 else:
413 parent = widget.get_parent()
414 if parent:
415 parent.remove(widget)
416
417 table.add(widget)
418 widget.show()
419
432
434 self.debug('setting current_component to %r' % instance)
435 self.current_component = instance
436
457 def eb(failure, self, mid):
458 if mid:
459 self.statusbar.remove('main', mid)
460 self.warning("Failed to execute %s on component %s: %s"
461 % (methodName, name, failure))
462 if fail:
463 self.statusbar.push('main', fail % name)
464
465 d.addCallback(cb, self, mid)
466 d.addErrback(eb, self, mid)
467
471
479
480 def flowStateRemove(state, key, value):
481 if key == 'components':
482 self._remove_component(value)
483
484 def atmosphereStateAppend(state, key, value):
485 if key == 'components':
486 self._components[value.get('name')] = value
487
488 self.update_components()
489
490 def atmosphereStateRemove(state, key, value):
491 if key == 'components':
492 self._remove_component(value)
493
494 def planetStateAppend(state, key, value):
495 if key == 'flows':
496 if value != state.get('flows')[0]:
497 self.warning('flumotion-admin can only handle one '
498 'flow, ignoring /%s', value.get('name'))
499 return
500 self.debug('%s flow started', value.get('name'))
501 value.addListener(self, append=flowStateAppend,
502 remove=flowStateRemove)
503 for c in value.get('components'):
504 flowStateAppend(value, 'components', c)
505
506 def planetStateRemove(state, key, value):
507 self.debug('something got removed from the planet')
508
509 self.debug('parsing planetState %r' % planetState)
510 self._planetState = planetState
511
512
513 self._components = {}
514
515 planetState.addListener(self, append=planetStateAppend,
516 remove=planetStateRemove)
517
518 a = planetState.get('atmosphere')
519 a.addListener(self, append=atmosphereStateAppend,
520 remove=atmosphereStateRemove)
521 for c in a.get('components'):
522 atmosphereStateAppend(a, 'components', c)
523
524 for f in planetState.get('flows'):
525 planetStateAppend(planetState, 'flows', f)
526
541
543 if key == 'names':
544 self.statusbar.set('main', 'Worker %s logged in.' % value)
545
547 if key == 'names':
548 self.statusbar.set('main', 'Worker %s logged out.' % value)
549
566
567
569 if self._planetState:
570 self._planetState.removeListener(self)
571 self._planetState = None
572
573 self.info('Connected to manager')
574 if self._disconnected_dialog:
575 self._disconnected_dialog.destroy()
576 self._disconnected_dialog = None
577
578
579 self.window.set_title(_('%s - Flumotion Administration') %
580 self.admin.adminInfoStr())
581 self._trayicon.set_tooltip(self.admin.adminInfoStr())
582
583 self.emit('connected')
584
585
586 self.setPlanetState(self.admin.planet)
587
588 if not self._components:
589 self.debug('no components detected, running wizard')
590
591 self.show()
592 self.runWizard()
593
595 message = _("Lost connection to manager, reconnecting ...")
596 d = gtk.MessageDialog(self.window, gtk.DIALOG_DESTROY_WITH_PARENT,
597 gtk.MESSAGE_WARNING, gtk.BUTTONS_NONE, message)
598
599 RESPONSE_REFRESH = 1
600 d.add_button(gtk.STOCK_REFRESH, RESPONSE_REFRESH)
601 d.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL)
602 d.connect("response", self._dialog_disconnected_response_cb)
603 d.show_all()
604 self._disconnected_dialog = d
605
607 if id == gtk.RESPONSE_CANCEL:
608
609 dialog.destroy()
610 return
611 elif id == 1:
612 self.admin.reconnect()
613
623
628
638
643
644
646
647 current = self.components_view.get_selected_name()
648 if current != componentName:
649 return
650
651 comp = self.current_component
652 if comp:
653 comp.propertyChanged(propertyName, value)
654
656 can_start = self.components_view.get_property('can-start-any')
657 can_stop = self.components_view.get_property('can-stop-any')
658 self.widgets['menuitem_manage_stop_all'].set_sensitive(can_stop)
659 self.widgets['menuitem_manage_start_all'].set_sensitive(can_start)
660
661 s = self.widgets['menuitem_manage_clear_all'].set_sensitive
662 s(can_start and not can_stop)
663
666
668 self.components_view.update(self._components)
669 self._trayicon.update(self._components)
670
672 state = self.current_component_state
673 d = self.widgets
674 can_start = bool(state
675 and moods.get(state.get('mood')).name == 'sleeping')
676 d['menuitem_manage_start_component'].set_sensitive(can_start)
677 d['toolbutton_start_component'].set_sensitive(can_start)
678
679 moodname = state and moods.get(state.get('mood')).name
680 can_stop = bool(moodname and moodname!='sleeping' and moodname!='lost')
681 d['menuitem_manage_stop_component'].set_sensitive(can_stop)
682 d['toolbutton_stop_component'].set_sensitive(can_stop)
683 self.debug('can start %r, can stop %r' % (can_start, can_stop))
684
685
686
690
691
697
698
710
711 def compAppend(state, key, value):
712 name = state.get('name')
713 self.debug('stateAppend on component state of %s' % name)
714 if key == 'messages':
715 current = self.components_view.get_selected_name()
716 if name == current:
717 self._messages_view.add_message(value)
718
719 def compRemove(state, key, value):
720 name = state.get('name')
721 self.debug('stateRemove on component state of %s' % name)
722 if key == 'messages':
723 current = self.components_view.get_selected_name()
724 if name == current:
725 self._messages_view.clear_message(value.id)
726 self._set_stop_start_component_sensitive()
727
728 if self.current_component_state:
729 self.current_component_state.removeListener(self)
730 self.current_component_state = state
731 if self.current_component_state:
732 self.current_component_state.addListener(self, compSet,
733 compAppend,
734 compRemove)
735
736 self._set_stop_start_component_sensitive()
737
738 if not state:
739 self.debug('no state, returning')
740 return
741
742 name = state.get('name')
743 mood = state.get('mood')
744 messages = state.get('messages')
745 self._messages_view.clear()
746 self._component_view_clear()
747
748 if messages:
749 for m in messages:
750 self.debug('have message %r' % m)
751 self._messages_view.add_message(m)
752
753 if mood == moods.sad.value:
754 self.debug('component %s is sad' % name)
755 self.statusbar.set('main',
756 _("Component %s is sad") % name)
757
758 return
759
760 def gotEntryCallback(result):
761 entryPath, filename, methodName = result
762
763 self.statusbar.set('main', _('Showing UI for %s') % name)
764
765 filepath = os.path.join(entryPath, filename)
766 self.debug("Got the UI, lives in %s" % filepath)
767
768
769 self.uidir = os.path.split(filepath)[0]
770 handle = open(filepath, "r")
771 data = handle.read()
772 handle.close()
773
774 self.debug("showing admin UI for component %s" % name)
775
776 reactor.callLater(0, self.show_component,
777 state, entryPath, filename, methodName, data)
778
779 def gotEntryNoBundleErrback(failure):
780 failure.trap(errors.NoBundleError)
781 self.debug("Making generic UI for component %s" % name)
782
783
784 from flumotion.component.base import admin_gtk
785 instance = admin_gtk.BaseAdminGtk(state, self.admin)
786 self._instanceSetup(instance, admin_gtk.BaseAdminGtk, name)
787
788 def gotEntrySleepingComponentErrback(failure):
789 failure.trap(errors.SleepingComponentError)
790
791 self.statusbar.set('main',
792 _("Component %s is still sleeping") % name)
793
794 self.statusbar.set('main', _("Requesting UI for %s ...") % name)
795
796
797 if self.current_component:
798 if hasattr(self.current_component, 'cleanup'):
799 self.debug('Cleaning up current component view')
800 self.current_component.cleanup()
801 self.current_component = None
802
803 d = self.admin.getEntry(state, 'admin/gtk')
804 d.addCallback(gotEntryCallback)
805 d.addErrback(gotEntryNoBundleErrback)
806 d.addErrback(gotEntrySleepingComponentErrback)
807
809 self.debug('action %s on component %s' % (action, state.get('name')))
810 method_name = '_component_' + action
811 if hasattr(self, method_name):
812 getattr(self, method_name)(state)
813 else:
814 self.warning("No method '%s' implemented" % method_name)
815
816
819
821 import pprint
822 import cStringIO
823 fd = cStringIO.StringIO()
824 pprint.pprint(configation, fd)
825 fd.seek(0)
826 self.debug('Configuration=%s' % fd.read())
827
840
841 def nullwizard(*args):
842 self.wizard = None
843
844 state = self.admin.getWorkerHeavenState()
845 if not state.get('names'):
846 self.show_error_dialog(
847 _('The wizard cannot be run because no workers are logged in.'))
848 return
849
850 wiz = wizard.Wizard(self.window, self.admin)
851 wiz.connect('finished', _wizard_finished_cb)
852 wiz.run(True, state, False)
853
854 self.wizard = wiz
855 self.wizard.connect('destroy', nullwizard)
856
857
863
864 def after_getProperty(value, dialog):
865 self.debug('got value %r' % value)
866 dialog.update_value_entry(value)
867
868 def dialog_set_cb(dialog, element, property, value, state):
869 cb = self.admin.setProperty(state, element, property, value)
870 cb.addErrback(propertyErrback)
871 def dialog_get_cb(dialog, element, property, state):
872 cb = self.admin.getProperty(state, element, property)
873 cb.addCallback(after_getProperty, dialog)
874 cb.addErrback(propertyErrback)
875
876 name = state.get('name')
877 d = dialogs.PropertyChangeDialog(name, self.window)
878 d.connect('get', dialog_get_cb, state)
879 d.connect('set', dialog_set_cb, state)
880 d.run()
881
894
896 """
897 @returns: a L{twisted.internet.defer.Deferred}
898 """
899 return self._component_do(state, 'Stop', 'Stopping', 'Stopped')
900
902 """
903 @returns: a L{twisted.internet.defer.Deferred}
904 """
905 return self._component_do(state, 'Start', 'Starting', 'Started')
906
914
916 if not state:
917 state = self.components_view.get_selected_state()
918 if not state:
919 self.statusbar.push('main', _("No component selected."))
920 return None
921
922 name = state.get('name')
923 if not name:
924 return None
925
926 mid = self.statusbar.push('main', "%s component %s" % (doing, name))
927 d = self.admin.callRemote('component' + action, state)
928
929 def _actionCallback(result, self, mid):
930 self.statusbar.remove('main', mid)
931 self.statusbar.push('main', "%s component %s" % (done, name))
932 def _actionErrback(failure, self, mid):
933 self.statusbar.remove('main', mid)
934 self.warning("Failed to %s component %s: %s" % (
935 action, name, failure))
936 self.statusbar.push('main', _("Failed to %s component %s") % (
937 action, name))
938
939 d.addCallback(_actionCallback, self, mid)
940 d.addErrback(_actionErrback, self, mid)
941
942 return d
943
944
945
949
954
961
963 d = gtk.FileChooserDialog(_("Import Configuration..."), self.window,
964 gtk.FILE_CHOOSER_ACTION_OPEN,
965 (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
966 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
967 d.set_default_response(gtk.RESPONSE_ACCEPT)
968 d.show()
969 d.connect('response', self.on_import_response)
970
972 file_exists = True
973 if os.path.exists(name):
974 d = gtk.MessageDialog(self.window, gtk.DIALOG_MODAL,
975 gtk.MESSAGE_ERROR, gtk.BUTTONS_YES_NO,
976 _("File already exists.\nOverwrite?"))
977 d.connect("response", lambda self, response: d.hide())
978 if d.run() == gtk.RESPONSE_YES:
979 file_exists = False
980 else:
981 file_exists = False
982
983 if not file_exists:
984 f = open(name, 'w')
985 f.write(conf_xml)
986 f.close()
987 chooser.destroy()
988
996
998 d = gtk.FileChooserDialog(_("Export Configuration..."), self.window,
999 gtk.FILE_CHOOSER_ACTION_SAVE,
1000 (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
1001 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT))
1002 d.set_default_response(gtk.RESPONSE_ACCEPT)
1003 d.show()
1004 d.connect('response', self.on_export_response)
1005
1008
1011
1014
1018
1022
1025
1028
1031
1034
1036
1037 def _stop(dialog):
1038 dialog.stop()
1039 dialog.destroy()
1040
1041 def _syntaxErrback(failure, self, progress):
1042 failure.trap(errors.ReloadSyntaxError)
1043 _stop(progress)
1044 self.show_error_dialog(
1045 _("Could not reload component:\n%s.") %
1046 failure.getErrorMessage())
1047 return None
1048
1049 def _callLater(admin, dialog):
1050 deferred = self.admin.reload()
1051 deferred.addCallback(lambda result, d: _stop(d), dialog)
1052 deferred.addErrback(_syntaxErrback, self, dialog)
1053 deferred.addErrback(self._defaultErrback)
1054
1055 dialog = dialogs.ProgressDialog(_("Reloading ..."),
1056 _("Reloading client code"), self.window)
1057 l = lambda admin, text, dialog: dialog.message(
1058 _("Reloading %s code") % text)
1059 self.admin.connect('reloading', l, dialog)
1060 dialog.start()
1061 reactor.callLater(0.2, _callLater, self.admin, dialog)
1062
1064 if sys.version_info[1] >= 4:
1065 from flumotion.extern import code
1066 else:
1067 import code
1068
1069 vars = \
1070 {
1071 "admin": self.admin,
1072 "components": self._components
1073 }
1074 message = (" Flumotion Admin Debug Shell\n"
1075 "\n"
1076 "Local variables are:\n"
1077 " admin (flumotion.admin.admin.AdminModel)\n"
1078 " components (dict: name -> flumotion.common.planet.AdminComponentState)\n"
1079 "\n"
1080 "You can do remote component calls using:\n"
1081 " admin.componentCallRemote(components['component-name'],\n"
1082 " 'methodName', arg1, arg2)\n\n")
1083
1084 code.interact(local=vars, banner=message)
1085
1087 dialog = gtk.Dialog(_('About Flumotion'), self.window,
1088 gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
1089 (gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
1090 dialog.set_has_separator(False)
1091 dialog.set_resizable(False)
1092 dialog.set_border_width(12)
1093 dialog.vbox.set_spacing(6)
1094
1095 image = gtk.Image()
1096 dialog.vbox.pack_start(image)
1097 image.set_from_file(os.path.join(configure.imagedir, 'fluendo.png'))
1098 image.show()
1099
1100 version = gtk.Label('<span size="xx-large"><b>Flumotion %s</b></span>' % configure.version)
1101 version.set_selectable(True)
1102 dialog.vbox.pack_start(version)
1103 version.set_use_markup(True)
1104 version.show()
1105
1106 text = _('Flumotion is a streaming media server.\n\n'
1107 '© 2004, 2005, 2006, 2007 Fluendo S.L.')
1108 authors = ('Andy Wingo',
1109 'Johan Dahlin',
1110 'Mike Smith',
1111 'Thomas Vander Stichele',
1112 'Wim Taymans',
1113 'Zaheer Abbas Merali',
1114 'Sébastien Merle'
1115 )
1116 text += '\n\n<small>' + _('Authors') + ':\n'
1117 for author in authors:
1118 text += ' %s\n' % author
1119 text += '</small>'
1120 info = gtk.Label(text)
1121 dialog.vbox.pack_start(info)
1122 info.set_use_markup(True)
1123 info.set_selectable(True)
1124 info.set_justify(gtk.JUSTIFY_FILL)
1125 info.set_line_wrap(True)
1126 info.show()
1127
1128 dialog.show()
1129 dialog.run()
1130 dialog.destroy()
1131
1133
1134 self.window.show()
1135
1136 pygobject.type_register(Window)
1137