This URL has Read-Only access.

Statistics
| Branch: | Tag: | Revision:

root / py / scenic / gui.py @ 15818c72

History | View | Annotate | Download (55.6 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
Former Notes
26
------------
27
 * bug pour setter le bouton par defaut quand on change de tab. Il faut que le tab est le focus pour que ca marche. Pourtant le "print" apparait ???
28
"""
29
### CONSTANTS ###
30
from scenic import configure
31

    
32
INVITE_TIMEOUT = 10
33
ONLINE_HELP_URL = "http://svn.sat.qc.ca/trac/scenic/wiki/Documentation"
34
ONE_LINE_DESCRIPTION = """Scenic is a telepresence software oriented for live performances."""
35
ALL_SUPPORTED_SIZE = [ # by milhouse video
36
    "924x576",
37
    "768x480",
38
    "720x480",
39
    "704x480",
40
    "704x240",
41
    "640x480",
42
    "352x240",
43
    "320x240",
44
    "176x120"
45
    ]
46

    
47
### MODULES IMPORTS  ###
48

    
49
import sys
50
import os
51
import smtplib
52
import gtk.glade
53
import webbrowser
54
import gettext
55
from twisted.internet import reactor
56
from twisted.internet import task
57
from twisted.python.reflect import prefixedMethods
58
from scenic import process # just for constants
59
from scenic import dialogs
60
from scenic.devices import cameras
61

    
62
### MULTILINGUAL SUPPORT ###
63
_ = gettext.gettext
64
gettext.bindtextdomain(configure.APPNAME, configure.LOCALE_DIR)
65
gettext.textdomain(configure.APPNAME)
66
gtk.glade.bindtextdomain(configure.APPNAME, configure.LOCALE_DIR)
67
gtk.glade.textdomain(configure.APPNAME)
68

    
69
LICENSE_TEXT = _("""Scenic
70
Copyright (C) 2009 Society for Arts and Technology (SAT)
71
http://www.sat.qc.ca
72
All rights reserved.
73

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

79
Scenic is distributed in the hope that it will be useful,
80
but WITHOUT ANY WARRANTY; without even the implied warranty of
81
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
82
GNU General Public License for more details.
83

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

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

    
94
COPYRIGHT_SHORT = _("Copyright 2009-2010 Society for Arts and Technology")
95

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

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

    
116
def _set_combobox_choices(widget, choices=[]):
117
    """
118
    Sets the choices in a GTK combobox.
119
    """
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
    _set_combobox_value(widget, previous_value)
127

    
128
def _set_combobox_value(widget, value=None):
129
    """
130
    Sets the current value of a GTK ComboBox widget.
131
    """
132
    index = None
133
    tree_model = widget.get_model()
134
    index = 0
135
    for i in iter(tree_model):
136
        v = i[0]
137
        if v == value:
138
            break # got it
139
        index += 1
140
    if index is None:
141
        widget.set_active(-1)
142
    else:
143
        widget.set_active(index)
144

    
145
#videotestsrc legible name:
146
VIDEO_TEST_INPUT = "Color bars"
147

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

    
173
def format_contact_markup(contact):
174
    """
175
    Formats a contact for the Adressbook GTK widget.
176
    @param contact: A dict with keys "name" and "address"
177
    @rettype: str
178
    @return: Pango markup for the TreeView widget.
179
    """
180
    auto_accept = ""
181
    if contact["auto_accept"]:
182
        auto_accept = "\n  " + _("Automatically accept invitations")
183
    return "<b>%s</b>\n  IP: %s%s" % (contact["name"], contact["address"], auto_accept) 
184

    
185
class Gui(object):
186
    """
187
    Graphical User Interface
188
     * Contains the main GTK window.
189
     * And some dialogs.
190
    """
191
    def __init__(self, app, kiosk_mode=False, fullscreen=False):
192
        self.app = app
193
        self.load_gtk_theme(self.app.config.theme)
194
        self.kiosk_mode_on = kiosk_mode
195
        self._offerer_invite_timeout = None
196
        # Set the Glade file
197
        glade_file = os.path.join(configure.GLADE_DIR, 'scenic.glade')
198
        if os.path.isfile(glade_file):
199
            glade_path = glade_file
200
        else:
201
            text = _("Error : Could not find the Glade file %s. Exitting.") % (glade_file)
202
            print(text)
203
            sys.exit()
204
        self.widgets = gtk.glade.XML(glade_path, domain=configure.APPNAME)
205
        
206
        # connects callbacks to widgets automatically
207
        glade_signal_slots = {}
208
        for method in prefixedMethods(self, "on_"):
209
            glade_signal_slots[method.__name__] = method
210
        self.widgets.signal_autoconnect(glade_signal_slots)
211
        
212
        # Get all the widgets that we use
213
        self.main_window = self.widgets.get_widget("main_window")
214
        self.main_window.connect('delete-event', self.on_main_window_deleted)
215
        self.main_window.set_icon_from_file(os.path.join(configure.PIXMAPS_DIR, 'scenic.png'))
216
        self.main_tabs_widget = self.widgets.get_widget("mainTabs")
217
        self.system_tab_contents_widget = self.widgets.get_widget("system_tab_contents")
218
        self.main_window.connect("window-state-event", self.on_window_state_event)
219
        # confirm_dialog:
220
        self.confirm_dialog = self.widgets.get_widget("confirm_dialog")
221
        self.confirm_dialog.connect('delete-event', self.confirm_dialog.hide_on_delete)
222
        self.confirm_dialog.set_transient_for(self.main_window)
223
        self.confirm_label = self.widgets.get_widget("confirm_label")
224
        # calling_dialog:
225
        self.calling_dialog = self.widgets.get_widget("calling_dialog")
226
        self.calling_dialog.connect('delete-event', self.on_invite_contact_cancelled)
227
        # error_dialog:
228
        self.error_dialog = self.widgets.get_widget("error_dialog")
229
        self.error_dialog.connect('delete-event', self.error_dialog.hide_on_delete)
230
        self.error_dialog.set_transient_for(self.main_window)
231
        # Could not connect:
232
        self.error_label_widget = self.widgets.get_widget("error_dialog_label")
233
        # invited_dialog:
234
        self.invited_dialog = self.widgets.get_widget("invited_dialog")
235
        self.invited_dialog.set_transient_for(self.main_window)
236
        self.invited_dialog.connect('delete-event', self.invited_dialog.hide_on_delete)
237
        self.invited_dialog_label_widget = self.widgets.get_widget("invited_dialog_label")
238

    
239
        # invite button:
240
        self.invite_label_widget = self.widgets.get_widget("invite_label")
241
        self.invite_icon_widget = self.widgets.get_widget("invite_icon")
242
        
243
        # edit_contact_window:
244
        self.edit_contact_window = self.widgets.get_widget("edit_contact_window")
245
        self.edit_contact_window.set_transient_for(self.main_window) # child of main window
246
        self.edit_contact_window.connect('delete-event', self.edit_contact_window.hide_on_delete)
247
        # fields in the edit contact window:
248
        self.contact_name_widget = self.widgets.get_widget("contact_name")
249
        self.contact_addr_widget = self.widgets.get_widget("contact_addr")
250
        self.contact_auto_accept_widget = self.widgets.get_widget("contact_auto_accept")
251
        # addressbook buttons:
252
        self.edit_contact_widget = self.widgets.get_widget("edit_contact")
253
        self.add_contact_widget = self.widgets.get_widget("add_contact")
254
        self.remove_contact_widget = self.widgets.get_widget("remove_contact")
255
        self.invite_contact_widget = self.widgets.get_widget("invite_contact")
256
        # treeview:
257
        self.contact_list_widget = self.widgets.get_widget("contact_list")
258
        # position of currently selected contact in list of contact:
259
        self.selected_contact_row = None
260
        self.select_contact_index = None
261

    
262
        # Summary text view:
263
        self.info_peer_widget = self.widgets.get_widget("info_peer")
264
        self.info_send_video_widget = self.widgets.get_widget("info_send_video")
265
        self.info_send_audio_widget = self.widgets.get_widget("info_send_audio")
266
        self.info_receive_video_widget = self.widgets.get_widget("info_receive_video")
267
        self.info_receive_audio_widget = self.widgets.get_widget("info_receive_audio")
268

    
269
        # video
270
        self.video_capture_size_widget = self.widgets.get_widget("video_capture_size")
271
        self.video_display_widget = self.widgets.get_widget("video_display")
272
        self.video_bitrate_widget = self.widgets.get_widget("video_bitrate")
273
        self.video_source_widget = self.widgets.get_widget("video_source")
274
        self.video_codec_widget = self.widgets.get_widget("video_codec")
275
        self.video_fullscreen_widget = self.widgets.get_widget("video_fullscreen")
276
        self.video_view_preview_widget = self.widgets.get_widget("video_view_preview")
277
        self.video_deinterlace_widget = self.widgets.get_widget("video_deinterlace")
278
        self.aspect_ratio_widget = self.widgets.get_widget("aspect_ratio")
279
        self.v4l2_input_widget = self.widgets.get_widget("v4l2_input")
280
        self.v4l2_standard_widget = self.widgets.get_widget("v4l2_standard")
281
        self.video_jitterbuffer_widget = self.widgets.get_widget("video_jitterbuffer")
282
        
283
        # audio
284
        self.audio_source_widget = self.widgets.get_widget("audio_source")
285
        self.audio_codec_widget = self.widgets.get_widget("audio_codec")
286
        self.audio_jack_icon_widget = self.widgets.get_widget("audio_jack_icon")
287
        self.audio_jack_state_widget = self.widgets.get_widget("audio_jack_state")
288
        self.audio_numchannels_widget = self.widgets.get_widget("audio_numchannels")
289

    
290
        self.jack_latency_widget = self.widgets.get_widget("jack_latency")
291
        self.jack_sampling_rate_widget = self.widgets.get_widget("jack_sampling_rate")
292
        # system tab contents:
293
        self.network_admin_widget = self.widgets.get_widget("network_admin")
294
            
295
        # switch to Kiosk mode if asked
296
        if self.kiosk_mode_on:
297
            self.main_window.set_decorated(False)
298
        else:
299
            # Removes the sytem_tab 
300
            tab_num = self.main_tabs_widget.page_num(self.system_tab_contents_widget)
301
            print "Removing tab number %d." % (tab_num)
302
            self.main_tabs_widget.remove_page(tab_num)
303
        
304
        self.is_fullscreen = False
305
        if fullscreen:
306
            print("Making the main window fullscreen.")
307
            self.toggle_fullscreen()
308
        
309
        # Build the contact list view
310
        self.selection = self.contact_list_widget.get_selection()
311
        self.selection.connect("changed", self.on_contact_list_changed, None) 
312
        self.contact_tree = gtk.ListStore(str)
313
        self.contact_list_widget.set_model(self.contact_tree)
314
        column = gtk.TreeViewColumn(_("Contacts"), gtk.CellRendererText(), markup=False)
315
        self.contact_list_widget.append_column(column)
316
        self._v4l2_input_changed_by_user = True # if False, the software is changing those drop-down values itself.
317
        self._v4l2_standard_changed_by_user = True
318
        self._video_source_changed_by_user = True
319
        self.main_window.show()
320

    
321
        self._streaming_state_check_task = task.LoopingCall(self.update_streaming_state)
322
        self._streaming_state_check_task.start(1.0, now=False)
323
        # The main app must call init_widgets_value
324
   
325
    #TODO: for the preview in the drawing area   
326
    #def on_expose_event(self, widget, event):
327
    #    self.preview_xid = widget.window.xid
328
    #    return False
329

    
330
    # ------------------ window events and actions --------------------
331

    
332
    def load_gtk_theme(self, name="Darklooks"):
333
        file_name = os.path.join(os.path.join(configure.THEMES_DIR, name, "gtkrc"))
334
        # FIXME: not able to reload themes dynamically.
335
        if os.path.exists(file_name):
336
            #os.environ["GTK2_RC_FILES"] = file_name
337
            print("Loading GTK2 theme %s" % (file_name))
338
            gtk.rc_parse(file_name)
339
            print("Done loading GTK2 theme.")
340
            #gtk.rc_reset_styles(gtk.settings_get_default())
341
            #print "loading theme", file_name
342
            #gtk.rc_parse(file_name)
343
            #gtk.rc_reparse_all()
344
        else:
345
            print("File name not found: %s" % (file_name))
346
     
347
    def toggle_fullscreen(self):
348
        """
349
        Toggles the fullscreen mode on/off.
350
        """
351
        if self.is_fullscreen:
352
            self.main_window.unfullscreen()
353
        else:
354
            self.main_window.fullscreen()
355

    
356
    def on_window_state_event(self, widget, event):
357
        """
358
        Called when toggled fullscreen.
359
        """
360
        self.is_fullscreen = event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN != 0
361
        print('fullscreen %s' % (self.is_fullscreen))
362
        return True
363
    
364
    def on_main_window_deleted(self, *args):
365
        """
366
        Destroy method causes appliaction to exit
367
        when main window closed
368
        """
369
        return self._confirm_and_quit()
370
        
371
    def _confirm_and_quit(self):
372
        def _cb(result):
373
            if result:
374
                print("Destroying the window.")
375
                self.main_window.destroy()
376
            else:
377
                print("Not quitting.")
378
        # If you return FALSE in the "delete_event" signal handler,
379
        # GTK will emit the "destroy" signal. Returning TRUE means
380
        # you don't want the window to be destroyed.
381
        # This is useful for popping up 'are you sure you want to quit?'
382
        # type dialogs. 
383
        if self.app.config.confirm_quit and self.app.has_session():
384
            d = dialogs.YesNoDialog.create("Really quit ?\nAll streaming processes will quit as well.", parent=self.main_window)
385
            d.addCallback(_cb)
386
            return True
387
        else:
388
            _cb(True)
389
            return False
390
    
391
    def on_main_window_destroyed(self, *args):
392
        # TODO: confirm dialog!
393
        if reactor.running:
394
            print("reactor.stop()")
395
            reactor.stop()
396

    
397
    # --------------- slots for some widget events ------------
398

    
399
    def on_video_view_preview_toggled(self, widget):
400
        """
401
        Shows a preview of the video input.
402
        """
403
        #TODO: create a new process protocol for the preview window
404
        #TODO: stop it when starting to stream, if running
405
        #TODO: stop it when button is toggled to false.
406
        # It can be the user that pushed the button, or it can be toggled by the software.
407
        print 'video_view_preview toggled', widget.get_active()
408
        if widget.get_active():
409
            self.app.save_configuration() #gathers and saves
410
            width, height = self.app.config.video_capture_size.split("x")
411
            aspect_ratio = self.app.config.video_aspect_ratio
412
            command = "milhouse --videosource %s --localvideo --window-title preview --width %s --height %s --aspect-ratio %s" % (self.app.config.video_source, width, height, aspect_ratio)
413
            if self.app.config.video_source != "videotestsrc":
414
                command += " --videodevice %s" % (self.app.config.video_device)
415
            print "spawning $%s" % (command)
416
            process.run_once(*command.split())
417
            dialogs.ErrorDialog.create("You must manually close the preview window.", parent=self.main_window)
418
        else:
419
            print "should be stopping preview"
420

    
421
    def on_main_tabs_switch_page(self, widget, notebook_page, page_number):
422
        """
423
        Called when the user switches to a different page.
424
        Pages names are : 
425
         * contacts_tab_contents
426
         * video_tab_contents
427
         * audio_tab_contents
428
         * system_tab_contents
429
         * about_tab_contents
430
        """
431
        tab_widget = widget.get_nth_page(page_number)
432
        tab_name = tab_widget.get_name()
433
        if tab_name == "contacts_tab_contents":
434
            self.invite_contact_widget.grab_default()
435
        elif tab_name == "video_tab_contents":
436
            self.app.poll_x11_devices()
437
            self.app.poll_camera_devices()
438
        elif tab_name == "audio_tab_contents":
439
            pass
440
        elif tab_name == "system_tab_contents":
441
            self.network_admin_widget.grab_default()
442

    
443
    def on_contact_list_changed(self, *args):
444
        # FIXME: what is args?
445
        tree_list, self.selected_contact_row = args[0].get_selected()
446
        if self.selected_contact_row:
447
            # make the edit, remove, invite buttons sensitive:
448
            self.edit_contact_widget.set_sensitive(True)
449
            self.remove_contact_widget.set_sensitive(True)
450
            self.invite_contact_widget.set_sensitive(True)
451
            # get selected contact
452
            self.selected_contact_index = tree_list.get_path(self.selected_contact_row)[0] # FIXME: this var should be deprecated
453
            self.app.address_book.selected_contact = self.app.address_book.contact_list[self.selected_contact_index] # FIXME: deprecate this!
454
            self.app.address_book.selected = self.selected_contact_index
455
        else:
456
            # make the edit, remove, invite buttons sensitive:
457
            self.edit_contact_widget.set_sensitive(False)
458
            self.remove_contact_widget.set_sensitive(False)
459
            self.invite_contact_widget.set_sensitive(False)
460
            # no contact is selected
461
            self.app.address_book.selected_contact = None
462

    
463
    # ---------------------- slots for addressbook widgets events --------
464
    
465
    def on_contact_double_clicked(self, *args):
466
        """
467
        When a contact in the list is double-clicked, 
468
        shows the edit contact dialog.
469
        """
470
        self.on_edit_contact_clicked(args)
471

    
472
    def on_add_contact_clicked(self, *args):
473
        """
474
        Pops up a dialog to be filled with new contact infos.
475
        
476
        The add_contact buttons has been clicked.
477
        """
478
        self.app.address_book.current_contact_is_new = True
479
        # Update the text in the edit/new contact dialog:
480
        self.contact_name_widget.set_text("")
481
        self.contact_addr_widget.set_text("")
482
        self.contact_auto_accept_widget.set_active(False)
483
        self.edit_contact_window.show()
484

    
485
    def on_remove_contact_clicked(self, *args):
486
        """
487
        Upon confirmation, the selected contact is removed.
488
        """
489
        def _on_confirm_result(result):
490
            if result:
491
                del self.app.address_book.contact_list[self.selected_contact_index]
492
                self.contact_tree.remove(self.selected_contact_row)
493
                num = self.selected_contact_index - 1
494
                if num < 0:
495
                    num = 0
496
                self.selection.select_path(num)
497
        text = _("<b><big>Delete this contact from the list?</big></b>\n\nAre you sure you want "
498
            "to delete this contact from the list?")
499
        self.show_confirm_dialog(text, _on_confirm_result)
500

    
501
    def on_edit_contact_clicked(self, *args):
502
        """
503
        Shows the edit contact dialog.
504
        """
505
        contact = self.app.address_book.selected_contact
506
        self.contact_name_widget.set_text(contact["name"])
507
        self.contact_addr_widget.set_text(contact["address"])
508
        auto_accept = False
509
        if contact["auto_accept"]:
510
            auto_accept = True
511
            print('auto accept should be true')
512
        self.contact_auto_accept_widget.set_active(auto_accept)
513
        self.edit_contact_window.show() # addr
514

    
515
    def on_edit_contact_cancel_clicked(self, *args):
516
        """
517
        The cancel button in the "edit_contact" window has been clicked.
518
        Hides the edit_contact window.
519
        """
520
        self.edit_contact_window.hide()
521

    
522
    def on_edit_contact_save_clicked(self, *args):
523
        """
524
        The save button in the "edit_contact" window has been clicked.
525
        Hides the edit_contact window and saves the changes. (new or modified contact)
526
        """
527
        def _when_valid_save():
528
            # Saves contact info after it's been validated and then closes the window
529
            # THIS IS WHERE WE CREATE THE CONTACTS IN THE ADDRESSBOOK
530
            # TODO: move to a dedicated function in save.py or so.
531
            contact = {
532
                "name": self.contact_name_widget.get_text(),
533
                "address": addr,
534
                "auto_accept": self.contact_auto_accept_widget.get_active(),
535
                }
536
            contact_markup = format_contact_markup(contact)
537
            if self.app.address_book.current_contact_is_new:
538
                self.contact_tree.append([contact_markup]) # add it to the tree list
539
                self.app.address_book.contact_list.append(contact) # and the internal address book
540
                self.selection.select_path(len(self.app.address_book.contact_list) - 1) # select it ...?
541
                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
542
                self.app.address_book.current_contact_is_new = False # FIXME: what does that mean?
543
            else:
544
                self.contact_tree.set_value(self.selected_contact_row, 0, contact_markup)
545
                self.app.address_book.contact_list[self.selected_contact_index] = contact # FIXME: this is flaky. make some functions to handle this
546
            self.app.address_book.selected_contact = contact
547
            self.edit_contact_window.hide()
548

    
549
        # Validate the address
550
        addr = self.contact_addr_widget.get_text()
551
        if len(addr) < 7:
552
            dialogs.ErrorDialog.create("The address is not valid\n\nEnter a valid address\n" +
553
                    "Example: 192.0.32.10 or example.org", parent=self.main_window)
554
            return
555
        # save it.
556
        _when_valid_save()
557

    
558
    # ---------------------------- Custom system tab buttons ---------------
559

    
560
    def on_network_admin_clicked(self, *args):
561
        """
562
        Opens the network-admin Gnome applet.
563
        """
564
        process.run_once("gksudo", "network-admin")
565

    
566
    def on_system_shutdown_clicked(self, *args):
567
        """
568
        Shuts down the computer.
569
        """
570
        def _on_confirm_result(result):
571
            if result:
572
                process.run_once("gksudo", "shutdown -h now")
573

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

    
577
    def on_system_reboot_clicked(self, *args):
578
        """
579
        Reboots the computer.
580
        """
581
        def _on_confirm_result(result):
582
            if result:
583
                process.run_once("gksudo", "shutdown -r now")
584

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

    
588
    def on_maintenance_apt_update_clicked(self, *args):
589
        """
590
        Opens APT update manager.
591
        """
592
        process.run_once("gksudo", "update-manager")
593

    
594
    def on_maintenance_send_info_clicked(self, *args):
595
        """
596
        Sends an email to SAT with this information : 
597
         * milhouse version
598
         * kernel version
599
         * Loaded kernel modules
600
        """
601
        # TODO: move this to an other file.
602
        def _on_confirm_result(result):
603
            milhouse_version = "unknown"
604
            if result:
605
                msg = "--- milhouse_version ---\n" + milhouse_version + "\n"
606
                msg += "--- uname -a ---\n"
607
                try:
608
                    w, r, err = os.popen3('uname -a')
609
                    msg += r.read() + "\n"
610
                    errRead = err.read()
611
                    if errRead:
612
                        msg += errRead + "\n"
613
                    w.close()
614
                    r.close()
615
                    err.close()
616
                except:
617
                    msg += "Error executing 'uname -a'\n"
618
                msg += "--- lsmod ---\n"
619
                try:
620
                    w, r, err = os.popen3('lsmod')
621
                    msg += r.read()
622
                    errRead = err.read()
623
                    if errRead:
624
                        msg += "\n" + errRead
625
                    w.close()
626
                    r.close()
627
                    err.close()
628
                except:
629
                    msg += "Error executing 'lsmod'"
630
                fromaddr = self.app.config.email_info
631
                toaddrs  = self.app.config.email_info
632
                toaddrs = toaddrs.split(', ')
633
                server = smtplib.SMTP(self.app.config.smtpserver)
634
                server.set_debuglevel(0)
635
                try:
636
                    server.sendmail(fromaddr, toaddrs, msg)
637
                except:
638
                    dialogs.ErrorDialog.create("Could not send info.\n\nCheck your internet connection.", parent=self.main_window)
639
                server.quit()
640
        
641
        text = _("<b><big>Send the settings?</big></b>\n\nAre you sure you want to send your computer settings to the administrator of scenic?")
642
        self.show_confirm_dialog(text, _on_confirm_result)
643

    
644
    # --------------------- configuration and widgets value ------------
645

    
646
    def _gather_configuration(self):
647
        """
648
        Updates the configuration with the value of each widget.
649
        """
650
        print("gathering configuration")
651
        # VIDEO SIZE:
652
        video_capture_size = _get_combobox_value(self.video_capture_size_widget)
653
        self.app.config.video_capture_size = video_capture_size
654
        print ' * video_capture_size:', self.app.config.video_capture_size
655
        # DISPLAY:
656
        video_display = _get_combobox_value(self.video_display_widget)
657
        print ' * video_display:', video_display
658
        self.app.config.video_display = self.app.config.video_display
659
        # VIDEO SOURCE AND DEVICE:
660
        video_source = _get_combobox_value(self.video_source_widget)
661
        if video_source == "Color bars":
662
            self.app.config.video_source = "videotestsrc"
663
        elif video_source.startswith("/dev/video"): # TODO: firewire!
664
            self.app.config.video_device = video_source
665
            self.app.config.video_source = "v4l2src"
666
        print ' * videosource:', self.app.config.video_source
667
        # VIDEO CODEC:
668
        video_codec = _get_combobox_value(self.video_codec_widget)
669
        self.app.config.video_codec = VIDEO_CODECS[video_codec]
670
        print ' * video_codec:', self.app.config.video_codec
671
        # VIDEO ASPECT RATIO:
672
        video_aspect_ratio = _get_combobox_value(self.aspect_ratio_widget)
673
        self.app.config.video_aspect_ratio = video_aspect_ratio
674
        print ' * video_aspect_ratio:', self.app.config.video_aspect_ratio
675
        #VIDEO FULLSCREEN
676
        video_fullscreen = self.video_fullscreen_widget.get_active()
677
        self.app.config.video_fullscreen = video_fullscreen
678
        print ' * video_fullscreen:', self.app.config.video_fullscreen
679
        #VIDEO DEINTERLACE
680
        video_deinterlace = self.video_deinterlace_widget.get_active()
681
        self.app.config.video_deinterlace = video_deinterlace
682
        print ' * video_deinterlace:', self.app.config.video_deinterlace
683
        # VIDEO JITTERBUFFER
684
        video_jitterbuffer = self.video_jitterbuffer_widget.get_value_as_int() # spinbutton
685
        self.app.config.video_jitterbuffer = video_jitterbuffer
686
        print ' * video_jitterbuffer:', self.app.config.video_jitterbuffer
687
        # VIDEO BITRATE
688
        video_bitrate = self.video_bitrate_widget.get_value() # spinbutton (float)
689
        self.app.config.video_bitrate = float(video_bitrate)
690
        print ' * video_bitrate:', self.app.config.video_bitrate
691
        
692
        # AUDIO:
693
        audio_source_readable = _get_combobox_value(self.audio_source_widget)
694
        audio_codec_readable = _get_combobox_value(self.audio_codec_widget)
695
        audio_numchannels = self.audio_numchannels_widget.get_value_as_int() # spinbutton
696
        print " * audio_source:", audio_source_readable
697
        print " * audio_codec:", audio_codec_readable
698
        print " * audio_numchannels:", audio_numchannels
699
        self.app.config.audio_source = AUDIO_SOURCES[audio_source_readable]
700
        self.app.config.audio_codec = AUDIO_CODECS[audio_codec_readable]
701
        # FIXME: the interface should already prevent this case from happening
702
        if audio_numchannels > 2 and self.app.config.audio_codec == "mp3":
703
            dialogs.ErrorDialog.create("Will receive 2 channels, since the MP3 codec allows a maximum of 2 channels.")
704
            audio_numchannels = 2
705
        self.app.config.audio_channels = audio_numchannels
706

    
707
    def update_widgets_with_saved_config(self):
708
        """
709
        Called once at startup.
710
         * Once the config file is read, and the devices have been polled
711
         * Sets the value of each widget according to the data stored in the configuration file.
712
        It could be called again, once another config file has been read.
713
        """
714
        print("Changing widgets value according to configuration.")
715
        # VIDEO CAPTURE SIZE:
716
        video_capture_size = self.app.config.video_capture_size
717
        _set_combobox_choices(self.video_capture_size_widget, ALL_SUPPORTED_SIZE)
718
        _set_combobox_value(self.video_capture_size_widget, video_capture_size)
719
        print ' * video_capture_size:', video_capture_size
720
        # DISPLAY:
721
        video_display = self.app.config.video_display
722
        _set_combobox_value(self.video_display_widget, video_display)
723
        print ' * video_display:', video_display
724
        # VIDEO SOURCE AND DEVICE:
725
        if self.app.config.video_source == "videotestsrc":
726
            video_source = "Color bars"
727
        elif self.app.config.video_source == "v4l2src":
728
            video_source = self.app.config.video_device
729
        _set_combobox_value(self.video_source_widget, video_source)
730
        print ' * videosource:', video_source
731
        # VIDEO CODEC:
732
        video_codec = _get_key_for_value(VIDEO_CODECS, self.app.config.video_codec)
733
        _set_combobox_value(self.video_codec_widget, video_codec)
734
        print ' * video_codec:', video_codec
735
        # VIDEO ASPECT RATIO:
736
        video_aspect_ratio = self.app.config.video_aspect_ratio
737
        _set_combobox_value(self.aspect_ratio_widget, video_aspect_ratio)
738
        print ' * video_aspect_ratio:', video_aspect_ratio
739
        # VIDEO FULLSCREEN:
740
        video_fullscreen = self.app.config.video_fullscreen
741
        self.video_fullscreen_widget.set_active(video_fullscreen)
742
        print ' * video_fullscreen:', video_fullscreen
743
        # VIDEO DEINTERLACE:
744
        video_deinterlace = self.app.config.video_deinterlace
745
        self.video_deinterlace_widget.set_active(video_deinterlace)
746
        print ' * video_deinterlace:', video_deinterlace
747
        # VIDEO JITTERBUFFER
748
        video_jitterbuffer = self.app.config.video_jitterbuffer
749
        self.video_jitterbuffer_widget.set_value(video_jitterbuffer) # spinbutton
750
        print ' * video_jitterbuffer:', video_jitterbuffer
751
        # VIDEO BITRATE
752
        video_bitrate = self.app.config.video_bitrate
753
        self.video_bitrate_widget.set_value(video_bitrate) # spinbutton
754
        print ' * video_bitrate:', video_bitrate
755
        
756
        # ADDRESSBOOK:
757
        # Init addressbook contact list:
758
        self.app.address_book.selected_contact = None
759
        self.app.address_book.current_contact_is_new = False
760
        if len(self.app.address_book.contact_list) > 0:
761
            for contact in self.app.address_book.contact_list:
762
                contact_markup = format_contact_markup(contact)
763
                self.contact_tree.append([contact_markup])
764
            self.selection.select_path(self.app.address_book.selected)
765
        else:
766
            self.edit_contact_widget.set_sensitive(False)
767
            self.remove_contact_widget.set_sensitive(False)
768
            self.invite_contact_widget.set_sensitive(False)
769
        # AUDIO:
770
        audio_source_readable = _get_key_for_value(AUDIO_SOURCES, self.app.config.audio_source)
771
        audio_codec = _get_key_for_value(AUDIO_CODECS, self.app.config.audio_codec)
772
        audio_numchannels = self.app.config.audio_channels
773
        print " * audio_source:", audio_source_readable
774
        print " * audio_codec:", audio_codec
775
        print " * audio_numchannels:", audio_numchannels
776
        self.audio_numchannels_widget.set_value(audio_numchannels) # spinbutton
777
        _set_combobox_value(self.audio_source_widget, audio_source_readable)
778
        _set_combobox_value(self.audio_codec_widget, audio_codec)
779

    
780
    def update_streaming_state(self):
781
        """
782
        Changes the sensitivity and state of many widgets according to if we are streaming or not.
783
        
784
        Makes most of the audio/video buttons and widgets sensitive or not.
785
        Changes the invite button:
786
         * the icon
787
         * the label
788
        Makes the contact list sensitive or not.
789
        """
790
        _widgets_to_toggle_sensitivity = [
791
            self.video_capture_size_widget,
792
            self.video_display_widget,
793
            self.video_source_widget,
794
            self.video_codec_widget,
795
            self.video_fullscreen_widget,
796
            self.video_view_preview_widget,
797
            self.video_deinterlace_widget,
798
            self.video_jitterbuffer_widget,
799
            self.aspect_ratio_widget,
800
            
801
            self.audio_source_widget,
802
            self.audio_codec_widget,
803
            self.audio_numchannels_widget,
804
            
805
            self.contact_list_widget,
806
            self.add_contact_widget,
807
            self.remove_contact_widget,
808
            self.edit_contact_widget,
809
            ]
810
        
811
        
812
        self.update_bitrate_and_codec()
813
        
814
        is_streaming = self.app.has_session()
815
        if is_streaming:
816
            details = self.app.streamer_manager.session_details
817
        currently_sensitive = self.contact_list_widget.get_property("sensitive")
818
        state_has_changed = is_streaming == currently_sensitive
819
        if state_has_changed:
820
            if is_streaming:
821
                text = _("Stop streaming")
822
                icon = gtk.STOCK_CONNECT
823
            else:
824
                text = _("Invite this contact")
825
                icon = gtk.STOCK_DISCONNECT
826
            self.invite_label_widget.set_text(text)
827
            self.invite_icon_widget.set_from_stock(icon, 4)
828
            
829
            # Toggle sensitivity of many widgets:
830
            print 'Got to change the sensitivity of many widgets to', not is_streaming
831
            for widget in _widgets_to_toggle_sensitivity:
832
                widget.set_sensitive(not is_streaming)
833
            # Update the summary: 
834
            # peer: --------------------------------
835
            if is_streaming:
836
                self.info_peer_widget.set_text(details["peer"]["name"])
837
            else:
838
                self.info_peer_widget.set_text("")
839

    
840
        # update the audio and video summary:(even if the state has not just changed)
841
        if is_streaming:
842
            # send video: --------------------------------
843
            _info_send_video = _("%(width)dx%(height)d %(codec)s") % {
844
                "width": details["send"]["video"]["width"], 
845
                "height": details["send"]["video"]["height"], 
846
                "codec": details["send"]["video"]["codec"], 
847
                }
848
            if details["send"]["video"]["bitrate"] is not None:
849
                 _info_send_video += " " + _("%(bitrate)2.2f Mbits/s") % {"bitrate": details["send"]["video"]["bitrate"]}
850
            _info_send_video += "\n"
851
            try:
852
                _video_packetloss = self.app.streamer_manager.rtcp_stats["send"]["video"]["packets-lost"] / float(self.app.streamer_manager.rtcp_stats["send"]["video"]["packets-sent"]) * 100
853
            except ZeroDivisionError:
854
                _video_packetloss = 0.0
855
            _info_send_video += _("Jitter: %(jitter)d ns. Packet lost: %(packetloss)2.2f%%.") % {# % is escaped with an other %
856
                "jitter": self.app.streamer_manager.rtcp_stats["send"]["video"]["jitter"],
857
                "packetloss": _video_packetloss
858
                #TODO: Bitrate: %(bitrate)f Mbps/s. 
859
                }
860
            print("info send video: " + _info_send_video)
861
            self.info_send_video_widget.set_text(_info_send_video)
862
            # send audio: --------------------------------
863
            _info_send_audio = _("%(numchannels)d-channel %(codec)s") % {
864
                "numchannels": details["send"]["audio"]["numchannels"], 
865
                "codec": details["send"]["audio"]["codec"] 
866
                }
867
            _info_send_audio += "\n"
868
            try:
869
                _audio_packetloss = self.app.streamer_manager.rtcp_stats["send"]["audio"]["packets-lost"] / float(self.app.streamer_manager.rtcp_stats["send"]["audio"]["packets-sent"]) * 100
870
            except ZeroDivisionError:
871
                _audio_packetloss = 0.0
872
            _info_send_audio += _("Jitter: %(jitter)d ns. Packet lost: %(packetloss)2.2f%%.") % { # % is escaped with an other %
873
                "jitter": self.app.streamer_manager.rtcp_stats["send"]["audio"]["jitter"],
874
                "packetloss": _audio_packetloss  
875
                #TODO: Bitrate: %(bitrate)f Mbps/s. 
876
                }
877
            print("info send audio: " + _info_send_audio)
878
            self.info_send_audio_widget.set_text(_info_send_audio)
879
            # recv video: --------------------------------
880
            _info_recv_video = _("%(width)dx%(height)d %(codec)s") % {
881
                "width": details["receive"]["video"]["width"], 
882
                "height": details["receive"]["video"]["height"], 
883
                "codec": details["receive"]["video"]["codec"], 
884
                }
885
            if details["receive"]["video"]["bitrate"] is not None:
886
                 _info_recv_video += " " + _("%(bitrate)2.2f Mbits/s") % {"bitrate": details["receive"]["video"]["bitrate"]}
887
            print("info recv video: " + _info_recv_video)
888
            self.info_receive_video_widget.set_text(_info_recv_video)
889
            # recv audio: --------------------------------
890
            self.info_receive_audio_widget.set_text(
891
                _("%(numchannels)d-channel %(codec)s") % {
892
                "numchannels": details["send"]["audio"]["numchannels"], 
893
                "codec": details["send"]["audio"]["codec"] 
894
                })
895
        else:
896
            self.info_send_video_widget.set_text("")
897
            self.info_send_audio_widget.set_text("")
898
            self.info_receive_video_widget.set_text("")
899
            self.info_receive_audio_widget.set_text("")
900
        
901

    
902
    def on_audio_codec_changed(self, widget):
903
        """
904
        Called when the user selects a different audio codec, updates
905
        the range of the numchannels box.
906
        """
907
        old_numchannels = self.audio_numchannels_widget.get_value()
908
        max_channels = None
909
        if _get_combobox_value(self.audio_codec_widget) == "MP3":
910
            max_channels = 2
911
        elif _get_combobox_value(self.audio_codec_widget) == "Raw":
912
            max_channels = 8
913
        elif _get_combobox_value(self.audio_codec_widget) == "Vorbis":
914
            max_channels = 24 
915
        # update range and clamp numchannels to new range 
916
        self.audio_numchannels_widget.set_range(1, max_channels)
917
        self.audio_numchannels_widget.set_value(min(old_numchannels, max_channels)) 
918

    
919
    def update_bitrate_and_codec(self):
920
        old_bitrate = self.video_bitrate_widget.get_value()
921
        codec = _get_combobox_value(self.video_codec_widget)
922
        is_streaming = self.app.has_session()
923
        if codec in VIDEO_BITRATE_MIN_MAX.keys():
924
            if is_streaming:
925
                self.video_bitrate_widget.set_sensitive(False)
926
            else:
927
                self.video_bitrate_widget.set_sensitive(True)
928
            mini = VIDEO_BITRATE_MIN_MAX[codec][0]
929
            maxi = VIDEO_BITRATE_MIN_MAX[codec][1]
930
            self.video_bitrate_widget.set_range(mini, maxi)
931
            self.video_bitrate_widget.set_value(min(maxi, max(old_bitrate, mini)))
932
        else:
933
            self.video_bitrate_widget.set_sensitive(False)
934
    
935
    def on_video_codec_changed(self, widget):
936
        self.update_bitrate_and_codec()
937
        
938
    def update_x11_devices(self):
939
        """
940
        Called once Application.poll_x11_devices has been run
941
        """
942
        x11_displays = [display["name"] for display in self.app.devices["x11_displays"]]
943
        print("Updating X11 displays with values %s" % (x11_displays))
944
        _set_combobox_choices(self.video_display_widget, x11_displays)
945

    
946
    def update_camera_devices(self):
947
        """
948
        Called once Application.poll_camera_devices has been run
949
        """
950
        self._video_source_changed_by_user = False
951
        cameras = self.app.devices["cameras"].keys()
952
        cameras.insert(0, VIDEO_TEST_INPUT)
953
        print("Updating video sources with values %s" % (cameras))
954
        _set_combobox_choices(self.video_source_widget, cameras)
955
        self.update_v4l2_inputs_size_and_norm()
956
        self._video_source_changed_by_user = True
957

    
958
    def update_v4l2_inputs_size_and_norm(self):
959
        """
960
        Called when : 
961
         * user chooses a different video source.
962
        If the selected is not a V4L2, disables the input and norm widgets.
963
        """
964
        value = _get_combobox_value(self.video_source_widget)
965
        self._v4l2_input_changed_by_user = False
966
        self._v4l2_standard_changed_by_user = False
967
        # change choices and value:
968
        if value == VIDEO_TEST_INPUT:
969
            # INPUTS:
970
            self.v4l2_input_widget.set_sensitive(False)
971
            self.v4l2_input_widget.set_active(-1)
972
            # STANDARD:
973
            self.v4l2_standard_widget.set_sensitive(False)
974
            self.v4l2_standard_widget.set_active(-1)
975
            # SIZE:
976
            _set_combobox_choices(self.video_capture_size_widget, ALL_SUPPORTED_SIZE)
977
        else:
978
            # INPUTS:
979
            current_camera_name = _get_combobox_value(self.video_source_widget)
980
            cam = self.app.devices["cameras"][current_camera_name]
981
            current_input = cam["input"]
982
            if current_input is not None: # check if device has many inputs
983
                self.v4l2_input_widget.set_sensitive(True)
984
                _set_combobox_choices(self.v4l2_input_widget, cam["inputs"])
985
                _set_combobox_value(self.v4l2_input_widget, cam["inputs"][current_input]) # which in turn calls on_v4l2_input_changed
986
            else:
987
                self.v4l2_input_widget.set_sensitive(False)
988
                self.v4l2_input_widget.set_active(-1)
989
                
990
            # STANDARD: 
991
            current_standard = cam["standard"]
992
            if current_standard is not None: # check if device supports different standards
993
                self.v4l2_standard_widget.set_sensitive(True)
994
                _set_combobox_choices(self.v4l2_standard_widget, VIDEO_STANDARDS)
995
                _set_combobox_value(self.v4l2_standard_widget, cam["standard"]) # which in turn calls on_v4l2_standard_changed
996
            else:
997
                self.v4l2_standard_widget.set_sensitive(False)
998
                self.v4l2_standard_widget.set_active(-1)
999
            #self.v4l2_standard_widget.set_sensitive(True)
1000
            # SIZE:
1001
            print "supported sizes: ", cam["supported_sizes"]
1002
            _set_combobox_choices(self.video_capture_size_widget, cam["supported_sizes"]) # TODO: more test sizes
1003
        # once done:
1004
        self._v4l2_input_changed_by_user = True
1005
        self._v4l2_standard_changed_by_user = True
1006
            
1007
    def on_video_source_changed(self, widget):
1008
        """
1009
        Called when the user changes the video source.
1010
         * updates the input
1011
        """
1012
        if self._video_source_changed_by_user:
1013
            current_camera_name = _get_combobox_value(self.video_source_widget)
1014
            if current_camera_name != VIDEO_TEST_INPUT:
1015
                self.app.poll_camera_devices()
1016
            self.update_v4l2_inputs_size_and_norm()
1017

    
1018
    def on_v4l2_standard_changed(self, widget):
1019
        """
1020
        When the user changes the V4L2 standard, we actually change this standard using milhouse.
1021
        Calls `milhouse --videodevice /dev/videoX --v4l2-standard XXX
1022
        Values are either NTSC or PAL.
1023
        """
1024
        if self._v4l2_standard_changed_by_user:
1025
            # change standard for device
1026
            current_camera_name = _get_combobox_value(self.video_source_widget)
1027
            if current_camera_name != VIDEO_TEST_INPUT:
1028
                standard_name = _get_combobox_value(widget)
1029
                cam = self.app.devices["cameras"][current_camera_name]
1030
                d = cameras.set_v4l2_video_standard(device_name=current_camera_name, standard=standard_name)
1031
                def _cb2(cameras):
1032
                    try:
1033
                        cam = cameras[current_camera_name]
1034
                    except KeyError, e:
1035
                        print("Camera %s disappeared !" % (current_camera_name))
1036
                    else:
1037
                        actual_standard = cam["standard"]
1038
                        if actual_standard != standard_name:
1039
                            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}
