This URL has Read-Only access.

Statistics
| Branch: | Tag: | Revision:

root / py / scenic / gui.py @ b58746c2

History | View | Annotate | Download (65.4 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

    
26
import sys
27
import os
28
import smtplib
29
import gtk
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.internationalization import _
46

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

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

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

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

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

    
80
PROJECT_WEBSITE = "http://svn.sat.qc.ca/trac/scenic"
81

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

    
89
COPYRIGHT_SHORT = _("Copyright 2009-2010 Society for Arts and Technology")
90

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

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

    
113
def _set_combobox_choices(widget, choices=[]):
114
    """
115
    Sets the choices in a GTK combobox.
116
    """
117
    #XXX: combo boxes in the glade file must have a space as a value to have a tree iter
118
    #TODO When we change a widget value, its changed callback is called...
119
    previous_value = _get_combobox_value(widget)
120
    tree_model = gtk.ListStore(str)
121
    for choice in choices:
122
        tree_model.append([choice])
123
    widget.set_model(tree_model)
124
    _set_combobox_value(widget, previous_value)
125

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

    
148
#videotestsrc legible name:
149
VIDEO_TEST_INPUT = "Color bars"
150

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

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

    
188

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

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

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

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

    
287
        self.jack_latency_widget = widgets_tree.get_widget("jack_latency")
288
        self.jack_sampling_rate_widget = widgets_tree.get_widget("jack_sampling_rate")
289
        # system tab contents:
290
        self.network_admin_widget = widgets_tree.get_widget("network_admin")
291

    
292
        # MIDI tab
293
        self.midi_send_enabled_widget = widgets_tree.get_widget("midi_send_enabled")
294
        self.midi_recv_enabled_widget = widgets_tree.get_widget("midi_recv_enabled")
295
        self.midi_input_device_widget = widgets_tree.get_widget("midi_input_device")
296
        self.midi_output_device_widget = widgets_tree.get_widget("midi_output_device")
297
        self.midi_jitterbuffer_widget = widgets_tree.get_widget("midi_jitterbuffer")
298

    
299
        # switch to Kiosk mode if asked
300
        if self.kiosk_mode_on:
301
            self.main_window.set_decorated(False)
302
        else:
303
            # Removes the sytem_tab 
304
            tab_num = self.main_tabs_widget.page_num(self.system_tab_contents_widget)
305
            print "Removing tab number %d." % (tab_num)
306
            self.main_tabs_widget.remove_page(tab_num)
307
        
308
        self.is_fullscreen = False
309
        if fullscreen:
310
            print("Making the main window fullscreen.")
311
            self.toggle_fullscreen()
312
        
313
        # ------------------ contact list view
314
        self.selection = self.contact_list_widget.get_selection()
315
        self.selection.connect("changed", self.on_contact_list_changed, None) 
316
        self.contact_tree = gtk.ListStore(str)
317
        self.contact_list_widget.set_model(self.contact_tree)
318
        column = gtk.TreeViewColumn(_("Contacts"), gtk.CellRendererText(), markup=False)
319
        self.contact_list_widget.append_column(column)
320
        # TODO: those state variables interactive/not could be merged into a single one
321
        self._v4l2_input_changed_by_user = True # if False, the software is changing those drop-down values itself.
322
        self._v4l2_standard_changed_by_user = True
323
        self._video_source_changed_by_user = True
324
        self._video_view_preview_toggled_by_user = True
325
        # preview:
326
        self.preview_manager = preview.Preview(self.app)
327
        self.video_preview_icon_widget = widgets_tree.get_widget("video_preview_icon")
328
        self.preview_manager.state_changed_signal.connect(self.on_preview_manager_state_changed)
329
        self.main_window.show()
330
        
331
        # recurring calls:
332
        self._streaming_state_check_task = task.LoopingCall(self.update_streaming_state)
333
        self._streaming_state_check_task.start(1.0, now=False)
334
        self._update_id_task = task.LoopingCall(self.update_local_ip)
335
        def _start_update_id():
336
            self._update_id_task.start(10.0, now=True)
337
        reactor.callLater(0, _start_update_id)
338
        # The main app must call init_widgets_value
339
   
340
    #TODO: for the preview in the drawing area   
341
    #def on_expose_event(self, widget, event):
342
    #    self.preview_xid = widget.window.xid
343
    #    return False
344

    
345
    # ------------------ window events and actions --------------------
346
    def toggle_fullscreen(self):
347
        """
348
        Toggles the fullscreen mode on/off.
349
        """
350
        if self.is_fullscreen:
351
            self.main_window.unfullscreen()
352
        else:
353
            self.main_window.fullscreen()
354

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

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

    
398
    def on_preview_area_realize(self, *args):
399
        # avoid bad xid errors
400
        gtk.gdk.display_get_default().sync()
401
        xid = self.preview_area_widget.window.xid
402
        print("Preview area X Window ID: %s" % (xid))
403
        self.preview_area_x_window_id = xid
404
        self.preview_area_widget.window.set_background(gtk.gdk.Color(0, 0, 0)) # black
405

    
406
    def on_preview_manager_state_changed(self, manager, new_state):
407
        if new_state == process.STATE_STOPPED:
408
            print("Making the preview button to False since the preview process died.")
409
            self._video_view_preview_toggled_by_user = False
410
            self.video_view_preview_widget.set_active(False)
411
            self._video_view_preview_toggled_by_user = True
412
            if self.preview_manager.is_busy():
413
                self.preview_manager.stop()
414
            self.video_preview_icon_widget.set_from_stock(gtk.STOCK_MEDIA_PLAY, 4)
415
        elif new_state == process.STATE_STARTING:
416
            self.video_preview_icon_widget.set_from_stock(gtk.STOCK_MEDIA_STOP, 4)
417

    
418
    def close_preview_if_running(self):
419
        """
420
        @rettype: L{Deferred}
421
        """
422
        def _cl(deferred):
423
            if self.preview_manager.is_busy():
424
                reactor.callLater(0.01, _cl, deferred)
425
            else:
426
                deferred.callback(None)
427
        if self.preview_manager.is_busy():
428
            self.preview_manager.stop()
429
            deferred = defer.Deferred()
430
            reactor.callLater(0.01, _cl, deferred)
431
            return deferred
432
        else:
433
            return defer.succeed(None)
434
        
435
    def on_video_view_preview_toggled(self, widget):
436
        """
437
        Shows a preview of the video input.
438
        """
439
        #TODO: create a new process protocol for the preview window
440
        #TODO: stop it when starting to stream, if running
441
        #TODO: stop it when button is toggled to false.
442
        # It can be the user that pushed the button, or it can be toggled by the software.
443
        print 'video_view_preview toggled', widget.get_active()
444
        if self._video_view_preview_toggled_by_user:
445
            if widget.get_active():
446
                self.app.save_configuration() #gathers and saves
447
                self.preview_manager.start()
448
            else:
449
                self.preview_manager.stop()
450

    
451
    def on_main_tabs_switch_page(self, widget, notebook_page, page_number):
452
        """
453
        Called when the user switches to a different page.
454
        Pages names are : 
455
         * contacts_tab_contents
456
         * video_tab_contents
457
         * audio_tab_contents
458
         * system_tab_contents
459
         * about_tab_contents
460
        """
461
        tab_widget = widget.get_nth_page(page_number)
462
        tab_name = tab_widget.get_name()
463
        if tab_name == "contacts_tab_contents":
464
            self.invite_contact_widget.grab_default()
465
        elif tab_name == "video_tab_contents":
466
            self.app.poll_x11_devices()
467
            self.app.poll_camera_devices()
468
        elif tab_name == "audio_tab_contents":
469
            pass
470
        elif tab_name == "system_tab_contents":
471
            self.network_admin_widget.grab_default()
472
        elif tab_name == "midi_tab_contents":
473
            self.app.poll_midi_devices()
474

    
475
    def on_contact_list_changed(self, *args):
476
        # FIXME: what is args?
477
        tree_list, self.selected_contact_row = args[0].get_selected()
478
        if self.selected_contact_row:
479
            # make the edit, remove, invite buttons sensitive:
480
            self.edit_contact_widget.set_sensitive(True)
481
            self.remove_contact_widget.set_sensitive(True)
482
            self.invite_contact_widget.set_sensitive(True)
483
            # get selected contact
484
            self.selected_contact_index = tree_list.get_path(self.selected_contact_row)[0] # FIXME: this var should be deprecated
485
            self.app.address_book.selected_contact = self.app.address_book.contact_list[self.selected_contact_index] # FIXME: deprecate this!
486
            self.app.address_book.selected = self.selected_contact_index
487
        else:
488
            # make the edit, remove, invite buttons sensitive:
489
            self.edit_contact_widget.set_sensitive(False)
490
            self.remove_contact_widget.set_sensitive(False)
491
            self.invite_contact_widget.set_sensitive(False)
492
            # no contact is selected
493
            self.app.address_book.selected_contact = None
494

    
495
    # ---------------------- slots for addressbook widgets events --------
496
    
497
    def on_contact_double_clicked(self, *args):
498
        """
499
        When a contact in the list is double-clicked, 
500
        shows the edit contact dialog.
501
        """
502
        self.on_edit_contact_clicked(args)
503

    
504
    def on_add_contact_clicked(self, *args):
505
        """
506
        Pops up a dialog to be filled with new contact infos.
507
        
508
        The add_contact buttons has been clicked.
509
        """
510
        self.app.address_book.current_contact_is_new = True
511
        # Update the text in the edit/new contact dialog:
512
        self.contact_name_widget.set_text("")
513
        self.contact_addr_widget.set_text("")
514
        self.contact_auto_accept_widget.set_active(False)
515
        self.edit_contact_window.show()
516

    
517
    def on_remove_contact_clicked(self, *args):
518
        """
519
        Upon confirmation, the selected contact is removed.
520
        """
521
        def _on_confirm_result(result):
522
            if result:
523
                del self.app.address_book.contact_list[self.selected_contact_index]
524
                self.contact_tree.remove(self.selected_contact_row)
525
                num = self.selected_contact_index - 1
526
                if num < 0:
527
                    num = 0
528
                self.selection.select_path(num)
529
        text = _("<b><big>Delete this contact from the list?</big></b>\n\nAre you sure you want "
530
            "to delete this contact from the list?")
531
        self.show_confirm_dialog(text, _on_confirm_result)
532

    
533
    def on_edit_contact_clicked(self, *args):
534
        """
535
        Shows the edit contact dialog.
536
        """
537
        contact = self.app.address_book.selected_contact
538
        self.contact_name_widget.set_text(contact["name"])
539
        self.contact_addr_widget.set_text(contact["address"])
540
        auto_accept = False
541
        if contact["auto_accept"]:
542
            auto_accept = True
543
            print('auto accept should be true')
544
        self.contact_auto_accept_widget.set_active(auto_accept)
545
        self.edit_contact_window.show() # addr
546

    
547
    def on_edit_contact_cancel_clicked(self, *args):
548
        """
549
        The cancel button in the "edit_contact" window has been clicked.
550
        Hides the edit_contact window.
551
        """
552
        self.edit_contact_window.hide()
553

    
554
    def on_edit_contact_save_clicked(self, *args):
555
        """
556
        The save button in the "edit_contact" window has been clicked.
557
        Hides the edit_contact window and saves the changes. (new or modified contact)
558
        """
559
        def _when_valid_save():
560
            # Saves contact info after it's been validated and then closes the window
561
            # THIS IS WHERE WE CREATE THE CONTACTS IN THE ADDRESSBOOK
562
            # TODO: move to a dedicated function in save.py or so.
563
            contact = {
564
                "name": self.contact_name_widget.get_text(),
565
                "address": addr,
566
                "auto_accept": self.contact_auto_accept_widget.get_active(),
567
                }
568
            contact_markup = format_contact_markup(contact)
569
            if self.app.address_book.current_contact_is_new:
570
                self.contact_tree.append([contact_markup]) # add it to the tree list
571
                self.app.address_book.contact_list.append(contact) # and the internal address book
572
                self.selection.select_path(len(self.app.address_book.contact_list) - 1) # select it ...?
573
                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
574
                self.app.address_book.current_contact_is_new = False # FIXME: what does that mean?
575
            else:
576
                self.contact_tree.set_value(self.selected_contact_row, 0, contact_markup)
577
                self.app.address_book.contact_list[self.selected_contact_index] = contact # FIXME: this is flaky. make some functions to handle this
578
            self.app.address_book.selected_contact = contact
579
            self.edit_contact_window.hide()
580

    
581
        # Validate the address
582
        addr = self.contact_addr_widget.get_text()
583
        if not network.validate_address(addr):
584
            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)
