This URL has Read-Only access.

Statistics
| Branch: | Tag: | Revision:

root / py / scenic / gui.py @ 2567ca37

History | View | Annotate | Download (72 kB)

1
#!/usr/bin/env python
2
# -*- coding: utf-8 -*-
3
# 
4
# Scenic
5
# Copyright (C) 2008 Société des arts technologiques (SAT)
6
# http://www.sat.qc.ca
7
# All rights reserved.
8
#
9
# This file is free software: you can redistribute it and/or modify
10
# it under the terms of the GNU General Public License as published by
11
# the Free Software Foundation, either version 2 of the License, or
12
# (at your option) any later version.
13
#
14
# Scenic is distributed in the hope that it will be useful,
15
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17
# GNU General Public License for more details.
18
#
19
# You should have received a copy of the GNU General Public License
20
# along with Scenic. If not, see <http://www.gnu.org/licenses/>.
21

    
22
"""
23
Scenic GTK GUI.
24

25
For more documentation on how the configuration option values are gathered and updated from and to GUI widgets, see scenic/application.py
26
"""
27

    
28
import os
29
import smtplib
30
import gtk.gdk
31
import webbrowser
32
from twisted.internet import reactor
33
from twisted.internet import defer
34
from twisted.internet import task
35
from twisted.python.reflect import prefixedMethods
36
from scenic import configure
37
from scenic import process # just for constants
38
from scenic import dialogs
39
from scenic import glade
40
from scenic import preview
41
from scenic import network
42
from scenic import communication
43
from scenic.devices import cameras
44
from scenic.devices import networkinterfaces
45
from scenic import logger
46
from scenic.internationalization import _
47

    
48
log = logger.start(name="gui")
49

    
50
ONLINE_HELP_URL = "http://svn.sat.qc.ca/trac/scenic/wiki/Documentation"
51
ONE_LINE_DESCRIPTION = """Scenic is a telepresence software oriented for live performances."""
52
ALL_SUPPORTED_SIZE = [ # by milhouse video
53
    "924x576",
54
    "768x480",
55
    "720x480",
56
    "704x480",
57
    "704x240",
58
    "640x480",
59
    "352x240",
60
    "320x240",
61
    "176x120"
62
    ]
63

    
64
LICENSE_TEXT = _("""Scenic
65
Copyright (C) 2009 Society for Arts and Technology (SAT)
66
http://www.sat.qc.ca
67
All rights reserved.
68

69
This file is free software: you can redistribute it and/or modify
70
it under the terms of the GNU General Public License as published by
71
the Free Software Foundation, either version 2 of the License, or
72
(at your option) any later version.
73

74
Scenic is distributed in the hope that it will be useful,
75
but WITHOUT ANY WARRANTY; without even the implied warranty of
76
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
77
GNU General Public License for more details.
78

79
You should have received a copy of the GNU General Public License
80
along with Scenic.  If not, see <http://www.gnu.org/licenses/>.""")
81

    
82
PROJECT_WEBSITE = "http://svn.sat.qc.ca/trac/scenic"
83

    
84
AUTHORS_LIST = [
85
    'Alexandre Quessy <alexandre@quessy.net>',
86
    'Tristan Matthews <tristan@sat.qc.ca>',
87
    'Simon Piette <simonp@sat.qc.ca>',
88
    u'Étienne Désautels <etienne@teknozen.net>',
89
    ]
90

    
91
COPYRIGHT_SHORT = _("Copyright 2009-2010 Society for Arts and Technology")
92

    
93
def _get_key_for_value(dictionnary, value):
94
    """
95
    Returns the key for a value in a dict.
96
    @param dictionnary: dict
97
    @param value: The value.
98
    """
99
    return dictionnary.keys()[dictionnary.values().index(value)]
100

    
101
def _get_combobox_value(widget):
102
    """
103
    Returns the current value of a GTK ComboBox widget.
104
    """
105
    index = widget.get_active()
106
    tree_model = widget.get_model()
107
    try:
108
        tree_model_row = tree_model[index]
109
    except IndexError, e:
110
        raise RuntimeError("ComboBox widget %s doesn't have value with index %s." % (widget, index))
111
    #except TypeError, e:
112
    #    raise RuntimeError("%s is not a ComboBox widget" % (widget))
113
    return tree_model_row[0] 
114

    
115
def _set_combobox_choices(widget, choices=[]):
116
    """
117
    Sets the choices in a GTK combobox.
118
    """
119
    #XXX: combo boxes in the glade file must have a space as a value to have a tree iter
120
    #TODO When we change a widget value, its changed callback is called...
121
    previous_value = _get_combobox_value(widget)
122
    tree_model = gtk.ListStore(str)
123
    for choice in choices:
124
        tree_model.append([choice])
125
    widget.set_model(tree_model)
126
    if previous_value != " ": # we put empty spaces in glade as value, but this is not a real value, and we get rid of it.
127
        _set_combobox_value(widget, previous_value)
128

    
129
def _set_combobox_value(widget, value=None):
130
    """
131
    Sets the current value of a GTK ComboBox widget.
132
    """
133
    #XXX: combo boxes in the glade file must have a space as a value to have a tree iter
134
    tree_model = widget.get_model()
135
    index = 0
136
    got_it = False
137
    for i in iter(tree_model):
138
        v = i[0]
139
        if v == value:
140
            got_it = True
141
            break # got it
142
        index += 1
143
    if got_it:
144
        #widget.set_active(-1)  NONE
145
        widget.set_active(index)
146
    else:
147
        widget.set_active(0) # FIXME: -1)
148
        msg = "ComboBox widget %s doesn't have value \"%s\"." % (widget, value)
149
        log.debug(msg)
150

    
151
#videotestsrc legible name:
152
VIDEO_TEST_INPUT = "Color bars"
153

    
154
# GUI legible value to milhouse value mapping:
155
VIDEO_CODECS = {
156
    "h.264": "h264",
157
    "h.263": "h263",
158
    "Theora": "theora",
159
    "MPEG4": "mpeg4"
160
    }
161
AUDIO_CODECS = {
162
    "Raw": "raw",
163
    "MP3": "mp3",
164
    "Vorbis": "vorbis",
165
    }
166
AUDIO_SOURCES = {
167
    "JACK": "jackaudiosrc",
168
    "Test sound": "audiotestsrc"
169
    }
170
# min/max:
171
VIDEO_BITRATE_MIN_MAX = {
172
    "h.264": [2.0, 16.0],
173
    "MPEG4": [0.5, 4.0],
174
    "h.263": [0.5, 4.0],
175
    }
176
# standards:
177
VIDEO_STANDARDS = ["NTSC", "PAL"]
178

    
179
def format_contact_markup(contact):
180
    """
181
    Formats a contact for the Adressbook GTK widget.
182
    @param contact: A dict with keys "name" and "address"
183
    @rtype: str
184
    @return: Pango markup for the TreeView widget.
185
    """
186
    auto_accept = ""
187
    if contact["auto_accept"]:
188
        auto_accept = "\n  " + _("Automatically accept invitations")
189
    return "<b>%s</b>\n  IP: %s%s" % (contact["name"], contact["address"], auto_accept) 
190

    
191

    
192
class Gui(object):
193
    """
194
    Graphical User Interface
195
     * Contains the main GTK window.
196
     * And some dialogs.
197
    """
198
    def __init__(self, app, kiosk_mode=False, fullscreen=False, enable_debug=False):
199
        self.app = app
200
        self.kiosk_mode_on = kiosk_mode
201
        widgets_tree = glade.get_widgets_tree()
202
        
203
        self._widgets_changed_by_user = True # if False, we are changing some widget's value programmatically.
204
        # connects callbacks to widgets automatically
205
        glade_signal_slots = {}
206
        for method in prefixedMethods(self, "on_"):
207
            glade_signal_slots[method.__name__] = method
208
        widgets_tree.signal_autoconnect(glade_signal_slots)
209
        
210
        # Get all the widgets that we use
211
        self.main_window = widgets_tree.get_widget("main_window")
212
        self.main_window.connect('delete-event', self.on_main_window_deleted)
213
        self.main_window.set_icon_from_file(os.path.join(configure.PIXMAPS_DIR, 'scenic.png'))
214
        self.main_tabs_widget = widgets_tree.get_widget("mainTabs")
215
        self.system_tab_contents_widget = widgets_tree.get_widget("system_tab_contents")
216
        self.debug_tab_contents_widget = widgets_tree.get_widget("debug_tab_contents")
217
        self.main_window.connect("window-state-event", self.on_window_state_event)
218
        
219
        # ------------------------------ dialogs:
220
        # confirm_dialog: (a simple yes/no)
221
        self.confirm_dialog = dialogs.ConfirmDialog(parent=self.main_window)
222

    
223
        # calling_dialog: (this widget is created and destroyed really often !!
224
        self.calling_dialog = None
225
        
226
        # invited_dialog:
227
        self.invited_dialog = dialogs.InvitedDialog(parent=self.main_window)
228
        
229
        # edit_contact_window:
230
        self.edit_contact_window = widgets_tree.get_widget("edit_contact_window")
231
        self.edit_contact_window.set_transient_for(self.main_window) # child of main window
232
        self.edit_contact_window.connect('delete-event', self.edit_contact_window.hide_on_delete)
233
        
234
        # fields in the edit contact window:
235
        self.contact_name_widget = widgets_tree.get_widget("contact_name")
236
        self.contact_addr_widget = widgets_tree.get_widget("contact_addr")
237
        self.contact_auto_accept_widget = widgets_tree.get_widget("contact_auto_accept")