1040
                            print(msg)
1041
                            # Maybe we should show an error dialog in that case, or set the value to what it really is.
1042
                        else:
1043
                            print("Successfully changed standard to %s for device %s." % (actual_standard, current_camera_name))
1044
                def _cb(result):
1045
                    d2 = cameras.list_cameras()
1046
                    d2.addCallback(_cb2)
1047
                d.addCallback(_cb)
1048
        
1049
    def on_v4l2_input_changed(self, widget):
1050
        """
1051
        When the user changes the V4L2 input, we actually change this input using milhouse.
1052
        Calls `milhouse --videodevice /dev/videoX --v4l2-input N
1053
        """
1054
        if self._v4l2_input_changed_by_user:
1055
            # change input for device
1056
            current_camera_name = _get_combobox_value(self.video_source_widget)
1057
            if current_camera_name != VIDEO_TEST_INPUT:
1058
                input_name = _get_combobox_value(widget)
1059
                cam = self.app.devices["cameras"][current_camera_name]
1060
                input_number = cam["inputs"].index(input_name)
1061
                d = cameras.set_v4l2_input_number(device_name=current_camera_name, input_number=input_number)
1062
                def _cb2(cameras):
1063
                    try:
1064
                        cam = cameras[current_camera_name]
1065
                    except KeyError, e:
1066
                        print("Camera %s disappeared !" % (current_camera_name))
1067
                    else:
1068
                        actual_input = cam["input"]
1069
                        if actual_input != input_number:
1070
                            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": input_number, "device_name": current_camera_name}
1071
                            print(msg)
1072
                            # Maybe we should show an error dialog in that case, or set the value to what it really is.
1073
                        else:
1074
                            print("Successfully changed input to %s for device %s." % (actual_input, current_camera_name))
1075
                def _cb(result):
1076
                    d2 = cameras.list_cameras()
1077
                    d2.addCallback(_cb2)
1078
                d.addCallback(_cb)
1079

    
1080
    # -------------------------- menu items -----------------
1081
    
1082
    def on_about_menu_item_activate(self, menu_item):
1083
        About.create() # TODO: set parent window ?
1084
    
1085
    def on_quit_menu_item_activated(self, menu_item):
1086
        """
1087
        Quits the application.
1088
        """
1089
        print menu_item, "chosen"
1090
        self._confirm_and_quit()
1091
    
1092
    def on_help_menu_item_activated(self, menu_item):
1093
        """
1094
        Opens a web browser to the scenic web site.
1095
        """
1096
        print menu_item, "chosen"
1097
        url = ONLINE_HELP_URL 
1098
        webbrowser.open(url)