585
            return
586
        # save it.
587
        _when_valid_save()
588

    
589
    # ---------------------------- Custom system tab buttons ---------------
590

    
591
    def on_network_admin_clicked(self, *args):
592
        """
593
        Opens the network-admin Gnome applet.
594
        """
595
        process.run_once("gksudo", "network-admin")
596

    
597
    def on_system_shutdown_clicked(self, *args):
598
        """
599
        Shuts down the computer.
600
        """
601
        def _on_confirm_result(result):
602
            if result:
603
                process.run_once("gksudo", "shutdown -h now")
604

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

    
608
    def on_system_reboot_clicked(self, *args):
609
        """
610
        Reboots the computer.
611
        """
612
        def _on_confirm_result(result):
613
            if result:
614
                process.run_once("gksudo", "shutdown -r now")
615

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

    
619
    def on_maintenance_apt_update_clicked(self, *args):
620
        """
621
        Opens APT update manager.
622
        """
623
        process.run_once("gksudo", "update-manager")
624

    
625
    def on_maintenance_send_info_clicked(self, *args):
626
        """
627
        Sends an email to SAT with this information : 
628
         * milhouse version
629
         * kernel version
630
         * Loaded kernel modules
631
        """
632
        # TODO: move this to an other file.
633
        def _on_confirm_result(result):