238
        
239
        # -------------------- main window widgets:
240
        # invite button:
241
        self.invite_label_widget = widgets_tree.get_widget("invite_label")
242
        self.invite_icon_widget = widgets_tree.get_widget("invite_icon")
243
        
244
        # addressbook buttons:
245
        self.edit_contact_widget = widgets_tree.get_widget("edit_contact")
246
        self.add_contact_widget = widgets_tree.get_widget("add_contact")
247
        self.remove_contact_widget = widgets_tree.get_widget("remove_contact")
248
        self.invite_contact_widget = widgets_tree.get_widget("invite_contact")
249
        # treeview:
250
        self.contact_list_widget = widgets_tree.get_widget("contact_list")
251
        # position of currently selected contact in list of contact:
252
        self.selected_contact_row = None
253
        self.select_contact_index = None
254

    
255
        # Summary text view:
256
        self.info_peer_widget = widgets_tree.get_widget("info_peer")
257
        self.info_send_video_widget = widgets_tree.get_widget("info_send_video")
258
        self.info_send_audio_widget = widgets_tree.get_widget("info_send_audio")
259
        self.info_receive_video_widget = widgets_tree.get_widget("info_receive_video")
260
        self.info_receive_audio_widget = widgets_tree.get_widget("info_receive_audio")
261
        self.info_ip_widget = widgets_tree.get_widget("info_ip")
262
        self.info_receive_midi_widget = widgets_tree.get_widget("info_receive_midi")
263
        self.info_send_midi_widget = widgets_tree.get_widget("info_send_midi")
264

    
265
        # video
266
        self.video_capture_size_widget = widgets_tree.get_widget("video_capture_size")
267
        self.video_display_widget = widgets_tree.get_widget("video_display")
268
        self.video_bitrate_widget = widgets_tree.get_widget("video_bitrate")
269
        self.video_source_widget = widgets_tree.get_widget("video_source")
270
        self.video_codec_widget = widgets_tree.get_widget("video_codec")
271
        self.video_fullscreen_widget = widgets_tree.get_widget("video_fullscreen")
272
        self.video_view_preview_widget = widgets_tree.get_widget("video_view_preview")
273
        self.video_deinterlace_widget = widgets_tree.get_widget("video_deinterlace")
274
        self.aspect_ratio_widget = widgets_tree.get_widget("aspect_ratio")
275
        self.v4l2_input_widget = widgets_tree.get_widget("v4l2_input")
276
        self.v4l2_standard_widget = widgets_tree.get_widget("v4l2_standard")
277
        self.video_jitterbuffer_widget = widgets_tree.get_widget("video_jitterbuffer")
278
        # video preview:
279
        self.preview_area_widget = widgets_tree.get_widget("preview_area")
280
        self.preview_area_x_window_id = None
281
        self.preview_in_window_widget = widgets_tree.get_widget("preview_in_window")
282
        
283
        # audio
284
        self.audio_source_widget = widgets_tree.get_widget("audio_source")
285
        self.audio_codec_widget = widgets_tree.get_widget("audio_codec")
286
        self.audio_jack_icon_widget = widgets_tree.get_widget("audio_jack_icon")
287
        self.audio_jack_state_widget = widgets_tree.get_widget("audio_jack_state")
288
        self.audio_numchannels_widget = widgets_tree.get_widget("audio_numchannels")
289
        self.audio_jitterbuffer_widget = widgets_tree.get_widget("audio_jitterbuffer")
290

    
291
        self.jack_latency_widget = widgets_tree.get_widget("jack_latency")
292
        self.jack_sampling_rate_widget = widgets_tree.get_widget("jack_sampling_rate")
293

    
294
        # audio levels:
295
        def _plug_removed_cb(widget):
296
            """ Called when a plug is removed from socket, returns
297
                True so that it can be reused
298
                """
299
            log.debug("I (%s) have just had a plug removed!" % (widget))
300
            return True
301
        
302
        self.audio_levels_input_widget = widgets_tree.get_widget("audio_levels_input")
303
        in_socket = gtk.Socket()
304
        in_socket.connect("plug-removed", _plug_removed_cb)
305
        in_socket.show()
306
        self.audio_levels_input_widget.add(in_socket)
307
        self.audio_levels_input_socket_id = in_socket.get_id()
308
        
309
        self.audio_levels_output_widget = widgets_tree.get_widget("audio_levels_output")
310
        out_socket = gtk.Socket()
311
        out_socket.connect("plug-removed", _plug_removed_cb)
312
        in_socket.show()
313
        out_socket.show()
314
        self.audio_levels_output_widget.add(out_socket)
315
        self.audio_levels_output_socket_id = out_socket.get_id()
316

    
317
        # system tab contents:
318
        self.network_admin_widget = widgets_tree.get_widget("network_admin")
319

    
320
        # MIDI tab
321
        self.midi_send_enabled_widget = widgets_tree.get_widget("midi_send_enabled")
322
        self.midi_recv_enabled_widget = widgets_tree.get_widget("midi_recv_enabled")
323
        self.midi_input_device_widget = widgets_tree.get_widget("midi_input_device")
324
        self.midi_output_device_widget = widgets_tree.get_widget("midi_output_device")
325
        self.midi_jitterbuffer_widget = widgets_tree.get_widget("midi_jitterbuffer")
326

    
327
        # synchronize and disable a/v stuff
328
        self.audio_input_buffer_widget = widgets_tree.get_widget("audio_input_buffer")
329
        self.audio_output_buffer_widget = widgets_tree.get_widget("audio_output_buffer")
330
        self.audio_receive_enabled_widget = widgets_tree.get_widget("audio_receive_enabled")
331
        self.audio_send_enabled_widget = widgets_tree.get_widget("audio_send_enabled")
332
        self.audio_video_synchronized_widget = widgets_tree.get_widget("audio_video_synchronized")
333
        self.video_send_enabled_widget = widgets_tree.get_widget("video_send_enabled")
334
        self.video_receive_enabled_widget = widgets_tree.get_widget("video_receive_enabled")
335

    
336
        # switch to Kiosk mode if asked
337
        if self.kiosk_mode_on:
338
            self.main_window.set_decorated(False)
339
        else:
340
            # Removes the sytem_tab 
341
            tab_num = self.main_tabs_widget.page_num(self.system_tab_contents_widget)
342
            log.debug("Removing tab number %d." % (tab_num))
343
            self.main_tabs_widget.remove_page(tab_num)
344

    
345
        self.enable_debug = enable_debug
346
        if not self.enable_debug: # hide the tab if not in debug
347
            # Removes the debug_tab 
348
            tab_num = self.main_tabs_widget.page_num(self.debug_tab_contents_widget)
349
            log.debug("Removing tab number %d." % (tab_num))
350
            self.main_tabs_widget.remove_page(tab_num)
351
        
352
        self.is_fullscreen = False
353
        if fullscreen:
354
            log.debug("Making the main window fullscreen.")
355
            self.toggle_fullscreen()
356
        
357
        # ------------------ contact list view
358
        self.selection = self.contact_list_widget.get_selection()
359
        self.selection.connect("changed", self.on_contact_list_changed, None) 
360
        self.contact_tree = gtk.ListStore(str)
361
        self.contact_list_widget.set_model(self.contact_tree)
362
        column = gtk.TreeViewColumn(_("Contacts"), gtk.CellRendererText(), markup=False)
363
        self.contact_list_widget.append_column(column)
364
        # TODO: those state variables interactive/not could be merged into a single one
365
        # preview:
366
        self.preview_manager = preview.Preview(self.app)
367
        self.video_preview_icon_widget = widgets_tree.get_widget("video_preview_icon")
368
        self.preview_manager.state_changed_signal.connect(self.on_preview_manager_state_changed)
369
        self.main_window.show()
370
        
371
        # recurring calls:
372
        self._streaming_state_check_task = task.LoopingCall(self.update_streaming_state)
373
        self._streaming_state_check_task.start(1.0, now=False)
374
        self._update_id_task = task.LoopingCall(self.update_local_ip)
375
        def _start_update_id():
376
            self._update_id_task.start(10.0, now=True)
377
        reactor.callLater(0, _start_update_id)
378

    
379
        self.debug_textview_widget = widgets_tree.get_widget("debug_textview")
380
        # The main app must call init_widgets_value
381
   
382
    #TODO: for the preview in the drawing area   
383
    #def on_expose_event(self, widget, event):
384
    #    self.preview_xid = widget.window.xid
385
    #    return False
386

    
387
    # ------------------ window events and actions --------------------
388
    def toggle_fullscreen(self):
389
        """
390
        Toggles the fullscreen mode on/off.
391
        """
392
        if self.is_fullscreen:
393
            self.main_window.unfullscreen()
394
        else:
395
            self.main_window.fullscreen()
396

    
397
    def on_window_state_event(self, widget, event):
398
        """
399
        Called when toggled fullscreen.
400
        """
401
        self.is_fullscreen = event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN != 0
402
        #print('fullscreen %s' % (self.is_fullscreen))
403
        return True
404
    
405
    def on_main_window_deleted(self, *args):
406
        """
407
        Destroy method causes application to exit
408
        when main window closed
409
        """
410
        return self._confirm_and_quit()
411
        
412
    def _confirm_and_quit(self):
413
        def _cb(result):
414
            if result:
415
                log.info("Destroying the window.")
416
                self.main_window.destroy()
417
            else:
418
                log.info("Not quitting.")
419
        # If you return FALSE in the "delete_event" signal handler,
420
        # GTK will emit the "destroy" signal. Returning TRUE means
421
        # you don't want the window to be destroyed.