1099

    
1100
    # ---------------------- invitation dialogs -------------------
1101

    
1102
    def on_invite_contact_clicked(self, *args):
1103
        """
1104
        Sends an INVITE to the remote peer.
1105
        """
1106
        if self.app.has_session():
1107
            self.app.stop_streamers()
1108
        else:
1109
            self.app.send_invite()
1110
    
1111
    def on_invite_contact_cancelled(self, *args):
1112
        """
1113
        Sends a CANCEL to the remote peer when invite contact window is closed.
1114
        """
1115
        # unschedule this timeout as we don't care if our peer answered or not
1116
        self._unschedule_offerer_invite_timeout()
1117
        self.app.send_cancel_and_disconnect()
1118
        # don't let the delete-event propagate
1119
        if self.calling_dialog.get_property('visible'):
1120
            self.calling_dialog.hide()
1121
        return True
1122

    
1123
    def show_confirm_dialog(self, text, callback=None):
1124
        """
1125
        Shows a confirm dialog, the old way.
1126
        """
1127
        # TODO: deprecate
1128
        def _response_cb(widget, response_id, callback):
1129
            widget.hide()
1130
            if callback is not None:
1131
                callback(response_id == gtk.RESPONSE_OK)
1132
            widget.disconnect(slot1)
1133

    
1134
        self.confirm_label.set_label(text)