634
            milhouse_version = "unknown"
635
            if result:
636
                msg = "--- milhouse_version ---\n" + milhouse_version + "\n"
637
                msg += "--- uname -a ---\n"
638
                try:
639
                    w, r, err = os.popen3('uname -a')
640
                    msg += r.read() + "\n"
641
                    errRead = err.read()
642
                    if errRead:
643
                        msg += errRead + "\n"
644
                    w.close()
645
                    r.close()
646
                    err.close()
647
                except:
648
                    msg += "Error executing 'uname -a'\n"
649
                msg += "--- lsmod ---\n"
650
                try:
651
                    w, r, err = os.popen3('lsmod')
652
                    msg += r.read()
653
                    errRead = err.read()
654
                    if errRead:
655
                        msg += "\n" + errRead
656
                    w.close()
657
                    r.close()
658
                    err.close()
659
                except:
660
                    msg += "Error executing 'lsmod'"
661
                fromaddr = self.app.config.email_info
662
                toaddrs  = self.app.config.email_info
663
                toaddrs = toaddrs.split(', ')
664
                server = smtplib.SMTP(self.app.config.smtpserver)
665
                server.set_debuglevel(0)
666
                try:
667
                    server.sendmail(fromaddr, toaddrs, msg)
668
                except:
669
                    dialogs.ErrorDialog.create(_("Could not send info.\nCheck your internet connection."), parent=self.main_window)
670
                server.quit()
671
        
672
        text = _("<b><big>Send the settings?</big></b>\n\nAre you sure you want to send your computer settings to the administrator of scenic?")
673
        self.show_confirm_dialog(text, _on_confirm_result)
674

    
675
    # --------------------- configuration and widgets value ------------
676

    
677
    def _gather_configuration(self):
678
        """
679
        Updates the configuration with the value of each widget.
680
        """
681
        print("Gathering configuration from the GUI widgets.")
682
        # VIDEO SIZE:
683
        video_capture_size = _get_combobox_value(self.video_capture_size_widget)
684
        self.app.config.video_capture_size = video_capture_size
685
        print ' * video_capture_size:', self.app.config.video_capture_size
686
        # DISPLAY:
687
        video_display = _get_combobox_value(self.video_display_widget)
688
        self.app.config.video_display = video_display
689
        print ' * video_display:', self.app.config.video_display
690
        # VIDEO SOURCE AND DEVICE:
691
        video_source = _get_combobox_value(self.video_source_widget)
692
        if video_source == "Color bars":
693
            self.app.config.video_source = "videotestsrc"
694
        elif video_source.startswith("/dev/video"): # TODO: firewire!
695
            self.app.config.video_device = video_source # this is subtle
696
            self.app.config.video_source = "v4l2src"
697
        print ' * videosource:', self.app.config.video_source
698
        # VIDEO CODEC:
699
        video_codec = _get_combobox_value(self.video_codec_widget)
700
        self.app.config.video_codec = VIDEO_CODECS[video_codec]
701
        print ' * video_codec:', self.app.config.video_codec
702
        # VIDEO ASPECT RATIO:
703
        video_aspect_ratio = _get_combobox_value(self.aspect_ratio_widget)
704
        self.app.config.video_aspect_ratio = video_aspect_ratio
705
        print ' * video_aspect_ratio:', self.app.config.video_aspect_ratio
706
        #VIDEO FULLSCREEN
707
        video_fullscreen = self.video_fullscreen_widget.get_active()
708
        self.app.config.video_fullscreen = video_fullscreen
709
        print ' * video_fullscreen:', self.app.config.video_fullscreen
710
        #VIDEO DEINTERLACE
711
        video_deinterlace = self.video_deinterlace_widget.get_active()
712
        self.app.config.video_deinterlace = video_deinterlace
713
        print ' * video_deinterlace:', self.app.config.video_deinterlace
714
        # VIDEO JITTERBUFFER
715
        video_jitterbuffer = self.video_jitterbuffer_widget.get_value_as_int() # spinbutton
716
        self.app.config.video_jitterbuffer = video_jitterbuffer
717
        print ' * video_jitterbuffer:', self.app.config.video_jitterbuffer
718
        # VIDEO BITRATE
719
        video_bitrate = self.video_bitrate_widget.get_value() # spinbutton (float)
720
        self.app.config.video_bitrate = float(video_bitrate)
721
        print ' * video_bitrate:', self.app.config.video_bitrate
722
        # VIDEO PREVIEW
723
        preview_in_window = self.preview_in_window_widget.get_active()
724
        self.app.config.preview_in_window = preview_in_window
725
        print " * preview_in_window: ", preview_in_window
726
        
727
        # AUDIO:
728
        audio_source_readable = _get_combobox_value(self.audio_source_widget)
729
        audio_codec_readable = _get_combobox_value(self.audio_codec_widget)
730
        audio_numchannels = self.audio_numchannels_widget.get_value_as_int() # spinbutton
731
        print " * audio_source:", audio_source_readable
732
        print " * audio_codec:", audio_codec_readable
733
        print " * audio_numchannels:", audio_numchannels
734
        self.app.config.audio_source = AUDIO_SOURCES[audio_source_readable]
735
        self.app.config.audio_codec = AUDIO_CODECS[audio_codec_readable]
736
        # FIXME: the interface should already prevent this case from happening
737
        if audio_numchannels > 2 and self.app.config.audio_codec == "mp3":
738
            print("Will receive 2 channels, since the MP3 codec allows a maximum of 2 channels.")
739
            print("This should have been prevented by the widgets logic iteself. Not likely to occur.")
740
            dialogs.ErrorDialog.create(_("Will receive 2 channels, since the MP3 codec allows a maximum of 2 channels."))
741
            audio_numchannels = 2
742
        self.app.config.audio_channels = audio_numchannels
743
        print " * audio_numchannels", self.app.config.audio_channels
744
        
745
        # MIDI:
746
        midi_send_enabled = self.midi_send_enabled_widget.get_active()
747
        midi_recv_enabled = self.midi_recv_enabled_widget.get_active()
748
        midi_input = _get_combobox_value(self.midi_input_device_widget)
749
        midi_output = _get_combobox_value(self.midi_output_device_widget)
750
        midi_jitterbuffer = self.midi_jitterbuffer_widget.get_value_as_int() 
751
        print " * midi_send_enabled:", midi_send_enabled
752
        print " * midi_recv_enabled:", midi_recv_enabled
753
        print " * midi_input_device:", midi_input
754
        print " * midi_output_device:", midi_output
755
        print " * midi_jitterbuffer:", midi_jitterbuffer
756
        self.app.config.midi_send_enabled = midi_send_enabled
757
        self.app.config.midi_recv_enabled = midi_recv_enabled
758
        self.app.config.midi_input_device = midi_input
759
        self.app.config.midi_output_device = midi_output
760
        self.app.config.midi_jitterbuffer = midi_jitterbuffer
761

    
762
    def update_widgets_with_saved_config(self):