422
        # This is useful for popping up 'are you sure you want to quit?'
423
        # type dialogs. 
424
        if self.app.config.confirm_quit and self.app.has_session():
425
            d = dialogs.YesNoDialog.create(_("Really quit ?\nAll streaming processes will quit as well."), parent=self.main_window)
426
            d.addCallback(_cb)
427
            return True
428
        else:
429
            _cb(True)
430
            return False
431
    
432
    def on_main_window_destroyed(self, *args):
433
        # TODO: confirm dialog!
434
        if reactor.running:
435
            log.debug("reactor.stop()")
436
            reactor.stop()
437

    
438
    # --------------- slots for some widget events ------------
439

    
440
    def on_preview_area_realize(self, *args):
441
        # avoid bad xid errors
442
        gtk.gdk.display_get_default().sync()
443
        xid = self.preview_area_widget.window.xid
444
        log.debug("Preview area X Window ID: %s" % (xid))
445
        self.preview_area_x_window_id = xid
446
        self.preview_area_widget.window.set_background(gtk.gdk.Color(0, 0, 0)) # black
447

    
448
    def on_preview_manager_state_changed(self, manager, new_state):
449
        if new_state == process.STATE_STOPPED:
450
            log.debug("Making the preview button to False since the preview process died.")
451
            if self.preview_manager.is_busy(): # very unlikely
452
                self.preview_manager.stop()
453
            self.video_preview_icon_widget.set_from_stock(gtk.STOCK_MEDIA_PLAY, 4)
454
            self._widgets_changed_by_user = False
455
            self.video_view_preview_widget.set_active(False)
456
            self._widgets_changed_by_user = True
457
        elif new_state == process.STATE_STARTING:
458
            self.video_preview_icon_widget.set_from_stock(gtk.STOCK_MEDIA_STOP, 4)
459

    
460
    def close_preview_if_running(self):
461
        """
462
        @rtype: L{Deferred}
463
        """
464
        def _cl(deferred):
465
            if self.preview_manager.is_busy():
466
                reactor.callLater(0.01, _cl, deferred)
467
            else:
468
                deferred.callback(None)
469
        if self.preview_manager.is_busy():
470
            self.preview_manager.stop()
471
            deferred = defer.Deferred()
472
            reactor.callLater(0.01, _cl, deferred)
473
            return deferred
474
        else:
475
            return defer.succeed(None)
476
        
477
    def on_video_view_preview_toggled(self, widget):
478
        """
479
        Shows a preview of the video input.
480
        """
481
        #TODO: create a new process protocol for the preview window
482
        #TODO: stop it when starting to stream, if running
483
        #TODO: stop it when button is toggled to false.
484
        # It can be the user that pushed the button, or it can be toggled by the software.
485
        log.debug('video_view_preview toggled %s' % (widget.get_active()))
486
        if self._widgets_changed_by_user:
487
            if widget.get_active():
488
                self.app.save_configuration() #gathers and saves
489
                self.preview_manager.start()
490
            else:
491
                self.preview_manager.stop()
492

    
493
    def on_main_tabs_switch_page(self, widget, notebook_page, page_number):
494
        """
495
        Called when the user switches to a different page.
496
        Pages names are : 
497
         * video_tab_contents
498
         * audio_tab_contents
499
         * system_tab_contents
500
         * about_tab_contents
501
        """
502
        tab_widget = widget.get_nth_page(page_number)
503
        tab_name = tab_widget.get_name()
504
        log.debug("Switching to tab %s" % (tab_name))
505
        if tab_name == "video_tab_contents":
506
            if not self.app.has_session():
507
                self.app.poll_x11_devices()
508
                self.app.poll_camera_devices()
509
        elif tab_name == "audio_tab_contents":
510
            pass
511
        elif tab_name == "system_tab_contents":
512
            self.network_admin_widget.grab_default()
513
        elif tab_name == "midi_tab_contents":
514
            if not self.app.has_session():
515
                self.app.poll_midi_devices()
516
                log.debug("polling MIDI devices")
517

    
518
    def on_contact_list_changed(self, *args):
519
        """
520
        Called when the selected contact has changed. 
521
        
522
        We'll also call this when a new contact has just been added. 
523
        """ 
524
        #print("on_contact_list_changed")
525
        # FIXME: what is args?
526
        tree_list, self.selected_contact_row = args[0].get_selected()
527
        is_streaming = self.app.has_session()
528
        if self.selected_contact_row:
529
            #print "yes, selected_contact_row"
530
            # make the edit, remove, invite buttons sensitive:
531
            self.edit_contact_widget.set_sensitive(True)
532
            self.remove_contact_widget.set_sensitive(True)
533
            self.invite_contact_widget.set_sensitive(True)
534
            # get selected contact
535
            self.selected_contact_index = tree_list.get_path(self.selected_contact_row)[0] # FIXME: this var should be deprecated
536
            self.app.address_book.selected_contact = self.app.address_book.contact_list[self.selected_contact_index] # FIXME: deprecate this!
537
            self.app.address_book.selected = self.selected_contact_index
538
        else:
539
            # make the edit, remove, invite buttons sensitive:
540
            self.edit_contact_widget.set_sensitive(False)
541
            self.remove_contact_widget.set_sensitive(False)
542
            self.invite_contact_widget.set_sensitive(False)
543
            # no contact is selected
544
            self.app.address_book.selected_contact = None
545
        if is_streaming: # XXX
546
            self.update_invite_button_with_contact_name()
547
            self.invite_contact_widget.set_sensitive(True)
548

    
549
    # ---------------------- slots for addressbook widgets events --------
550
    
551
    def on_contact_double_clicked(self, *args):
552
        """
553
        When a contact in the list is double-clicked, 
554
        shows the edit contact dialog.
555
        """
556
        self.on_edit_contact_clicked(args)
557

    
558
    def on_add_contact_clicked(self, *args):
559
        """
560
        Pops up a dialog to be filled with new contact infos.
561
        
562
        The add_contact buttons has been clicked.
563
        """
564
        self.app.address_book.current_contact_is_new = True
565
        # Update the text in the edit/new contact dialog:
566
        self.contact_name_widget.set_text("")
567
        self.contact_addr_widget.set_text("")
568
        self.contact_auto_accept_widget.set_active(False)
569
        self.edit_contact_window.show()
570

    
571
    def on_remove_contact_clicked(self, *args):
572
        """
573
        Upon confirmation, the selected contact is removed.
574
        """
575
        def _on_confirm_result(result):
576
            if result:
577
                del self.app.address_book.contact_list[self.selected_contact_index]
578
                self.contact_tree.remove(self.selected_contact_row)
579
                num = self.selected_contact_index - 1
580
                if num < 0:
581
                    num = 0
582
                self.selection.select_path(num)
583
        text = _("<b><big>Delete this contact from the list?</big></b>\n\nAre you sure you want "
584
            "to delete this contact from the list?")
585
        self.show_confirm_dialog(text, _on_confirm_result)
586

    
587
    def on_edit_contact_clicked(self, *args):
588
        """
589
        Shows the edit contact dialog.
590
        """
591
        contact = self.app.address_book.selected_contact
592
        self.contact_name_widget.set_text(contact["name"])
593
        self.contact_addr_widget.set_text(contact["address"])
594
        auto_accept = False
595
        if contact["auto_accept"]:
596
            auto_accept = True
597
            log.debug('auto accept should be true') # FIXME: why is this printed ?
598
        self.contact_auto_accept_widget.set_active(auto_accept)
599
        self.edit_contact_window.show() # addr
600

    
601
    def on_edit_contact_cancel_clicked(self, *args):
602
        """
603
        The cancel button in the "edit_contact" window has been clicked.
604
        Hides the edit_contact window.
605
        """
606
        self.edit_contact_window.hide()
607

    
608
    def on_edit_contact_save_clicked(self, *args):
609
        """
610
        The save button in the "edit_contact" window has been clicked.
611
        Hides the edit_contact window and saves the changes. (new or modified contact)
612
        """
613
        def _when_valid_save():
614
            # Saves contact info after it's been validated and then closes the window
615
            # THIS IS WHERE WE CREATE THE CONTACTS IN THE ADDRESSBOOK
616
            # TODO: move to a dedicated function in save.py or so.
617
            contact = {
618
                "name": self.contact_name_widget.get_text().strip(),
619
                "address": addr,
620
                "auto_accept": self.contact_auto_accept_widget.get_active(),
621
                }
622
            contact_markup = format_contact_markup(contact)
623
            if self.app.address_book.current_contact_is_new:
624
                self.contact_tree.append([contact_markup]) # add it to the tree list
625
                self.app.address_book.contact_list.append(contact) # and the internal address book
626
                self.selection.select_path(len(self.app.address_book.contact_list) - 1) # select it ...?
627
                self.app.address_book.selected_contact = self.app.address_book.contact_list[len(self.app.address_book.contact_list) - 1] #FIXME: we should not copy a dict like that
628
                self.app.address_book.current_contact_is_new = False # FIXME: what does that mean?
629
            else:
630
                self.contact_tree.set_value(self.selected_contact_row, 0, contact_markup)
631
                self.app.address_book.contact_list[self.selected_contact_index] = contact # FIXME: this is flaky. make some functions to handle this
632
            self.app.address_book.selected_contact = contact
633
            self.edit_contact_window.hide()
634

    
635
        # Validate the address
636
        addr = self.contact_addr_widget.get_text().strip()
637
        if not network.validate_address(addr):
638
            dialogs.ErrorDialog.create(_("The address is not valid\n\nEnter a valid address\nExample: 192.0.32.10 or example.org"), parent=self.main_window)