1135
        dialog = self.confirm_dialog
1136
        dialog.set_modal(True)
1137
        slot1 = dialog.connect('response', _response_cb, callback)
1138
        dialog.show()
1139

    
1140
    def show_invited_dialog(self, text, callback=None):
1141
        """ 
1142
        We disconnect and reconnect the callbacks every time
1143
        this is called, otherwise we'd would have multiple 
1144
        callback invokations per response since the widget 
1145
        stays alive 
1146
        """
1147
        def _response_cb(widget, response_id, callback):
1148
            widget.hide()
1149
            if callback is not None:
1150
                callback(response_id)
1151
            widget.disconnect(slot1)
1152

    
1153
        self.invited_dialog_label_widget.set_label(text)
1154
        dialog = self.invited_dialog
1155
        dialog.set_modal(True)
1156
        slot1 = dialog.connect('response', _response_cb, callback)
1157
        dialog.show()
1158

    
1159
    def hide_calling_dialog(self, msg="", err=""):
1160
        """
1161
        Hides the "calling_dialog" dialog.
1162
        Shows an error dialog if the argument msg is set to "err", "timeout", "answTimeout", "send", "refuse" or "badAnsw".
1163
        """
1164
        self.calling_dialog.hide()
1165
        text = None
1166
        if msg == "err":