763
        """
764
        Called once at startup.
765
         * Once the config file is read, and the devices have been polled
766
         * Sets the value of each widget according to the data stored in the configuration file.
767
        It could be called again, once another config file has been read.
768
        """
769
        self._widgets_changed_by_user = False
770
        print("Changing widgets value according to configuration.")
771
        print(self.app.config.__dict__)
772
        # VIDEO CAPTURE SIZE:
773
        video_capture_size = self.app.config.video_capture_size
774
        _set_combobox_choices(self.video_capture_size_widget, ALL_SUPPORTED_SIZE)
775
        _set_combobox_value(self.video_capture_size_widget, video_capture_size)
776
        print ' * video_capture_size:', video_capture_size
777
        # DISPLAY:
778
        video_display = self.app.config.video_display
779
        _set_combobox_value(self.video_display_widget, video_display)
780
        print ' * video_display:', video_display
781
        # VIDEO SOURCE AND DEVICE:
782
        if self.app.config.video_source == "videotestsrc":
783
            video_source = "Color bars"
784
        elif self.app.config.video_source == "v4l2src":
785
            video_source = self.app.config.video_device
786
        _set_combobox_value(self.video_source_widget, video_source)
787
        print ' * videosource:', video_source
788
        # VIDEO CODEC:
789
        video_codec = _get_key_for_value(VIDEO_CODECS, self.app.config.video_codec)
790
        _set_combobox_value(self.video_codec_widget, video_codec)
791
        print ' * video_codec:', video_codec
792
        # VIDEO ASPECT RATIO:
793
        video_aspect_ratio = self.app.config.video_aspect_ratio
794
        _set_combobox_value(self.aspect_ratio_widget, video_aspect_ratio)
795
        print ' * video_aspect_ratio:', video_aspect_ratio
796
        # VIDEO FULLSCREEN:
797
        video_fullscreen = self.app.config.video_fullscreen
798
        self.video_fullscreen_widget.set_active(video_fullscreen)
799
        print ' * video_fullscreen:', video_fullscreen
800
        # VIDEO DEINTERLACE:
801
        video_deinterlace = self.app.config.video_deinterlace
802
        self.video_deinterlace_widget.set_active(video_deinterlace)
803
        print ' * video_deinterlace:', video_deinterlace
804
        # VIDEO JITTERBUFFER
805
        video_jitterbuffer = self.app.config.video_jitterbuffer
806
        self.video_jitterbuffer_widget.set_value(video_jitterbuffer) # spinbutton
807
        print ' * video_jitterbuffer:', video_jitterbuffer
808
        # VIDEO BITRATE
809
        video_bitrate = self.app.config.video_bitrate
810
        self.video_bitrate_widget.set_value(video_bitrate) # spinbutton
811
        print ' * video_bitrate:', video_bitrate
812
        # VIDEO PREVIEW
813
        preview_in_window = self.app.config.preview_in_window
814
        self.preview_in_window_widget.set_active(preview_in_window)
815
        print " * preview_in_window: ", preview_in_window
816
        
817
        # ADDRESSBOOK:
818
        # Init addressbook contact list:
819
        self.app.address_book.selected_contact = None
820
        self.app.address_book.current_contact_is_new = False
821
        if len(self.app.address_book.contact_list) > 0:
822
            for contact in self.app.address_book.contact_list:
823
                contact_markup = format_contact_markup(contact)
824
                self.contact_tree.append([contact_markup])
825
            self.selection.select_path(self.app.address_book.selected)
826
        else:
827
            self.edit_contact_widget.set_sensitive(False)
828
            self.remove_contact_widget.set_sensitive(False)
829
            self.invite_contact_widget.set_sensitive(False)
830
        # AUDIO:
831
        audio_source_readable = _get_key_for_value(AUDIO_SOURCES, self.app.config.audio_source)
832
        audio_codec = _get_key_for_value(AUDIO_CODECS, self.app.config.audio_codec)
833
        audio_numchannels = self.app.config.audio_channels
834
        print " * audio_source:", audio_source_readable
835
        print " * audio_codec:", audio_codec
836
        print " * audio_numchannels:", audio_numchannels
837
        self.audio_numchannels_widget.set_value(audio_numchannels) # spinbutton
838
        _set_combobox_value(self.audio_source_widget, audio_source_readable)
839
        _set_combobox_value(self.audio_codec_widget, audio_codec)
840
        
841
        # MIDI:
842
        print "MIDI send enabled:", self.app.config.midi_send_enabled
843
        print "MIDI recv enabled:", self.app.config.midi_recv_enabled
844
        print "MIDI input:", self.app.config.midi_input_device
845
        print "MIDI output:", self.app.config.midi_output_device
846
        print "MIDI jitterbuffer:", self.app.config.midi_jitterbuffer
847
        self.midi_send_enabled_widget.set_active(self.app.config.midi_send_enabled)
848
        self.midi_recv_enabled_widget.set_active(self.app.config.midi_recv_enabled)
849
        _set_combobox_value(self.midi_input_device_widget, self.app.config.midi_input_device)
850
        _set_combobox_value(self.midi_output_device_widget, self.app.config.midi_output_device)
851
        self.midi_jitterbuffer_widget.set_value(self.app.config.midi_jitterbuffer)
852
        self._widgets_changed_by_user = True
853

    
854
    def update_streaming_state(self):
855
        """
856
        Changes the sensitivity and state of many widgets according to if we are streaming or not.
857
        
858
        Makes most of the audio/video buttons and widgets sensitive or not.
859
        Changes the invite button:
860
         * the icon
861
         * the label
862
        Makes the contact list sensitive or not.
863
        """
864
        self._toggle_streaming_state_sensitivity()
865
        self._update_rtcp_stats()
866

    
867
    def _toggle_streaming_state_sensitivity(self):
868
        _video_widgets_to_toggle_sensitivity = [
869
            self.video_capture_size_widget,
870
            self.video_source_widget,
871
            self.aspect_ratio_widget,
872
            self.video_view_preview_widget,
873
            self.preview_in_window_widget, 
874
            ]
875
        
876
        _other_widgets_to_toggle_sensitivity = [
877
            self.audio_source_widget,
878
            self.audio_codec_widget,
879
            self.audio_numchannels_widget,
880
            self.contact_list_widget,
881
            self.add_contact_widget,
882
            self.remove_contact_widget,
883
            self.edit_contact_widget,
884
            self.video_fullscreen_widget,
885
            self.video_deinterlace_widget,
886
            self.video_jitterbuffer_widget,
887
            self.video_codec_widget,
888
            self.video_display_widget,
889
            self.midi_input_device_widget, 
890
            self.midi_output_device_widget,
891
            self.midi_send_enabled_widget, 
892
            self.midi_recv_enabled_widget,
893
            self.midi_jitterbuffer_widget,
894
            ]
895
        
896
        self.update_bitrate_and_codec()
897
        
898
        is_streaming = self.app.has_session()
899
        is_previewing =  self.preview_manager.is_busy()
900
        if is_streaming:
901
            details = self.app.streamer_manager.session_details