639
            return
640
        # save it.
641
        _when_valid_save()
642

    
643
    # ---------------------------- Custom system tab buttons ---------------
644

    
645
    def on_network_admin_clicked(self, *args):
646
        """
647
        Opens the network-admin Gnome applet.
648
        """
649
        process.run_once("gksudo", "network-admin")
650

    
651
    def on_system_shutdown_clicked(self, *args):
652
        """
653
        Shuts down the computer.
654
        """
655
        def _on_confirm_result(result):
656
            if result:
657
                process.run_once("gksudo", "shutdown -h now")
658

    
659
        text = _("<b><big>Shutdown the computer?</big></b>\n\nAre you sure you want to shutdown the computer now?")
660
        self.show_confirm_dialog(text, _on_confirm_result)
661

    
662
    def on_system_reboot_clicked(self, *args):
663
        """
664
        Reboots the computer.
665
        """
666
        def _on_confirm_result(result):
667
            if result:
668
                process.run_once("gksudo", "shutdown -r now")
669

    
670
        text = _("<b><big>Reboot the computer?</big></b>\n\nAre you sure you want to reboot the computer now?")
671
        self.show_confirm_dialog(text, _on_confirm_result)
672

    
673
    def on_maintenance_apt_update_clicked(self, *args):
674
        """
675
        Opens APT update manager.
676
        """
677
        process.run_once("gksudo", "update-manager")
678

    
679
    def on_maintenance_send_info_clicked(self, *args):
680
        """
681
        Sends an email to SAT with this information : 
682
         * milhouse version
683
         * kernel version
684
         * Loaded kernel modules
685
        """
686
        # TODO: move this to an other file.
687
        def _on_confirm_result(result):
688
            milhouse_version = "unknown"
689
            if result:
690
                msg = "--- milhouse_version ---\n" + milhouse_version + "\n"
691
                msg += "--- uname -a ---\n"
692
                try:
693
                    w, r, err = os.popen3('uname -a')
694
                    msg += r.read() + "\n"
695
                    errRead = err.read()
696
                    if errRead:
697
                        msg += errRead + "\n"
698
                    w.close()
699
                    r.close()
700
                    err.close()
701
                except:
702
                    msg += "Error executing 'uname -a'\n"
703
                msg += "--- lsmod ---\n"
704
                try:
705
                    w, r, err = os.popen3('lsmod')
706
                    msg += r.read()
707
                    errRead = err.read()
708
                    if errRead:
709
                        msg += "\n" + errRead
710
                    w.close()
711
                    r.close()
712
                    err.close()
713
                except:
714
                    msg += "Error executing 'lsmod'"
715
                fromaddr = self.app.config.email_info
716
                toaddrs  = self.app.config.email_info
717
                toaddrs = toaddrs.split(', ')
718
                server = smtplib.SMTP(self.app.config.smtpserver)
719
                server.set_debuglevel(0)
720
                try:
721
                    server.sendmail(fromaddr, toaddrs, msg)
722
                except:
723
                    dialogs.ErrorDialog.create(_("Could not send info.\nCheck your internet connection."), parent=self.main_window)
724
                server.quit()
725
        
726
        text = _("<b><big>Send the settings?</big></b>\n\nAre you sure you want to send your computer settings to the administrator of scenic?")
727
        self.show_confirm_dialog(text, _on_confirm_result)
728

    
729
    # --------------------- configuration and widgets value ------------
730

    
731
    def _gather_configuration(self):
732
        """
733
        Updates the configuration with the value of each widget.
734
        """
735
        def _set_config(attribute_name, value):
736
            """
737
            Sets a configuration entry value and prints it.
738
            """
739
            if not hasattr(self.app.config, attribute_name):
740
                raise RuntimeError("Config has no attribute %s" % (attribute_name))
741
            cast = type(getattr(self.app.config, attribute_name))
742
            if type(value) is not cast:
743
                raise RuntimeError("Wrong type for attribute %s." % (attribute_name))
744
            setattr(self.app.config, attribute_name, value)
745
            log.debug(" * %s: %s" % (attribute_name, getattr(self.app.config, attribute_name)))
746

    
747
        log.debug("Gathering configuration from the GUI widgets.")
748
        # VIDEO:
749
        _set_config("video_send_enabled", self.video_send_enabled_widget.get_active())
750
        _set_config("video_recv_enabled", self.video_receive_enabled_widget.get_active())
751
        _set_config("video_capture_size", _get_combobox_value(self.video_capture_size_widget))
752
        _set_config("video_display", _get_combobox_value(self.video_display_widget))
753
        video_source = _get_combobox_value(self.video_source_widget)
754
        if video_source == "Color bars":
755
            _set_config("video_source", "videotestsrc")
756
        else:
757
            #video_device = self.app.parse_v4l2_device_name(video_source)
758
            #if video_device is None:
759
            #    print "Could not find video device %s" % (video_source)
760
            #elif video_source.startswith("/dev/video"): # TODO: firewire!
761
            #TODO: check if it is a v4l2 device.
762
            _set_config("video_source", "v4l2src")
763
            _set_config("video_device", video_source) # Using the name and id as a video_device
764
            _set_config("video_input", self.v4l2_input_widget.get_active())  # we need to save it, but the restoration of the widget value is done when we poll the cameras
765
            _set_config("video_standard", _get_combobox_value(self.v4l2_standard_widget)) # same as line above
766
        _set_config("video_codec", VIDEO_CODECS[_get_combobox_value(self.video_codec_widget)])
767
        _set_config("video_aspect_ratio", _get_combobox_value(self.aspect_ratio_widget))
768
        _set_config("video_fullscreen", self.video_fullscreen_widget.get_active())
769
        _set_config("video_deinterlace",  self.video_deinterlace_widget.get_active())
770
        _set_config("video_jitterbuffer", self.video_jitterbuffer_widget.get_value_as_int()) # spinbutton
771
        _set_config("video_bitrate", float(self.video_bitrate_widget.get_value())) # spinbutton (float)
772
        _set_config("preview_in_window", self.preview_in_window_widget.get_active())
773
        
774
        # AUDIO:
775
        _set_config("audio_send_enabled", self.audio_send_enabled_widget.get_active())
776
        _set_config("audio_recv_enabled", self.audio_receive_enabled_widget.get_active())
777
        _set_config("audio_video_synchronized", self.audio_video_synchronized_widget.get_active())
778
        _set_config("audio_source", AUDIO_SOURCES[_get_combobox_value(self.audio_source_widget)])
779
        _set_config("audio_codec", AUDIO_CODECS[_get_combobox_value(self.audio_codec_widget)])
780
        _set_config("audio_channels", self.audio_numchannels_widget.get_value_as_int()) # spinbutton
781
        _set_config("audio_input_buffer", self.audio_input_buffer_widget.get_value_as_int())
782
        _set_config("audio_output_buffer", self.audio_output_buffer_widget.get_value_as_int())
783
        _set_config("audio_jitterbuffer", self.audio_jitterbuffer_widget.get_value_as_int()) # spinbutton
784
        
785
        # MIDI:
786
        _set_config("midi_send_enabled", self.midi_send_enabled_widget.get_active())
787
        _set_config("midi_recv_enabled", self.midi_recv_enabled_widget.get_active())
788
        _set_config("midi_input_device", _get_combobox_value(self.midi_input_device_widget))
789
        _set_config("midi_output_device", _get_combobox_value(self.midi_output_device_widget))
790
        _set_config("midi_jitterbuffer", self.midi_jitterbuffer_widget.get_value_as_int())
791
        
792
    def update_widgets_with_saved_config(self):
793
        """
794
        Called once at startup.
795
         * Once the config file is read, and the devices have been polled
796
         * Sets the value of each widget according to the data stored in the configuration file.
797
        It could be called again, once another config file has been read.
798
        """
799
        def _get_config(attribute_name):
800
            value = getattr(self.app.config, attribute_name)
801
            log.debug(" * %s: %s" % (attribute_name, value))
802
            return value
803

    
804
        self._widgets_changed_by_user = False
805
        log.debug("Changing widgets value according to configuration.")
806
        #print(self.app.config.__dict__)
807
        # VIDEO:
808
        self.video_send_enabled_widget.set_active(_get_config("video_send_enabled"))
809
        self.video_receive_enabled_widget.set_active(_get_config("video_recv_enabled"))
810
        _set_combobox_choices(self.video_capture_size_widget, ALL_SUPPORTED_SIZE)
811
        _set_combobox_value(self.video_capture_size_widget, _get_config("video_capture_size"))
812
        _set_combobox_value(self.video_display_widget, _get_config("video_display"))
813
        _set_combobox_value(self.aspect_ratio_widget, _get_config("video_aspect_ratio"))
814
        self.video_fullscreen_widget.set_active(_get_config("video_fullscreen"))
815
        self.video_deinterlace_widget.set_active(_get_config("video_deinterlace"))
816
        self.video_jitterbuffer_widget.set_value(_get_config("video_jitterbuffer")) # spinbutton
817
        self.video_bitrate_widget.set_value(_get_config("video_bitrate")) # spinbutton
818
        self.preview_in_window_widget.set_active(_get_config("preview_in_window"))
819
        # VIDEO SOURCE AND DEVICE:
820
        if self.app.config.video_source == "videotestsrc":
821
            video_source = "Color bars"
822
        elif self.app.config.video_source == "v4l2src":
823
            video_source = self.app.config.video_device
824
        _set_combobox_value(self.video_source_widget, video_source)
825
        log.debug(' * video_source: %s' % (video_source))