1167
            text = _("Contact unreacheable.\n\nCould not connect to the IP address of this contact.")
1168
        elif msg == "answTimeout":
1169
            text = _("Contact answer timeout.\n\nThe contact did not answer soon enough.")
1170
        elif msg == "send":
1171
            text = _("Problem sending command.\n\nError: %s") % err
1172
        elif msg == "refuse":
1173
            text = _("Connection refused.\n\nThe contact refused the connection.")
1174
        elif msg == "badAnsw":
1175
            text = _("Invalid answer.\n\nThe answer was not valid.")
1176
        if text is not None:
1177
            dialogs.ErrorDialog.create(text, parent=self.main_window)
1178

    
1179
    def _unschedule_offerer_invite_timeout(self):
1180
        """ Unschedules our offer invite timeout function """
1181
        if self._offerer_invite_timeout is not None and self._offerer_invite_timeout.active():
1182
            self._offerer_invite_timeout.cancel()
1183
            self._offerer_invite_timeout = None
1184
    
1185
    def _schedule_offerer_invite_timeout(self):
1186
        """ Schedules our offer invite timeout function """
1187
        def _cl_offerer_invite_timed_out():
1188
            # XXX
1189
            # in case of invite timeout, act as if we'd cancelled the invite ourselves
1190
            self.on_invite_contact_cancelled()