902
        _contact_list_currently_sensitive = self.contact_list_widget.get_property("sensitive")
903
        streaming_state_has_changed = is_streaming == _contact_list_currently_sensitive
904
        if streaming_state_has_changed:
905
            print("streaming state has changed to %s" % (is_streaming))
906
            if is_streaming:
907
                text = _("Stop streaming")
908
                icon = gtk.STOCK_CONNECT
909
            else:
910
                text = _("Invite this contact")
911
                icon = gtk.STOCK_DISCONNECT
912
            self.invite_label_widget.set_text(text)
913
            self.invite_icon_widget.set_from_stock(icon, 4)
914
            
915
            # Toggle sensitivity of many widgets:
916
            new_sensitivity = not is_streaming
917
            print 'Got to change the sensitivity of many widgets to', new_sensitivity
918
            for widget in _other_widgets_to_toggle_sensitivity:
919
                widget.set_sensitive(new_sensitivity)
920
            for widget in _video_widgets_to_toggle_sensitivity:
921
                widget.set_sensitive(new_sensitivity)
922
                
923
            # Update the summary: 
924
            # peer: --------------------------------
925
            if is_streaming:
926
                peer_name = details["peer"]["name"]
927
                if details["peer"]["name"] != details["peer"]["address"]:
928
                    peer_name += " (%s)" % (details["peer"]["address"])
929
                self.info_peer_widget.set_text(peer_name)
930
            else:
931
                self.info_peer_widget.set_text(_("Not connected"))
932
            
933
            self.make_midi_widget_sensitive_or_not()       
934
        
935
        # also clean up the preview drawing area every second
936
        if self.preview_manager.is_busy():
937
            if self.preview_in_window_widget.get_property('sensitive'): # check if we have to change their state.
938
                for widget in _video_widgets_to_toggle_sensitivity:
939
                    if widget is not self.video_view_preview_widget:
940
                        widget.set_sensitive(False)
941
                # change icon
942
        else:
943
            if not is_streaming: # FIXME
944
                #if self.video_preview_icon_widget.get_stock() == gtk.STOCK_MEDIA_STOP:
945
                for widget in _video_widgets_to_toggle_sensitivity:
946
                    widget.set_sensitive(True)
947
            if self.preview_area_x_window_id is not None:
948
                if self.preview_area_widget.window is not None:
949
                    self.preview_area_widget.window.clear()
950

    
951
    def make_midi_widget_sensitive_or_not(self):
952
        # make the MIDI widget insensitive if disabled
953
        print("make_midi_widget_sensitive_or_not")
954
        is_streaming = self.app.has_session()
955
        if not is_streaming:
956
            if not self.app.config.midi_send_enabled:
957
                self.midi_input_device_widget.set_sensitive(False)
958
            else:
959
                self.midi_input_device_widget.set_sensitive(True)
960
                
961
            if not self.app.config.midi_recv_enabled:
962
                self.midi_output_device_widget.set_sensitive(False)
963
                self.midi_jitterbuffer_widget.set_sensitive(False)
964
            else:
965
                self.midi_output_device_widget.set_sensitive(True)
966
                self.midi_jitterbuffer_widget.set_sensitive(True)
967

    
968
    def on_midi_send_enabled_toggled(self, *args):
969
        if self._widgets_changed_by_user:
970
            self._gather_configuration()
971
            self.make_midi_widget_sensitive_or_not()
972
    
973
    def on_midi_recv_enabled_toggled(self, *args):
974
        if self._widgets_changed_by_user:
975
            self._gather_configuration()
976
            self.make_midi_widget_sensitive_or_not()
977

    
978
    def _update_rtcp_stats(self):
979
        is_streaming = self.app.has_session()
980
        # update the audio and video summary:(even if the state has not just changed)
981
        if is_streaming:
982
            def _format_bitrate(bitrate):
983
                """ Returns formatted bitrate string """
984
                BITS_PER_MBIT = 1000000.0
985
                BITS_PER_KBIT = 1000.0
986
                if bitrate is not None:
987
                    divisor = BITS_PER_MBIT
988
                    prefix = "M"
989
                    if bitrate < BITS_PER_MBIT:
990
                        divisor = BITS_PER_KBIT
991
                        prefix = "K"
992
                    return " " + _("%2.2f %sbits/s") % (bitrate / divisor, prefix)
993
                else:
994
                    return ""
995

    
996
            details = self.app.streamer_manager.session_details
997
            rtcp_stats = self.app.streamer_manager.rtcp_stats
998
            # send video: --------------------------------
999
            _info_send_video = _("%(width)dx%(height)d %(codec)s") % {
1000
                "width": details["send"]["video"]["width"], 
1001
                "height": details["send"]["video"]["height"], 
1002
                "codec": details["send"]["video"]["codec"], 
1003
                }
1004
            _info_send_video += _format_bitrate(rtcp_stats["send"]["video"]["bitrate"])
1005
            _info_send_video += "\n"
1006
            #_video_packetloss = rtcp_stats["send"]["video"]["packets-loss-percent"]
1007
            _info_send_video += _("Jitter: %(jitter)d ns") % {# % is escaped with an other %
1008
                "jitter": rtcp_stats["send"]["video"]["jitter"]
1009
                }
1010
            #_info_send_video += _("jitter: %(jitter)d ns. packet loss: %(packetloss)2.2f%%.") % {# % is escaped with an other %
1011
            #    "jitter": rtcp_stats["send"]["video"]["jitter"],
1012
            #    "packetloss": _video_packetloss
1013
            #    }
1014
            #print("info send video: " + _info_send_video)
1015
            self.info_send_video_widget.set_text(_info_send_video)
1016
            # send audio: --------------------------------
1017
            _info_send_audio = _("%(numchannels)d-channel %(codec)s") % {
1018
                "numchannels": details["send"]["audio"]["numchannels"], 
1019
                "codec": details["send"]["audio"]["codec"] 
1020
                }
1021
            _info_send_audio += _format_bitrate(rtcp_stats["send"]["audio"]["bitrate"])
1022
            _info_send_audio += "\n"
1023
            #_audio_packetloss = rtcp_stats["send"]["audio"]["packets-loss-percent"]
1024
            _info_send_audio += _("Jitter: %(jitter)d ns") % { # % is escaped with an other %
1025
                "jitter": rtcp_stats["send"]["audio"]["jitter"]
1026
                }
1027
            #print("info send audio: " + _info_send_audio)
1028
            self.info_send_audio_widget.set_text(_info_send_audio)
1029
            # recv video: --------------------------------
1030
            _info_recv_video = _("%(width)dx%(height)d %(codec)s") % {
1031
                "width": details["receive"]["video"]["width"], 
1032
                "height": details["receive"]["video"]["height"], 
1033
                "codec": details["receive"]["video"]["codec"], 
1034
                }
1035
            _info_recv_video += _format_bitrate(rtcp_stats["receive"]["video"]["bitrate"])
1036
            _info_recv_video += "\n" + _("Display: %(display)s") % {"display": details["receive"]["video"]["display"]}