826
        # VIDEO CODEC:
827
        video_codec = _get_key_for_value(VIDEO_CODECS, self.app.config.video_codec)
828
        _set_combobox_value(self.video_codec_widget, video_codec)
829
        log.debug(' * video_codec: %s' % (video_codec))
830
        
831
        # ADDRESSBOOK:
832
        # Init addressbook contact list:
833
        self.app.address_book.selected_contact = None
834
        self.app.address_book.current_contact_is_new = False
835
        if len(self.app.address_book.contact_list) > 0:
836
            for contact in self.app.address_book.contact_list:
837
                contact_markup = format_contact_markup(contact)
838
                self.contact_tree.append([contact_markup])
839
            self.selection.select_path(self.app.address_book.selected)
840
            self.edit_contact_widget.set_sensitive(True)
841
            self.remove_contact_widget.set_sensitive(True)
842
            self.invite_contact_widget.set_sensitive(True)
843
        else:
844
            self.edit_contact_widget.set_sensitive(False)
845
            self.remove_contact_widget.set_sensitive(False)
846
            self.invite_contact_widget.set_sensitive(False)
847
        # change invite button with the name of the selected contact
848
        self.update_invite_button_with_contact_name()
849

    
850
        # AUDIO:
851
        self.audio_send_enabled_widget.set_active(_get_config("audio_send_enabled"))
852
        self.audio_receive_enabled_widget.set_active(_get_config("audio_recv_enabled"))
853
        self.audio_video_synchronized_widget.set_active(_get_config("audio_video_synchronized"))
854
        self.audio_numchannels_widget.set_value(_get_config("audio_channels")) # spinbutton
855
        self.audio_input_buffer_widget.set_value(_get_config("audio_input_buffer"))
856
        self.audio_output_buffer_widget.set_value(_get_config("audio_output_buffer"))
857
        self.audio_jitterbuffer_widget.set_value(_get_config("audio_jitterbuffer"))
858
        # source, codec
859
        audio_source_readable = _get_key_for_value(AUDIO_SOURCES, self.app.config.audio_source)
860
        log.debug(" * audio_source: %s" % (audio_source_readable))
861
        _set_combobox_value(self.audio_source_widget, audio_source_readable)
862
        audio_codec = _get_key_for_value(AUDIO_CODECS, self.app.config.audio_codec)
863
        log.debug(" * audio_codec: %s" % (audio_codec))
864
        _set_combobox_value(self.audio_codec_widget, audio_codec)
865
        
866
        # MIDI:
867
        log.debug("MIDI jitterbuffer: %s" % (self.app.config.midi_jitterbuffer))
868
        self.midi_send_enabled_widget.set_active(_get_config("midi_send_enabled"))
869
        self.midi_recv_enabled_widget.set_active(_get_config("midi_recv_enabled"))
870
        _set_combobox_value(self.midi_input_device_widget, _get_config("midi_input_device"))
871
        _set_combobox_value(self.midi_output_device_widget, _get_config("midi_output_device"))
872
        self.make_midi_widget_sensitive_or_not()
873
        self.midi_jitterbuffer_widget.set_value(self.app.config.midi_jitterbuffer)
874
        self.make_audio_jitterbuffer_enabled_or_not()
875

    
876
        # IMPORTANT: (to be done last)
877
        self._widgets_changed_by_user = True
878

    
879
    def update_streaming_state(self):
880
        """
881
        Changes the sensitivity and state of many widgets according to if we are streaming or not.
882
        
883
        Makes most of the audio/video buttons and widgets sensitive or not.
884
        Changes the invite button:
885
         * the icon
886
         * the label
887
        Makes the contact list sensitive or not.
888
        """
889
        self._toggle_streaming_state_sensitivity()
890
        self._update_rtcp_stats()
891

    
892
    def _toggle_streaming_state_sensitivity(self):
893
        _video_widgets_to_toggle_sensitivity = [
894
            self.video_capture_size_widget,
895
            self.video_source_widget,
896
            self.aspect_ratio_widget,
897
            self.video_view_preview_widget,
898
            self.preview_in_window_widget, 
899
            ]
900
        
901
        _other_widgets_to_toggle_sensitivity = [
902
            self.audio_source_widget,
903
            self.audio_codec_widget,
904
            self.audio_numchannels_widget,
905
            self.audio_output_buffer_widget,
906
            self.audio_input_buffer_widget,
907
            self.audio_video_synchronized_widget,
908
            self.contact_list_widget,
909
            self.add_contact_widget,
910
            self.remove_contact_widget,
911
            self.edit_contact_widget,
912
            self.video_fullscreen_widget,
913
            self.video_deinterlace_widget,
914
            self.video_jitterbuffer_widget,
915
            self.video_codec_widget,
916
            self.video_display_widget,
917
            self.midi_input_device_widget, 
918
            self.midi_output_device_widget,
919
            self.midi_send_enabled_widget, 
920
            self.midi_recv_enabled_widget,
921
            self.midi_jitterbuffer_widget,
922
            self.audio_send_enabled_widget,
923
            self.audio_receive_enabled_widget,
924
            self.audio_jitterbuffer_widget,
925
            self.video_send_enabled_widget,
926
            self.video_receive_enabled_widget,
927
            ]
928
        
929
        self.update_bitrate_and_codec()
930
        
931
        is_streaming = self.app.has_session()
932
        is_previewing =  self.preview_manager.is_busy()
933
        if is_streaming:
934
            details = self.app.streamer_manager.session_details
935
        _contact_list_currently_sensitive = self.contact_list_widget.get_property("sensitive")
936
        streaming_state_has_changed = is_streaming == _contact_list_currently_sensitive
937
        if streaming_state_has_changed:
938
            log.debug("streaming state has changed to %s" % (is_streaming))
939
            if is_streaming:
940
                text = _("Stop streaming")
941
                self.invite_label_widget.set_text(text)
942
                self.invite_contact_widget.set_sensitive(True)
943
                icon = gtk.STOCK_CONNECT
944
            else:
945
                self.update_invite_button_with_contact_name()
946
                icon = gtk.STOCK_DISCONNECT
947
                if len(self.app.address_book.contact_list) == 0: 
948
                    self.invite_contact_widget.set_sensitive(False)
949
            self.invite_icon_widget.set_from_stock(icon, 4)
950
            
951
            # Toggle sensitivity of many widgets:
952
            new_sensitivity = not is_streaming
953
            log.debug('Got to change the sensitivity of many widgets to %s' % (new_sensitivity))
954
            for widget in _other_widgets_to_toggle_sensitivity:
955
                widget.set_sensitive(new_sensitivity)
956
            for widget in _video_widgets_to_toggle_sensitivity:
957
                widget.set_sensitive(new_sensitivity)
958
                
959
            # Update the summary: 
960
            # peer: --------------------------------
961
            if is_streaming:
962
                peer_name = details["peer"]["name"]
963
                if details["peer"]["name"] != details["peer"]["address"]:
964
                    peer_name += " (%s)" % (details["peer"]["address"])
965
                self.info_peer_widget.set_text(peer_name)
966
            else:
967
                self.info_peer_widget.set_text(_("Not connected"))
968
            
969
            self.make_midi_widget_sensitive_or_not()       
970
        
971
        # also clean up the preview drawing area every second
972
        if self.preview_manager.is_busy():
973
            if self.preview_in_window_widget.get_property('sensitive'): # check if we have to change their state.
974
                for widget in _video_widgets_to_toggle_sensitivity:
975
                    if widget is not self.video_view_preview_widget:
976
                        widget.set_sensitive(False)
977
                # change icon
978
        else:
979
            if not is_streaming: # FIXME
980
                #if self.video_preview_icon_widget.get_stock() == gtk.STOCK_MEDIA_STOP:
981
                for widget in _video_widgets_to_toggle_sensitivity:
982
                    if widget is self.audio_jitterbuffer_widget:
983
                        self.make_audio_jitterbuffer_enabled_or_not()
984
                    else:
985
                        widget.set_sensitive(True)
986
            if self.preview_area_x_window_id is not None:
987
                if self.preview_area_widget.window is not None:
988
                    self.preview_area_widget.window.clear()
989

    
990
    def update_invite_button_with_contact_name(self):
991
        contact = self.app.address_book.selected_contact
992
        #is_streaming = self.app.has_session()
993
        if contact is None:
994
            text = _("Invite") #Please select a contact")
995
        else:
996
            text = _("Invite") # %(contact)s") % {"contact": contact["name"]}
997
        self.invite_label_widget.set_text(text)
998

    
999
    def make_midi_widget_sensitive_or_not(self):
1000
        # make the MIDI widget insensitive if disabled
1001
        log.debug("make_midi_widget_sensitive_or_not")
1002
        is_streaming = self.app.has_session()
1003
        if not is_streaming:
1004
            if not self.app.config.midi_send_enabled:
1005
                self.midi_input_device_widget.set_sensitive(False)
1006
            else:
1007
                self.midi_input_device_widget.set_sensitive(True)
1008
                
1009
            if not self.app.config.midi_recv_enabled:
1010
                self.midi_output_device_widget.set_sensitive(False)
1011
                self.midi_jitterbuffer_widget.set_sensitive(False)
1012
            else:
1013
                self.midi_output_device_widget.set_sensitive(True)
1014
                self.midi_jitterbuffer_widget.set_sensitive(True)
1015

    
1016
    def on_midi_send_enabled_toggled(self, *args):
1017
        if self._widgets_changed_by_user:
1018
            self._gather_configuration()
1019
            self.make_midi_widget_sensitive_or_not()