1191
            self.hide_calling_dialog("answTimeout")
1192
            # here we return false so that this callback is unregistered
1193
            return False
1194

    
1195
        if self._offerer_invite_timeout is None or not self._offerer_invite_timeout.active():
1196
            self._offerer_invite_timeout = reactor.callLater(INVITE_TIMEOUT, _cl_offerer_invite_timed_out)
1197
        else:
1198
            print("Warning: Already scheduled a timeout as we're already inviting a contact")
1199

    
1200
    def update_jackd_status(self):
1201
        is_zombie = self.app.devices["jackd_is_zombie"]
1202
        is_running = self.app.devices["jackd_is_running"]
1203
        fill_stats = False
1204
        if is_zombie:
1205
                self.audio_jack_state_widget.set_markup(_("<b>Zombie</b>"))
1206
                self.audio_jack_icon_widget.set_from_stock(gtk.STOCK_DIALOG_WARNING, 4)
1207
        else:
1208
            if is_running:
1209
                self.audio_jack_state_widget.set_markup(_("<b>Running</b>"))
1210
                self.audio_jack_icon_widget.set_from_stock(gtk.STOCK_YES, 4)
1211
                fill_stats = True
1212
            else:
1213
                self.audio_jack_state_widget.set_markup(_("<b>Not running</b>"))