1037
            if details["receive"]["video"]["fullscreen"]:
1038
                _info_recv_video += "\n" + _("Fullscreen is enabled.")
1039

    
1040
            #print("info recv video: " + _info_recv_video)
1041
            self.info_receive_video_widget.set_text(_info_recv_video)
1042
            # recv audio: --------------------------------
1043
            _info_recv_audio = _("%(numchannels)d-channel %(codec)s") % {
1044
                "numchannels": details["receive"]["audio"]["numchannels"], 
1045
                "codec": details["receive"]["audio"]["codec"] 
1046
                }
1047
            _info_recv_audio += _format_bitrate(rtcp_stats["receive"]["audio"]["bitrate"])
1048
            self.info_receive_audio_widget.set_text(_info_recv_audio)
1049
            # MIDI : --------------------------
1050
            _info_recv_midi = ""
1051
            _info_send_midi = ""
1052
            if details["receive"]["midi"]["enabled"]:
1053
                _info_recv_midi += _("Receiving MIDI") + "\n"
1054
                _info_recv_midi += _("Output device: %(name)s" % {"name": self.app.config.midi_output_device}) + "\n"
1055
                _info_recv_midi += _("Jitter buffer: %(jitterbuffer)d ms" % {"jitterbuffer": self.app.config.midi_jitterbuffer})
1056
            else:
1057
                _info_recv_midi += _("Disabled")
1058
            if details["send"]["midi"]["enabled"]:
1059
                _info_send_midi += _("Sending MIDI") + "\n"
1060
                _info_send_midi += _("Input device: %(name)s" % {"name": self.app.config.midi_input_device})
1061
            else:
1062
                _info_send_midi += _("Disabled")
1063
                
1064
            self.info_receive_midi_widget.set_text(_info_recv_midi)
1065
            self.info_send_midi_widget.set_text(_info_send_midi)
1066
        else:
1067
            self.info_send_video_widget.set_text("")
1068
            self.info_send_audio_widget.set_text("")
1069
            self.info_receive_video_widget.set_text("")
1070
            self.info_receive_audio_widget.set_text("")
1071
            self.info_receive_midi_widget.set_text("")
1072
            self.info_send_midi_widget.set_text("")
1073
        
1074
        
1075
    def update_local_ip(self):
1076
        """
1077
        Updates the local IP addresses widgets.
1078
        Called every n seconds.
1079
        """
1080
        def _cb(result):
1081
            """
1082
            @param result: list of ipv4 addresses
1083
            """
1084
            num = len(result)
1085
            txt = ""
1086
            for i in range(len(result)):
1087
                ip = result[i]
1088
                txt += ip
1089
                if i != num - 1:
1090
                    txt += "\n"
1091
            self.info_ip_widget.set_text(txt)
1092
        deferred = networkinterfaces.list_network_interfaces_addresses()
1093
        deferred.addCallback(_cb)
1094
        
1095
    def on_audio_codec_changed(self, widget):
1096
        """
1097
        Called when the user selects a different audio codec, updates
1098
        the range of the numchannels box.
1099
        """
1100
        old_numchannels = self.audio_numchannels_widget.get_value()
1101
        max_channels = None
1102
        if _get_combobox_value(self.audio_codec_widget) == "MP3":
1103
            max_channels = 2
1104
        elif _get_combobox_value(self.audio_codec_widget) == "Raw":
1105
            max_channels = 8
1106
        elif _get_combobox_value(self.audio_codec_widget) == "Vorbis":
1107
            max_channels = 24 
1108
        # update range and clamp numchannels to new range 
1109
        self.audio_numchannels_widget.set_range(1, max_channels)
1110
        self.audio_numchannels_widget.set_value(min(old_numchannels, max_channels)) 
1111

    
1112
    def update_bitrate_and_codec(self):
1113
        old_bitrate = self.video_bitrate_widget.get_value()
1114
        codec = _get_combobox_value(self.video_codec_widget)
1115
        is_streaming = self.app.has_session()
1116
        if codec in VIDEO_BITRATE_MIN_MAX.keys():
1117
            if is_streaming:
1118
                self.video_bitrate_widget.set_sensitive(False)
1119
            else:
1120
                self.video_bitrate_widget.set_sensitive(True)
1121
            mini = VIDEO_BITRATE_MIN_MAX[codec][0]
1122
            maxi = VIDEO_BITRATE_MIN_MAX[codec][1]
1123
            self.video_bitrate_widget.set_range(mini, maxi)
1124
            self.video_bitrate_widget.set_value(min(maxi, max(old_bitrate, mini)))
1125
        else:
1126
            self.video_bitrate_widget.set_sensitive(False)
1127
    
1128
    def on_video_codec_changed(self, widget):
1129
        self.update_bitrate_and_codec()
1130
        
1131
    def update_x11_devices(self):
1132
        """
1133
        Called once Application.poll_x11_devices has been run
1134
        """
1135
        x11_displays = [display["name"] for display in self.app.devices["x11_displays"]]
1136
        print("Updating X11 displays with values %s" % (x11_displays))
1137
        _set_combobox_choices(self.video_display_widget, x11_displays)
1138

    
1139
    def update_midi_devices(self):
1140
        """
1141
        Called once Application.poll_midi_devices has been run
1142
        """
1143
        self._widgets_changed_by_user = False
1144
        input_devices = [device["name"] for device in self.app.devices["midi_input_devices"]]
1145
        output_devices = [device["name"] for device in self.app.devices["midi_output_devices"]]
1146
        print("Updating MIDI devices with values %s %s" % (input_devices, output_devices))
1147
        _set_combobox_choices(self.midi_input_device_widget, input_devices)
1148
        _set_combobox_choices(self.midi_output_device_widget, output_devices)
1149
        self._widgets_changed_by_user = True
1150

    
1151
    def update_camera_devices(self):
1152
        """
1153
        Called once Application.poll_camera_devices has been run
1154
        """
1155
        self._video_source_changed_by_user = False
1156
        cameras = self.app.devices["cameras"].keys()
1157
        cameras.insert(0, VIDEO_TEST_INPUT)
1158
        print("Updating video sources with values %s" % (cameras))
1159
        _set_combobox_choices(self.video_source_widget, cameras)
1160
        self.update_v4l2_inputs_size_and_norm()
1161
        self._video_source_changed_by_user = True
1162

    
1163
    def update_v4l2_inputs_size_and_norm(self):
1164
        """
1165
        Called when : 
1166
         * user chooses a different video source.
1167
        If the selected is not a V4L2, disables the input and norm widgets.
1168
        """
1169
        value = _get_combobox_value(self.video_source_widget)
1170
        self._v4l2_input_changed_by_user = False
1171
        self._v4l2_standard_changed_by_user = False
1172
        # change choices and value:
1173
        if value == VIDEO_TEST_INPUT:
1174
            # INPUTS:
1175
            self.v4l2_input_widget.set_sensitive(False)
1176
            self.v4l2_input_widget.set_active(-1)
1177
            # STANDARD:
1178
            self.v4l2_standard_widget.set_sensitive(False)
1179
            self.v4l2_standard_widget.set_active(-1)
1180
            # SIZE:
1181
            _set_combobox_choices(self.video_capture_size_widget, ALL_SUPPORTED_SIZE)
1182
        else:
1183
            # INPUTS:
1184
            current_camera_name = _get_combobox_value(self.video_source_widget)
1185
            cam = self.app.devices["cameras"][current_camera_name]
1186
            current_input = cam["input"]
1187
            if current_input is not None: # check if device has many inputs
1188
                self.v4l2_input_widget.set_sensitive(True)
1189
                _set_combobox_choices(self.v4l2_input_widget, cam["inputs"])
1190
                _set_combobox_value(self.v4l2_input_widget, cam["inputs"][current_input]) # which in turn calls on_v4l2_input_changed
1191
            else:
1192
                self.v4l2_input_widget.set_sensitive(False)
1193
                self.v4l2_input_widget.set_active(-1)
1194
                
1195
            # STANDARD: 
1196
            current_standard = cam["standard"]
1197
            if current_standard is not None: # check if device supports different standards
1198
                self.v4l2_standard_widget.set_sensitive(True)
1199
                _set_combobox_choices(self.v4l2_standard_widget, VIDEO_STANDARDS)
1200
                _set_combobox_value(self.v4l2_standard_widget, cam["standard"]) # which in turn calls on_v4l2_standard_changed
1201
            else:
1202
                self.v4l2_standard_widget.set_sensitive(False)
1203
                self.v4l2_standard_widget.set_active(-1)
1204
            #self.v4l2_standard_widget.set_sensitive(True)
1205
            # SIZE:
1206
            print "supported sizes: ", cam["supported_sizes"]
1207
            _set_combobox_choices(self.video_capture_size_widget, cam["supported_sizes"]) # TODO: more test sizes
1208
        # once done:
1209
        self._v4l2_input_changed_by_user = True
1210
        self._v4l2_standard_changed_by_user = True
1211
            
1212
    def on_video_source_changed(self, widget):
1213
        """
1214
        Called when the user changes the video source.
1215
         * updates the input
1216
        """
1217
        if self._video_source_changed_by_user:
1218
            current_camera_name = _get_combobox_value(self.video_source_widget)
1219
            if current_camera_name != VIDEO_TEST_INPUT:
1220
                self.app.poll_camera_devices()
1221
            self.update_v4l2_inputs_size_and_norm()
1222

    
1223
    def on_v4l2_standard_changed(self, widget):
1224
        """
1225
        When the user changes the V4L2 standard, we actually change this standard using milhouse.
1226
        Calls `milhouse --videodevice /dev/videoX --v4l2-standard XXX
1227
        Values are either NTSC or PAL.
1228
        """
1229
        if self._v4l2_standard_changed_by_user:
1230
            # change standard for device
1231
            current_camera_name = _get_combobox_value(self.video_source_widget)
1232
            if current_camera_name != VIDEO_TEST_INPUT:
1233
                def _cb2(result):
1234
                    # callback for the poll_cameras_devices deferred.
1235
                    # check if successfully changed norm
1236
                    # see below
1237
                    cameras = self.app.devices["cameras"]
1238
                    try:
1239
                        cam = cameras[current_camera_name]
1240
                    except KeyError, e:
1241
                        print("Camera %s disappeared !" % (current_camera_name))
1242
                    else:
1243
                        actual_standard = cam["standard"]
1244
                        if actual_standard != standard_name:
1245
                            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}
1246
                            print(msg)
1247
                            dialogs.ErrorDialog.create(msg, parent=self.main_window)
1248
                            
1249
                            self._v4l2_standard_changed_by_user = False
1250
                            _set_combobox_value(self.v4l2_standard_widget, actua_standard)
1251
                            self._v4l2_standard_changed_by_user = True
1252
                            # Maybe we should show an error dialog in that case, or set the value to what it really is.
1253
                        else:
1254
                            print("Successfully changed standard to %s for device %s." % (actual_standard, current_camera_name))
1255
                            print("Now polling cameras.")
1256
                    self.v4l2_standard_widget.set_sensitive(True)
1257
                
1258
                standard_name = _get_combobox_value(widget)
1259
                cam = self.app.devices["cameras"][current_camera_name]
1260
                
1261
                self.v4l2_standard_widget.set_sensitive(False)
1262
                d = cameras.set_v4l2_video_standard(device_name=current_camera_name, standard=standard_name)
1263
                def _cb(result):
1264
                    d2 = self.app.poll_camera_devices()
1265
                    d2.addCallback(_cb2)
1266
                    
1267
                d.addCallback(_cb)
1268
        
1269
    def on_v4l2_input_changed(self, widget):
1270
        """
1271
        When the user changes the V4L2 input, we actually change this input using milhouse.
1272
        Calls `milhouse --videodevice /dev/videoX --v4l2-input N
1273
        """
1274
        if self._v4l2_input_changed_by_user:
1275
            # change input for device
1276
            current_camera_name = _get_combobox_value(self.video_source_widget)
1277
            if current_camera_name != VIDEO_TEST_INPUT:
1278
                input_name = _get_combobox_value(widget)
1279
                cam = self.app.devices["cameras"][current_camera_name]
1280
                input_number = cam["inputs"].index(input_name)
1281
                d = cameras.set_v4l2_input_number(device_name=current_camera_name, input_number=input_number)
1282
                def _cb2(cameras):
1283
                    try:
1284
                        cam = cameras[current_camera_name]
1285
                    except KeyError, e:
1286
                        print("Camera %s disappeared !" % (current_camera_name))
1287
                    else:
1288
                        actual_input = cam["input"]
1289
                        if actual_input != input_number:
1290
                            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}
1291
                            print(msg)
1292
                            # Maybe we should show an error dialog in that case, or set the value to what it really is.
1293
                        else:
1294
                            print("Successfully changed input to %s for device %s." % (actual_input, current_camera_name))
1295
                def _cb(result):
1296
                    d2 = cameras.list_cameras()
1297
                    d2.addCallback(_cb2)
1298
                d.addCallback(_cb)
1299

    
1300
    # -------------------------- menu items -----------------
1301
    
1302
    def on_about_menu_item_activate(self, menu_item):
1303
        About.create() # TODO: set parent window ?
1304
    
1305
    def on_quit_menu_item_activated(self, menu_item):
1306
        """
1307
        Quits the application.
1308
        """
1309
        print menu_item, "chosen"
1310
        self._confirm_and_quit()
1311
    
1312
    def on_help_menu_item_activated(self, menu_item):
1313
        """
1314
        Opens a web browser to the scenic web site.
1315
        """
1316
        print menu_item, "chosen"
1317
        url = ONLINE_HELP_URL 
1318
        webbrowser.open(url)
1319

    
1320
    # ---------------------- invitation dialogs -------------------
1321

    
1322
    def on_invite_contact_clicked(self, *args):
1323
        """
1324
        Sends an INVITE to the remote peer.
1325
        """