1020
    
1021
    def on_midi_recv_enabled_toggled(self, *args):
1022
        if self._widgets_changed_by_user:
1023
            self._gather_configuration()
1024
            self.make_midi_widget_sensitive_or_not()
1025

    
1026
    def _update_rtcp_stats(self):
1027
        is_streaming = self.app.has_session()
1028
        # update the audio and video summary:(even if the state has not just changed)
1029
        if is_streaming:
1030
            def _format_bitrate(bitrate):
1031
                """ Returns formatted bitrate string """
1032
                BITS_PER_MBIT = 1000000.0
1033
                BITS_PER_KBIT = 1000.0
1034
                if bitrate is not None:
1035
                    divisor = BITS_PER_MBIT
1036
                    prefix = "M"
1037
                    if bitrate < BITS_PER_MBIT:
1038
                        divisor = BITS_PER_KBIT
1039
                        prefix = "K"
1040
                    return " " + _("%2.2f %sbits/s") % (bitrate / divisor, prefix)
1041
                else:
1042
                    return ""
1043

    
1044
            def _format_audio_buffer(buffer_ms):
1045
                """
1046
                Formats audio buffer in/out for the summary.
1047
                """
1048
                return _("Audio buffer: %(buffer)d ms") % {"buffer": buffer_ms}
1049

    
1050

    
1051
            details = self.app.streamer_manager.session_details
1052
            rtcp_stats = self.app.streamer_manager.rtcp_stats
1053
            # send video: --------------------------------
1054
            _info_send_video = ""
1055
            if details["send"]["video"]["enabled"]:
1056
                _info_send_video += _("%(width)dx%(height)d %(codec)s") % {
1057
                    "width": details["send"]["video"]["width"], 
1058
                    "height": details["send"]["video"]["height"], 
1059
                    "codec": details["send"]["video"]["codec"], 
1060
                    }
1061
                _info_send_video += _format_bitrate(rtcp_stats["send"]["video"]["bitrate"])
1062
                _info_send_video += "\n"
1063
                #_video_packetloss = rtcp_stats["send"]["video"]["packets-loss-percent"]
1064
                _info_send_video += _("Jitter: %(jitter)d ns") % {# % is escaped with an other %
1065
                    "jitter": rtcp_stats["send"]["video"]["jitter"]
1066
                    }
1067
            else:
1068
                _info_send_video += _("Disabled")
1069

    
1070
            #_info_send_video += _("jitter: %(jitter)d ns. packet loss: %(packetloss)2.2f%%.") % {# % is escaped with an other %
1071
            #    "jitter": rtcp_stats["send"]["video"]["jitter"],
1072
            #    "packetloss": _video_packetloss
1073
            #    }
1074
            #print("info send video: " + _info_send_video)
1075
            self.info_send_video_widget.set_text(_info_send_video)
1076
            
1077
            # send audio: --------------------------------
1078
            _info_send_audio = ""
1079
            if details["send"]["audio"]["enabled"]:
1080
                _info_send_audio += _("%(numchannels)d-channel %(codec)s") % {
1081
                    "numchannels": details["send"]["audio"]["numchannels"], 
1082
                    "codec": details["send"]["audio"]["codec"] 
1083
                    }
1084
                _info_send_audio += _format_bitrate(rtcp_stats["send"]["audio"]["bitrate"])
1085
                _info_send_audio += "\n"
1086
                _info_send_audio += _format_audio_buffer(details["send"]["audio"]["buffer"])
1087
                _info_send_audio += "\n"
1088
                #_audio_packetloss = rtcp_stats["send"]["audio"]["packets-loss-percent"]
1089
                _info_send_audio += _("Jitter: %(jitter)d ns") % { # % is escaped with an other %
1090
                    "jitter": rtcp_stats["send"]["audio"]["jitter"]
1091
                    }
1092
            else:
1093
                _info_send_audio += _("Disabled.")
1094
            self.info_send_audio_widget.set_text(_info_send_audio)
1095
            # recv video: --------------------------------
1096
            _info_recv_video = ""
1097
            if details["receive"]["video"]["enabled"]:
1098
                _info_recv_video += _("%(width)dx%(height)d %(codec)s") % {
1099
                    "width": details["receive"]["video"]["width"], 
1100
                    "height": details["receive"]["video"]["height"], 
1101
                    "codec": details["receive"]["video"]["codec"], 
1102
                    }
1103
                _info_recv_video += _format_bitrate(rtcp_stats["receive"]["video"]["bitrate"])
1104
                _info_recv_video += "\n" + _("Display: %(display)s") % {"display": details["receive"]["video"]["display"]}
1105
                if details["receive"]["video"]["fullscreen"]:
1106
                    _info_recv_video += "\n" + _("Fullscreen is enabled.")
1107
            else:
1108
                _info_recv_video += _("Disabled.")
1109
                
1110
            #print("info recv video: " + _info_recv_video)
1111
            self.info_receive_video_widget.set_text(_info_recv_video)
1112
            # recv audio: --------------------------------
1113
            _info_recv_audio = ''
1114
            if details["receive"]["audio"]["enabled"]:
1115
                _info_recv_audio = _("%(numchannels)d-channel %(codec)s") % {
1116
                    "numchannels": details["receive"]["audio"]["numchannels"], 
1117
                    "codec": details["receive"]["audio"]["codec"] 
1118
                    }
1119
                _info_recv_audio += _format_bitrate(rtcp_stats["receive"]["audio"]["bitrate"])
1120
                _info_recv_audio += "\n"
1121
                _info_recv_audio += _format_audio_buffer(details["receive"]["audio"]["buffer"])
1122
                _info_recv_audio += "\n"
1123
            else:
1124
                _info_recv_audio += _("Disabled.")
1125

    
1126
            self.info_receive_audio_widget.set_text(_info_recv_audio)
1127
            # MIDI : --------------------------
1128
            _info_recv_midi = ""
1129
            _info_send_midi = ""
1130
            if details["receive"]["midi"]["enabled"]:
1131
                _info_recv_midi += _("Receiving MIDI") + "\n"
1132
                #_info_recv_midi += _("Output device: %(name)s" % {"name": self.app.config.midi_output_device}) + "\n"
1133
                _info_recv_midi += _("Jitter buffer: %(jitterbuffer)d ms" % {"jitterbuffer": self.app.config.midi_jitterbuffer})
1134
            else:
1135
                _info_recv_midi += _("Disabled")
1136
            if details["send"]["midi"]["enabled"]:
1137
                _info_send_midi += _("Sending MIDI") + "\n"
1138
                #_info_send_midi += _("Input device: %(name)s" % {"name": self.app.config.midi_input_device})
1139
            else:
1140
                _info_send_midi += _("Disabled")
1141
                
1142
            self.info_receive_midi_widget.set_text(_info_recv_midi)
1143
            self.info_send_midi_widget.set_text(_info_send_midi)
1144

    
1145
        else: # not streaming
1146
            self.info_send_video_widget.set_text("")
1147
            self.info_send_audio_widget.set_text("")
1148
            self.info_receive_video_widget.set_text("")
1149
            self.info_receive_audio_widget.set_text("")
1150
            self.info_receive_midi_widget.set_text("")
1151
            self.info_send_midi_widget.set_text("")
1152

    
1153
    def write_info_in_debug_tab(self):
1154
        """
1155
        Writes some information in the debug textview, if debugging is enabled. 
1156
        """
1157
        def _format_command_line(cmd):
1158
            """
1159
            Formats a bash command line for the summary.
1160
            """
1161
            text = cmd
1162
            #words = cmd.split()
1163
            #length = 0
1164
            #text = ""
1165
            #for word in words: 
1166
            #    length += len(word)
1167
            #    if length >= 40:
1168
            #        text += "\n"
1169
            #        length = 0
1170
            #    text += word + " "
1171
            return _("$ %(command)s") % {"command": text} 
1172
        if self.enable_debug:
1173
            _debug_text = ""
1174
            _debug_text += _("Command lines:") + "\n"
1175
            for command in self.app.streamer_manager.get_command_lines():
1176
                _debug_text += _format_command_line(command) + "\n"
1177
            self.debug_textview_widget.get_buffer().set_text(unicode(_debug_text))
1178
        else:
1179
            pass
1180
            #if self.enable_debug:
1181
            #    self.debug_textview_widget.get_buffer().set_text(u"")
1182
        
1183
    def update_local_ip(self):
1184
        """
1185
        Updates the local IP addresses widgets.
1186
        Called every n seconds.
1187
        """
1188
        def _cb(result):
1189
            """
1190
            @param result: list of ipv4 addresses
1191
            """
1192
            num = len(result)
1193
            txt = ""
1194
            for i in range(len(result)):
1195
                ip = result[i]
1196
                txt += ip
1197
                if i != num - 1:
1198
                    txt += "\n"
1199
            self.info_ip_widget.set_text(txt)
1200
        deferred = networkinterfaces.list_network_interfaces_addresses()
1201
        deferred.addCallback(_cb)
1202

    
1203
    def on_audio_video_synchronized_toggled(self, *args):
1204
        """
1205
        Called when the user toggles on/off the synchronization of audio and video. 
1206
        """
1207
        #if self._widgets_changed_by_user:
1208
        self.make_audio_jitterbuffer_enabled_or_not()
1209

    
1210
    def on_video_receive_enabled_toggled(self, *args):
1211
        self.make_audio_jitterbuffer_enabled_or_not()
1212

    
1213
    def make_audio_jitterbuffer_enabled_or_not(self):
1214
        """
1215
        Checks if the audio_jitterbuffer button should be sensitive or not and sets it.
1216
        """