1214
                self.audio_jack_icon_widget.set_from_stock(gtk.STOCK_NO, 4)
1215
        if fill_stats:
1216
            j = self.app.devices["jack_servers"][0] 
1217
            latency = (j["period"] * j["nperiods"] / float(j["rate"])) * 1000 # ms
1218
            self.jack_latency_widget.set_text("%4.2f ms" % (latency))
1219
            self.jack_sampling_rate_widget.set_text("%d Hz" % (j["rate"]))
1220
        else:
1221
            self.jack_latency_widget.set_text("")
1222
            self.jack_sampling_rate_widget.set_text("")
1223
            
1224
PROJECT_WEBSITE = "http://svn.sat.qc.ca/trac/scenic"
1225

    
1226
class About(object):
1227
    """
1228
    About dialog
1229
    """
1230
    def __init__(self):
1231
        # TODO: set parent window ?
1232
        self.icon_file = os.path.join(configure.PIXMAPS_DIR, 'scenic.png')
1233
        self.about_dialog = gtk.AboutDialog()
1234

    
1235
    def show_about_dialog(self):
1236
        self.about_dialog.set_name(configure.APPNAME)
1237
        self.about_dialog.set_role('about')
1238
        self.about_dialog.set_version(configure.VERSION)
1239
        commentlabel = ONE_LINE_DESCRIPTION 
1240
        self.about_dialog.set_comments(commentlabel)
1241
        self.about_dialog.set_copyright(COPYRIGHT_SHORT) 
1242
        self.about_dialog.set_license(LICENSE_TEXT)
1243
        self.about_dialog.set_authors(AUTHORS_LIST)
1244
        #self.about_dialog.set_artists(['Public domain'])
1245
        gtk.about_dialog_set_url_hook(self.show_website)
1246
        self.about_dialog.set_website(PROJECT_WEBSITE)
1247
        if not os.path.exists(self.icon_file):
1248
            print("Could not find icon file %s." % (self.icon_file))
1249
        else:
1250
            large_icon = gtk.gdk.pixbuf_new_from_file(self.icon_file)
1251
            self.about_dialog.set_logo(large_icon)
1252
        # Connect to callbacks
1253
        self.about_dialog.connect('response', self.destroy_about)
1254
        self.about_dialog.connect('delete_event', self.destroy_about)
1255
        self.about_dialog.connect("delete-event", self.destroy_about)
1256
        self.about_dialog.show_all()
1257

    
1258
    @staticmethod
1259
    def create():
1260
        """
1261
        @rettype: None
1262
        """
1263
        dialog = About()
1264
        return dialog.show_about_dialog()
1265
     
1266
    def show_website(self, widget, data):
1267
        webbrowser.open(data)
1268

    
1269
    def destroy_about(self, *args):
1270
        self.about_dialog.destroy()