1326
        if self.app.has_session():
1327
            self.app.stop_streamers()
1328
        else:
1329
            self.app.send_invite()
1330

    
1331
    def show_confirm_dialog(self, text, callback=None):
1332
        """
1333
        This could be replaced by a yes/no dialog. That's actually what it is.
1334
        """
1335
        deferred = self.confirm_dialog.show(text)
1336
        if callback is not None:
1337
            deferred.addCallback(callback)
1338

    
1339
    def show_invited_dialog(self, text):
1340
        """ 
1341
        This could be replaced by a yes/no dialog. That's actually what it is.
1342
        @rettype: L{Deferred}
1343
        """
1344
        return self.invited_dialog.show(text)
1345

    
1346
    def show_calling_dialog(self):
1347
        """
1348
        Creates a new widget and show it.
1349
        """
1350
        self.calling_dialog = None
1351
        widgets_tree = glade.get_widgets_tree()
1352
        self.calling_dialog = widgets_tree.get_widget("calling_dialog")
1353
        self.calling_dialog.set_parent(self.main_window)
1354
        self.calling_dialog.connect('delete-event', self.on_invite_contact_cancelled)
1355
        self.calling_dialog.show()
1356
    
1357
    def on_invite_contact_cancelled(self, *args):
1358
        """
1359
        Sends a CANCEL to the remote peer when invite contact window is closed.
1360
        """
1361
        # unschedule this timeout as we don't care if our peer answered or not
1362
        self.app.send_cancel_and_disconnect(reason=communication.CANCEL_REASON_CANCELLED)
1363
        print("Inviting window is closed. ")
1364
        self.hide_calling_dialog()
1365
        return True # don't let the delete-event propagate
1366

    
1367
    def hide_calling_dialog(self):
1368
        """
1369
        Hides the "calling_dialog" dialog.
1370
        """
1371
        self._unschedule_inviting_timeout_delayed()
1372
        if self.calling_dialog is not None:
1373
            self.calling_dialog.hide()
1374

    
1375
    def _unschedule_inviting_timeout_delayed(self):
1376
        """
1377
        Unschedules our offer timeout delayed call. 
1378
        """
1379
        if self._inviting_timeout_delayed is not None and self._inviting_timeout_delayed.active():
1380
            self._inviting_timeout_delayed.cancel()
1381
            self._inviting_timeout_delayed = None
1382
    
1383
    def _schedule_inviting_timeout_delayed(self):
1384
        """ 
1385
        Schedules our offer invite timeout function 
1386
        """
1387
        def _cl_offerer_invite_timed_out():
1388
            # in case of invite timeout, act as if we'd cancelled the invite ourselves
1389
            print("Inviting window time out. ")
1390
            self.app.send_cancel_and_disconnect(reason=communication.CANCEL_REASON_TIMEOUT)
1391
            self.on_invite_contact_cancelled()
1392
            self.hide_calling_dialog()
1393
            text = _("The invitation expired. \n\nThe remote peer did not answer quick enough.")
1394
            dialogs.ErrorDialog.create(text, parent=self.main_window)
1395
            # here we return false so that this callback is unregistered
1396
            return False
1397

    
1398
        if self._inviting_timeout_delayed is None or not self._inviting_timeout_delayed.active():
1399
            self._inviting_timeout_delayed = reactor.callLater(INVITE_TIMEOUT, _cl_offerer_invite_timed_out)
1400
        else:
1401
            print("Warning: Already scheduled a timeout as we're already inviting a contact")
1402

    
1403
    def update_jackd_status(self):
1404
        is_zombie = self.app.devices["jackd_is_zombie"]
1405
        is_running = self.app.devices["jackd_is_running"]
1406
        fill_stats = False
1407
        if is_zombie:
1408
                self.audio_jack_state_widget.set_markup(_("<b>Zombie</b>"))
1409
                self.audio_jack_icon_widget.set_from_stock(gtk.STOCK_DIALOG_WARNING, 4)
1410
        else:
1411
            if is_running:
1412
                self.audio_jack_state_widget.set_markup(_("<b>Running</b>"))
1413
                self.audio_jack_icon_widget.set_from_stock(gtk.STOCK_YES, 4)
1414
                fill_stats = True
1415
            else:
1416
                self.audio_jack_state_widget.set_markup(_("<b>Not running</b>"))
1417
                self.audio_jack_icon_widget.set_from_stock(gtk.STOCK_NO, 4)
1418
        if fill_stats:
1419
            j = self.app.devices["jack_servers"][0] 
1420
            try:
1421
                latency = (j["period"] * j["nperiods"] / float(j["rate"])) * 1000 # ms
1422
            except KeyError, e:
1423
                print 'Key %s is missing for the jack server process' % (e)
1424
            else:
1425
                self.jack_latency_widget.set_text("%4.2f ms" % (latency))
1426
                self.jack_sampling_rate_widget.set_text("%d Hz" % (j["rate"]))
1427
        else:
1428
            self.jack_latency_widget.set_text("")
1429
            self.jack_sampling_rate_widget.set_text("")
1430
            
1431

    
1432
class About(object):
1433
    """
1434
    About dialog
1435
    """
1436
    def __init__(self):
1437
        # TODO: set parent window ?
1438
        self.icon_file = os.path.join(configure.PIXMAPS_DIR, 'scenic.png')
1439
        self.about_dialog = gtk.AboutDialog()
1440

    
1441
    def show_about_dialog(self):
1442
        self.about_dialog.set_name(configure.APPNAME)
1443
        self.about_dialog.set_role('about')
1444
        self.about_dialog.set_version(configure.VERSION)
1445
        commentlabel = ONE_LINE_DESCRIPTION 
1446
        self.about_dialog.set_comments(commentlabel)
1447
        self.about_dialog.set_copyright(COPYRIGHT_SHORT) 
1448
        self.about_dialog.set_license(LICENSE_TEXT)
1449
        self.about_dialog.set_authors(AUTHORS_LIST)
1450
        #self.about_dialog.set_artists(['Public domain'])
1451
        gtk.about_dialog_set_url_hook(self.show_website)
1452
        self.about_dialog.set_website(PROJECT_WEBSITE)
1453
        if not os.path.exists(self.icon_file):
1454
            print("Could not find icon file %s." % (self.icon_file))
1455
        else:
1456
            large_icon = gtk.gdk.pixbuf_new_from_file(self.icon_file)
1457
            self.about_dialog.set_logo(large_icon)
1458
        # Connect to callbacks
1459
        self.about_dialog.connect('response', self.destroy_about)
1460
        self.about_dialog.connect('delete_event', self.destroy_about)
1461
        self.about_dialog.show_all()
1462

    
1463
    @staticmethod
1464
    def create():
1465
        """
1466
        @rettype: None
1467
        """
1468
        dialog = About()
1469
        return dialog.show_about_dialog()
1470
     
1471
    def show_website(self, widget, data):
1472
        webbrowser.open(data)
1473

    
1474
    def destroy_about(self, *args):
1475
        self.about_dialog.destroy()
1476