1217
        sync_enabled = self.audio_video_synchronized_widget.get_active()
1218
        video_recv_enabled = self.video_receive_enabled_widget.get_active()
1219
        if sync_enabled and video_recv_enabled:
1220
            self.audio_jitterbuffer_widget.set_sensitive(False)
1221
        else:
1222
            self.audio_jitterbuffer_widget.set_sensitive(True)
1223
        
1224
    def on_audio_codec_changed(self, widget):
1225
        """
1226
        Called when the user selects a different audio codec, updates
1227
        the range of the numchannels box.
1228
        """
1229
        old_numchannels = self.audio_numchannels_widget.get_value()
1230
        max_channels = None
1231
        if _get_combobox_value(self.audio_codec_widget) == "MP3":
1232
            max_channels = 2
1233
        elif _get_combobox_value(self.audio_codec_widget) == "Raw":
1234
            max_channels = self.app.config.audio_maximum_number_of_channels_in_raw  
1235
        elif _get_combobox_value(self.audio_codec_widget) == "Vorbis":
1236
            max_channels = 24 
1237
        # update range and clamp numchannels to new range 
1238
        self.audio_numchannels_widget.set_range(1, max_channels)
1239
        self.audio_numchannels_widget.set_value(min(old_numchannels, max_channels)) 
1240

    
1241
    def update_bitrate_and_codec(self):
1242
        old_bitrate = self.video_bitrate_widget.get_value()
1243
        codec = _get_combobox_value(self.video_codec_widget)
1244
        is_streaming = self.app.has_session()
1245
        if codec in VIDEO_BITRATE_MIN_MAX.keys():
1246
            if is_streaming:
1247
                self.video_bitrate_widget.set_sensitive(False)
1248
            else:
1249
                self.video_bitrate_widget.set_sensitive(True)
1250
            mini = VIDEO_BITRATE_MIN_MAX[codec][0]
1251
            maxi = VIDEO_BITRATE_MIN_MAX[codec][1]
1252
            self.video_bitrate_widget.set_range(mini, maxi)
1253
            self.video_bitrate_widget.set_value(min(maxi, max(old_bitrate, mini)))
1254
        else:
1255
            self.video_bitrate_widget.set_sensitive(False)
1256
    
1257
    def on_video_codec_changed(self, widget):
1258
        self.update_bitrate_and_codec()
1259
        
1260
    def update_x11_devices(self):
1261
        """
1262
        Called once Application.poll_x11_devices has been run
1263
        """
1264
        x11_displays = [display["name"] for display in self.app.devices["x11_displays"]]
1265
        log.debug("Updating X11 displays with values %s" % (x11_displays))
1266
        _set_combobox_choices(self.video_display_widget, x11_displays)
1267

    
1268
    def update_midi_devices(self):
1269
        """
1270
        Called once Application.poll_midi_devices has been run
1271
        """
1272
        self._widgets_changed_by_user = False
1273
        input_devices = [self.app.format_midi_device_name(device) for device in self.app.devices["midi_input_devices"]]
1274
        output_devices = [self.app.format_midi_device_name(device) for device in self.app.devices["midi_output_devices"]]
1275
        log.debug("Updating MIDI devices with values %s %s" % (input_devices, output_devices))
1276
        _set_combobox_choices(self.midi_input_device_widget, input_devices)
1277
        _set_combobox_choices(self.midi_output_device_widget, output_devices)
1278
        self._widgets_changed_by_user = True
1279

    
1280
    def update_camera_devices(self):
1281
        """
1282
        Called once Application.poll_camera_devices has been run
1283
        """
1284
        self._widgets_changed_by_user = False
1285
        video_sources = [self.app.format_v4l2_device_name(dev) for dev in self.app.devices["cameras"].values()]
1286
        video_sources.insert(0, VIDEO_TEST_INPUT)
1287
        log.debug("Updating video sources with values %s" % (video_sources))
1288
        _set_combobox_choices(self.video_source_widget, video_sources)
1289
        self.update_v4l2_inputs_size_and_norm()
1290
        self._widgets_changed_by_user = True
1291

    
1292
    def update_v4l2_inputs_size_and_norm(self):
1293
        """
1294
        Called when : 
1295
         * user chooses a different video source.
1296
        If the selected is not a V4L2, disables the input and norm widgets.
1297
        """
1298
        value = _get_combobox_value(self.video_source_widget)
1299
        self._widgets_changed_by_user = False
1300
        # change choices and value:
1301
        if value == VIDEO_TEST_INPUT:
1302
            # INPUTS:
1303
            self.v4l2_input_widget.set_sensitive(False)
1304
            self.v4l2_input_widget.set_active(-1)
1305
            # STANDARD:
1306
            self.v4l2_standard_widget.set_sensitive(False)
1307
            self.v4l2_standard_widget.set_active(-1)
1308
            # SIZE:
1309
            _set_combobox_choices(self.video_capture_size_widget, ALL_SUPPORTED_SIZE)
1310
        else:
1311
            # INPUTS:
1312
            current_camera_name = _get_combobox_value(self.video_source_widget)
1313
            cam = self.app.parse_v4l2_device_name(current_camera_name)
1314
            if cam is None:
1315
                log.warning("v4l2 device is none !! %s" % (current_camera_name))
1316
                # INPUTS:
1317
                self.v4l2_input_widget.set_sensitive(False)
1318
                self.v4l2_input_widget.set_active(-1)
1319
                # STANDARD:
1320
                self.v4l2_standard_widget.set_sensitive(False)
1321
                self.v4l2_standard_widget.set_active(-1)
1322
                # SIZE:
1323
                _set_combobox_choices(self.video_capture_size_widget, ALL_SUPPORTED_SIZE)
1324
            else:
1325
                current_input = cam["input"]
1326
                if current_input is not None: # check if device has many inputs
1327
                    self.v4l2_input_widget.set_sensitive(True)
1328
                    _set_combobox_choices(self.v4l2_input_widget, cam["inputs"])
1329
                    _set_combobox_value(self.v4l2_input_widget, cam["inputs"][current_input]) # which in turn calls on_v4l2_input_changed
1330
                else:
1331
                    self.v4l2_input_widget.set_sensitive(False)
1332
                    self.v4l2_input_widget.set_active(-1)
1333
                
1334
                # STANDARD: 
1335
                current_standard = cam["standard"]
1336
                if current_standard is not None: # check if device supports different standards
1337
                    self.v4l2_standard_widget.set_sensitive(True)
1338
                    _set_combobox_choices(self.v4l2_standard_widget, VIDEO_STANDARDS)
1339
                    _set_combobox_value(self.v4l2_standard_widget, cam["standard"]) # which in turn calls on_v4l2_standard_changed
1340
                else:
1341
                    self.v4l2_standard_widget.set_sensitive(False)
1342
                    self.v4l2_standard_widget.set_active(-1)
1343
                #self.v4l2_standard_widget.set_sensitive(True)
1344
                # SIZE:
1345
                log.debug("supported sizes: %s" % (cam["supported_sizes"]))
1346
                _set_combobox_choices(self.video_capture_size_widget, cam["supported_sizes"]) # TODO: more test sizes
1347
        # once done:
1348
        self._widgets_changed_by_user = True
1349
            
1350
    def on_video_source_changed(self, widget):
1351
        """
1352
        Called when the user changes the video source.
1353
         * updates the input
1354
        """
1355
        if self._widgets_changed_by_user: 
1356
            full_name = _get_combobox_value(self.video_source_widget)
1357
            if full_name != VIDEO_TEST_INPUT:
1358
                dev = self.app.parse_v4l2_device_name(full_name)
1359
                current_camera_name = dev["name"]
1360
                self.app.poll_camera_devices()
1361
            self.update_v4l2_inputs_size_and_norm()
1362

    
1363
    def on_v4l2_standard_changed(self, widget):
1364
        """
1365
        When the user changes the V4L2 standard, we actually change this standard using milhouse.
1366
        Calls `milhouse --videodevice /dev/videoX --v4l2-standard XXX
1367
        Values are either NTSC or PAL.
1368
        """
1369
        if self._widgets_changed_by_user: 
1370
            # change standard for device
1371
            full_name = _get_combobox_value(self.video_source_widget)
1372
            if full_name != VIDEO_TEST_INPUT:
1373
                dev = self.app.parse_v4l2_device_name(full_name)
1374
                current_camera_name = dev["name"]
1375
                def _cb2(result):
1376
                    # callback for the poll_cameras_devices deferred.
1377
                    # check if successfully changed norm
1378
                    # see below
1379
                    cameras = self.app.devices["cameras"]
1380
                    try:
1381
                        cam = cameras[current_camera_name]
1382
                    except KeyError, e:
1383
                        log.error("Camera %s disappeared once changed standard!" % (current_camera_name))
1384
                    else:
1385
                        actual_standard = cam["standard"]
1386
                        if actual_standard != standard_name:
1387
                            msg = _("Could not change V4L2 standard from %(current_standard)s to %(desired_standard)s for device %(device_name)s.") % {"current_standard": actual_standard, "desired_standard": standard_name, "device_name": current_camera_name}
1388
                            log.error(msg)
1389
                            dialogs.ErrorDialog.create(msg, parent=self.main_window)
1390
                            
1391
                            self._widgets_changed_by_user = False
1392
                            _set_combobox_value(self.v4l2_standard_widget, actual_standard)
1393
                            self._widgets_changed_by_user = True
1394
                            # Maybe we should show an error dialog in that case, or set the value to what it really is.
1395
                        else:
1396
                            log.info("Successfully changed standard to %s for device %s." % (actual_standard, current_camera_name))
1397
                            log.debug("Now polling cameras.")
1398
                    self.v4l2_standard_widget.set_sensitive(True)
1399
                
1400
                standard_name = _get_combobox_value(widget)
1401
                #cam = self.app.devices["cameras"][current_camera_name]
1402
                self.v4l2_standard_widget.set_sensitive(False)
1403
                d = cameras.set_v4l2_video_standard(device_name=current_camera_name, standard=standard_name)
1404
                def _cb(result):
1405
                    d2 = self.app.poll_camera_devices()
1406
                    d2.addCallback(_cb2)
1407
                    
1408
                d.addCallback(_cb)
1409
        
1410
    def on_v4l2_input_changed(self, widget):
1411
        """
1412
        When the user changes the V4L2 input, we actually change this input using milhouse.
1413
        Calls `milhouse --videodevice /dev/videoX --v4l2-input N
1414
        """
1415
        def _cb2(cameras):
1416
            self.app.devices["cameras"] = cameras # important! needed by parse_v4l2_device_name
1417
            try:
1418
                cam = self.app.parse_v4l2_device_name(current_camera_name)
1419
            except KeyError, e:
1420
                log.error("Camera %s disappeared once changed input!" % (current_camera_name))
1421
                log.error("List of cameras: %s" % (cameras))
1422
            else:
1423
                if cam is None:
1424
                    log.error("Camera %s disappeared once changed input!" % (current_camera_name))
1425
                else:
1426
                    log.debug("Cameras: %s" % (cameras))
1427
                    actual_input = cam["input"]
1428
                    if actual_input != desired_input_number:
1429
                        msg = _("Could not change V4L2 input from %(current_input)s to %(desired_input)s for device %(device_name)s.") % {"current_input": actual_input, "desired_input": desired_input_number, "device_name": current_camera_name}
1430
                        log.error(msg)
1431
                        # Maybe we should show an error dialog in that case, or set the value to what it really is.
1432
                    else:
1433
                        log.info("Successfully changed input to %s for device %s." % (actual_input, current_camera_name))
1434

    
1435
        def _cb(result):
1436
            d2 = cameras.list_cameras()
1437
            d2.addCallback(_cb2)
1438

    
1439
        if self._widgets_changed_by_user:
1440
            # change input for device
1441
            current_camera_name = _get_combobox_value(self.video_source_widget)
1442
            if current_camera_name != VIDEO_TEST_INPUT:
1443
                input_name = _get_combobox_value(widget) # self.v4l2_input_widget
1444
                #cam = self.app.devices["cameras"][input_name]
1445
                cam = self.app.parse_v4l2_device_name(current_camera_name)
1446
                if cam is None:
1447
                    log.error("No such v4l2 device: %s" % (input_name))
1448
                else:
1449
                    desired_input_number = cam["inputs"].index(input_name)
1450
                    d = cameras.set_v4l2_input_number(device_name=cam["name"], input_number=desired_input_number)
1451
                    d.addCallback(_cb)
1452

    
1453
    # -------------------------- menu items -----------------
1454
    
1455
    def on_about_menu_item_activate(self, menu_item):
1456
        About.create() # TODO: set parent window ?
1457
    
1458
    def on_quit_menu_item_activated(self, menu_item):
1459
        """
1460
        Quits the application.
1461
        """
1462
        log.info("Menu item 'Quit' chosen")
1463
        self._confirm_and_quit()
1464
    
1465
    def on_help_menu_item_activated(self, menu_item):
1466
        """
1467
        Opens a web browser to the scenic web site.
1468
        """
1469
        log.info("Menu item 'Help' chosen")
1470
        docbook_file = os.path.join(configure.DOCBOOK_DIR, "user-manual.xml")
1471
        process.run_once("yelp", docbook_file)
1472

    
1473
    # ---------------------- invitation dialogs -------------------
1474

    
1475
    def on_invite_contact_clicked(self, *args):
1476
        """
1477
        Sends an INVITE to the remote peer.
1478
        """
1479
        if self.app.has_session():
1480
            self.app.stop_streamers()
1481
        elif self.app.has_negotiation_in_progress():
1482
            log.error("Invite button clicked but we have a negotiation in progress.")
1483
        else:
1484
            self.app.send_invite()
1485

    
1486
    def show_confirm_dialog(self, text, callback=None):
1487
        """
1488
        This could be replaced by a yes/no dialog. That's actually what it is.
1489
        """
1490
        deferred = self.confirm_dialog.show(text)
1491
        if callback is not None:
1492
            deferred.addCallback(callback)
1493

    
1494
    def show_invited_dialog(self, text):
1495
        """ 
1496
        This could be replaced by a yes/no dialog. That's actually what it is.
1497
        @rtype: L{Deferred}
1498
        """
1499
        return self.invited_dialog.show(text)
1500

    
1501
    def show_calling_dialog(self):
1502
        """
1503
        Creates a new widget and show it.
1504
        """
1505
        self.calling_dialog = None
1506
        widgets_tree = glade.get_widgets_tree()
1507
        self.calling_dialog = widgets_tree.get_widget("calling_dialog")
1508
        # FIXME: can't set a parent on a toplevel window
1509
        #self.calling_dialog.set_parent(self.main_window)
1510
        self.calling_dialog.connect('delete-event', self.on_invite_contact_cancelled)
1511
        self.calling_dialog.show()
1512
    
1513
    def on_invite_contact_cancelled(self, *args):
1514
        """
1515
        Sends a CANCEL to the remote peer when invite contact window is closed.
1516
        """
1517
        self.hide_calling_dialog()
1518
        log.debug("Th user has closed the inviting window.")
1519
        self.app.send_cancel_and_disconnect(reason=communication.CANCEL_REASON_CANCELLED)
1520
        return True # don't let the delete-event propagate
1521

    
1522
    def hide_calling_dialog(self):
1523
        """
1524
        Hides the "calling_dialog" dialog.
1525
        """
1526
        if self.calling_dialog is not None:
1527
            self.calling_dialog.hide()
1528

    
1529
    def update_jackd_status(self):
1530
        is_zombie = self.app.devices["jackd_is_zombie"]
1531
        is_running = self.app.devices["jackd_is_running"]
1532
        fill_stats = False
1533
        if is_zombie:
1534
                self.audio_jack_state_widget.set_markup(_("<b>Zombie</b>"))
1535
                self.audio_jack_icon_widget.set_from_stock(gtk.STOCK_DIALOG_WARNING, 4)
1536
        else:
1537
            if is_running:
1538
                self.audio_jack_state_widget.set_markup(_("<b>Running</b>"))
1539
                self.audio_jack_icon_widget.set_from_stock(gtk.STOCK_YES, 4)
1540
                fill_stats = True
1541
            else:
1542
                self.audio_jack_state_widget.set_markup(_("<b>Not running</b>"))
1543
                self.audio_jack_icon_widget.set_from_stock(gtk.STOCK_NO, 4)
1544
        if fill_stats:
1545
            j = self.app.devices["jack_servers"][0] 
1546
            try:
1547
                latency = (j["period"] * j["nperiods"] / float(j["rate"])) * 1000 # ms
1548
            except KeyError, e:
1549
                log.error('Key %s is missing for the jack server process' % (e))
1550
            else:
1551
                self.jack_latency_widget.set_text("%4.2f ms" % (latency))
1552
                self.jack_sampling_rate_widget.set_text("%d Hz" % (j["rate"]))
1553
        else:
1554
            self.jack_latency_widget.set_text("")
1555
            self.jack_sampling_rate_widget.set_text("")
1556
            
1557

    
1558
class About(object):
1559
    """
1560
    About dialog
1561
    """
1562
    def __init__(self):
1563
        # TODO: set parent window ?
1564
        self.icon_file = os.path.join(configure.PIXMAPS_DIR, 'scenic.png')
1565
        self.about_dialog = gtk.AboutDialog()
1566

    
1567
    def show_about_dialog(self):
1568
        self.about_dialog.set_name(configure.APPNAME)
1569
        self.about_dialog.set_role('about')
1570
        self.about_dialog.set_version(configure.VERSION)
1571
        commentlabel = ONE_LINE_DESCRIPTION 
1572
        self.about_dialog.set_comments(commentlabel)
1573
        self.about_dialog.set_copyright(COPYRIGHT_SHORT) 
1574
        self.about_dialog.set_license(LICENSE_TEXT)
1575
        self.about_dialog.set_authors(AUTHORS_LIST)
1576
        #self.about_dialog.set_artists(['Public domain'])
1577
        gtk.about_dialog_set_url_hook(self.show_website)
1578
        self.about_dialog.set_website(PROJECT_WEBSITE)
1579
        if not os.path.exists(self.icon_file):
1580
            log.error("Could not find icon file %s." % (self.icon_file))
1581
        else:
1582
            large_icon = gtk.gdk.pixbuf_new_from_file(self.icon_file)
1583
            self.about_dialog.set_logo(large_icon)
1584
        # Connect to callbacks
1585
        self.about_dialog.connect('response', self.destroy_about)
1586
        self.about_dialog.connect('delete_event', self.destroy_about)
1587
        self.about_dialog.show_all()
1588

    
1589
    @staticmethod
1590
    def create():
1591
        """
1592
        @rtype: None
1593
        """
1594
        dialog = About()
1595
        return dialog.show_about_dialog()
1596
     
1597
    def show_website(self, widget, data):
1598
        webbrowser.open(data)
1599

    
1600
    def destroy_about(self, *args):
1601
        self.about_dialog.destroy()
1602