This URL has Read-Only access.

Statistics
| Branch: | Tag: | Revision:

root / py / maugis / scenic / gui.py @ e9787f59

History | View | Annotate | Download (41.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
Negotiation is done as follow:
26
------------------------------
27
 * {"msg":"INVITE", "videoport":10000, "audioport":11000, "sid":0, "please_send_to_port":999}
28
   * Each peer ask for ports to send to, and of media settings as well. "video": [{"port":10000, "codec":"mpeg4", "bitrate":3000000}]
29
 * {"msg":"ACCEPT", "videoport":10000, "audioport":11000, "sid":0}
30
 * {"msg":"REFUSE", "sid":0}
31
 * {"msg":"CANCEL", "sid":0}
32
 * {"msg":"ACK", "sid":0}
33
 * {"msg":"BYE", "sid":0}
34
 * {"msg":"OK", "sid":0}
35

36
Former Notes
37
------------
38
 * 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 ???
39
"""
40
### CONSTANTS ###
41
from scenic import version
42
__version__ = version.__version__
43
APP_NAME = "scenic"
44

    
45
### MODULES IMPORTS  ###
46

    
47
import sys
48
import os
49
import smtplib
50
import gtk.glade
51
import gobject
52
import webbrowser
53
import gettext
54
import shutil
55
import tempfile
56

    
57
from twisted.internet import defer
58
from twisted.internet import error
59
from twisted.internet import reactor
60

    
61
from scenic import communication
62
from scenic import saving
63
from scenic import process # just for constants
64
from scenic.streamer import StreamerManager
65
from scenic import dialogs
66
from scenic import ports
67
from scenic import data
68
PACKAGE_DATA = os.path.dirname(data.__file__)
69

    
70
### MULTILINGUAL SUPPORT ###
71
_ = gettext.gettext
72
gettext.bindtextdomain(APP_NAME, os.path.join(PACKAGE_DATA, "locale"))
73
gettext.textdomain(APP_NAME)
74
gtk.glade.bindtextdomain(APP_NAME, os.path.join(PACKAGE_DATA, "locale"))
75
gtk.glade.textdomain(APP_NAME)
76

    
77
class Config(saving.ConfigStateSaving):
78
    """
79
    Class attributes are default.
80
    """
81
    # Default values
82
    negotiation_port = 17446 # receiving TCP (SIC) messages on it.
83
    smtpserver = "smtp.sat.qc.ca"
84
    email_info = "scenic@sat.qc.ca"
85
    audio_source = "jackaudiosrc"
86
    audio_sink = "jackaudiosink"
87
    audio_codec = "raw"
88
    audio_channels = 8
89
    video_source = "v4l2src"
90
    video_device = "/dev/video0"
91
    video_sink = "xvimagesink"
92
    video_codec = "mpeg4"
93
    video_display = ":0.0"
94
    video_bitrate = "3000000"
95
    video_width = 640
96
    video_height = 480
97
    confirm_quit = False
98

    
99
    def __init__(self):
100
        config_file = 'scenic.cfg'
101
        if os.path.isfile('/etc/' + config_file):
102
            config_dir = '/etc'
103
        else:
104
            config_dir = os.environ['HOME'] + '/.scenic'
105
        config_file_path = os.path.join(config_dir, config_file)
106
        saving.ConfigStateSaving.__init__(self, config_file_path)
107

    
108
def _get_combobox_value(widget):
109
    """
110
    Returns the current value of a GTK ComboBox widget.
111
    """
112
    index = widget.get_active()
113
    tree_model = widget.get_model()
114
    tree_model_row = tree_model[index]
115
    return tree_model_row[0] 
116

    
117
def _set_combobox_value(widget, value=None):
118
    """
119
    Sets the current value of a GTK ComboBox widget.
120
    """
121
    index = None
122
    tree_model = widget.get_model()
123
    index = 0
124
    for i in iter(tree_model):
125
        v = i[0]
126
        if v == value:
127
            break # got it
128
        index += 1
129
    if index is None:
130
        widget.set_active(-1)
131
    else:
132
        widget.set_active(index)
133

    
134
# GUI value to milhouse value mapping:
135
VIDEO_CODECS = {
136
    "h.264": "h264",
137
    "h.263": "h263",
138
    "Theora": "theora",
139
    "MPEG4": "mpeg4"
140
    }
141

    
142
def format_contact_markup(contact):
143
    """
144
    Formats a contact for the Adressbook GTK widget.
145
    @param contact: A dict with keys "name", "address" and "port"
146
    @rettype: str
147
    @return: Pango markup for the TreeView widget.
148
    """
149
    return "<b>%s</b>\n  IP: %s\n  Port: %s" % (contact["name"], contact["address"], contact["port"])
150

    
151
ABOUT_LABEL = """<b><big>Scenic</big></b>
152
Version: %s
153
Copyright: SAT
154
Authors: Etienne Desautels, Alexandre Quessy, Tristan Matthews, Simon Piette""" % (__version__)
155

    
156
ABOUT_TEXT_VIEW = """
157
Scenic is the advanced user graphical interface for the Milhouse audio/video streamer for GNU/Linux. 
158
"""
159

    
160
class Gui(object):
161
    """
162
    Main application (arguably God) class
163
     * Contains the main GTK window
164
    """
165
    def __init__(self, kiosk_mode=False, fullscreen=False):
166
        # --------------------------------------
167
        # TODO: move that stuff to the Application class
168
        self.config = Config() # XXX
169
        self.load_gtk_theme()
170
        self.kiosk_mode_on = kiosk_mode
171
        self.send_video_port = None # XXX
172
        self.recv_video_port = None # XXX
173
        self.send_audio_port = None # XXX
174
        self.recv_audio_port = None # XXX
175
        self.address_book = saving.AddressBook() # XXX
176
        self.streamer_manager = StreamerManager(self) # XXX
177
        self._has_session = False # XXX
178
        self.streamer_manager.state_changed_signal.connect(self.on_streamer_state_changed) # XXX
179
        print("Starting SIC server on port %s" % (self.config.negotiation_port)) # XXX
180
        self.server = communication.Server(self, self.config.negotiation_port) # XXX
181
        self.client = None # XXX
182
        self.got_bye = False # XXX
183
        # ---------------------------------------
184
        
185
        self._offerer_invite_timeout = None
186
        # Set the Glade file
187
        glade_file = os.path.join(PACKAGE_DATA, 'scenic.glade')
188
        if os.path.isfile(glade_file):
189
            glade_path = glade_file
190
        else:
191
            text = _("<b><big>Could not find the Glade file?</big></b>\n\n" \
192
                    "Be sure the file %s exists. Quitting.") % glade_file
193
            print(text)
194
            sys.exit()
195
        self.widgets = gtk.glade.XML(glade_path, domain=APP_NAME)
196
        
197
        # connects callbacks to widgets automatically
198
        cb = {}
199
        for n in dir(self.__class__):
200
            if n[0] != '_' and hasattr(self, n):
201
                cb[n] = getattr(self, n)
202
        self.widgets.signal_autoconnect(cb)
203

    
204
        # Get all the widgets that we use
205
        self.main_window = self.widgets.get_widget("main_window")
206
        self.main_window.connect('delete-event', self.on_main_window_deleted)
207
        self.main_window.set_icon_from_file(os.path.join(PACKAGE_DATA, 'scenic.png'))
208
        self.main_tabs_widget = self.widgets.get_widget("mainTabs")
209
        self.main_window.connect("window-state-event", self.on_window_state_event)
210
        # confirm_dialog:
211
        self.confirm_dialog = self.widgets.get_widget("confirm_dialog")
212
        self.confirm_dialog.connect('delete-event', self.confirm_dialog.hide_on_delete)
213
        self.confirm_dialog.set_transient_for(self.main_window)
214
        self.confirm_label = self.widgets.get_widget("confirm_label")
215
        # calling_dialog:
216
        self.calling_dialog = self.widgets.get_widget("calling_dialog")
217
        self.calling_dialog.connect('delete-event', self.on_invite_contact_cancelled)
218
        # error_dialog:
219
        self.error_dialog = self.widgets.get_widget("error_dialog")
220
        self.error_dialog.connect('delete-event', self.error_dialog.hide_on_delete)
221
        self.error_dialog.set_transient_for(self.main_window)
222
        # Could not connect:
223
        self.error_label_widget = self.widgets.get_widget("error_dialog_label")
224
        # invited_dialog:
225
        self.invited_dialog = self.widgets.get_widget("invited_dialog")
226
        self.invited_dialog.set_transient_for(self.main_window)
227
        self.invited_dialog.connect('delete-event', self.invited_dialog.hide_on_delete)
228
        self.invited_dialog_label_widget = self.widgets.get_widget("invited_dialog_label")
229
        # edit_contact_window:
230
        self.edit_contact_window = self.widgets.get_widget("edit_contact_window")
231
        self.edit_contact_window.set_transient_for(self.main_window) # child of main window
232
        self.edit_contact_window.connect('delete-event', self.edit_contact_window.hide_on_delete)
233
        self.contact_name_widget = self.widgets.get_widget("contact_name")
234
        self.contact_addr_widget = self.widgets.get_widget("contact_addr")
235
        self.contact_port_widget = self.widgets.get_widget("contact_port")
236
        # address book buttons and list:
237
        self.edit_contact_widget = self.widgets.get_widget("edit_contact")
238
        self.remove_contact_widget = self.widgets.get_widget("remove_contact")
239
        self.invite_contact_widget = self.widgets.get_widget("invite_contact")
240
        self.contact_list_widget = self.widgets.get_widget("contact_list")
241
        # position of currently selected contact in list of contact:
242
        self.selected_contact_row = None
243
        self.select_contact_index = None
244
        # video tab drop-down menus
245
        self.video_size_widget = self.widgets.get_widget("video_size")
246
        self.video_display_widget = self.widgets.get_widget("video_display")
247
        self.video_bitrate_widget = self.widgets.get_widget("video_bitrate")
248
        self.video_source_widget = self.widgets.get_widget("video_source")
249
        self.video_codec_widget = self.widgets.get_widget("video_codec")
250
        self.video_view_preview_widget = self.widgets.get_widget("video_view_preview")
251
        # about tab contents:
252
        self.about_label_widget = self.widgets.get_widget("about_label")
253
        self.about_text_view_widget = self.widgets.get_widget("about_text_view")
254
            
255
        # switch to Kiosk mode if asked
256
        if self.kiosk_mode_on:
257
            self.main_window.set_decorated(False)
258
            self.widgets.get_widget("sysBox").show() # shows shutdown and reboot buttons.
259
        self.is_fullscreen = False
260
        if fullscreen:
261
            self.toggle_fullscreen()
262
        
263
        # Build the contact list view
264
        self.selection = self.contact_list_widget.get_selection()
265
        self.selection.connect("changed", self.on_contact_list_changed, None) 
266
        self.contact_tree = gtk.ListStore(str)
267
        self.contact_list_widget.set_model(self.contact_tree)
268
        column = gtk.TreeViewColumn(_("Contacts"), gtk.CellRendererText(), markup=0)
269
        self.contact_list_widget.append_column(column)
270
        # set value of widgets.
271
        # TODO: get rid of those methods
272
        self._init_widgets_value() # XXX
273

    
274
        self.main_window.show()
275
        self.ports_allocator = ports.PortsAllocator()
276
        try:
277
            self.server.start_listening()
278
        except error.CannotListenError, e:
279
            print("Cannot start SIC server.")
280
            print(str(e))
281
            raise
282
        reactor.addSystemEventTrigger("before", "shutdown", self.before_shutdown)
283
        reactor.callLater(3, self.load_gtk_theme, "/usr/share/themes/Glossy/gtk-2.0/gtkrc")
284
   
285
    # ------------------ window events and actions --------------------
286

    
287
    def load_gtk_theme(self, file_name="/usr/share/themes/Darklooks/gtk-2.0/gtkrc"):
288
        # FIXME: not able to reload themes dynamically.
289
        if os.path.exists(file_name):
290
            gtk.rc_reset_styles(gtk.settings_get_default())
291
            print "loading theme", file_name
292
            gtk.rc_parse(file_name)
293
            gtk.rc_reparse_all()
294
        else:
295
            print("File name not found: %s" % (file_name))
296
     
297
    def toggle_fullscreen(self):
298
        """
299
        Toggles the fullscreen mode on/off.
300
        """
301
        if self.is_fullscreen:
302
            self.main_window.unfullscreen()
303
        else:
304
            self.main_window.fullscreen()
305
    
306
    def on_window_state_event(self, widget, event):
307
        """
308
        Called when toggled fullscreen.
309
        """
310
        self.is_fullscreen = event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN != 0
311
        print('fullscreen %s' % (self.is_fullscreen))
312
        return True
313
    
314
    def on_main_window_deleted(self, *args):
315
        """
316
        Destroy method causes appliaction to exit
317
        when main window closed
318
        """
319
        return self._confirm_and_quit()
320
        
321
    def _confirm_and_quit(self):
322
        def _cb(result):
323
            if result:
324
                print("Destroying the window.")
325
                self.main_window.destroy()
326
            else:
327
                print("Not quitting.")
328
        # If you return FALSE in the "delete_event" signal handler,
329
        # GTK will emit the "destroy" signal. Returning TRUE means
330
        # you don't want the window to be destroyed.
331
        # This is useful for popping up 'are you sure you want to quit?'
332
        # type dialogs. 
333
        if self.config.confirm_quit:
334
            d = dialogs.YesNoDialog.create("Really quit ?\nAll streaming processes will quit as well.\nMake sure to save your settings if desired.", parent=self.main_window)
335
            d.addCallback(_cb)
336
            return True
337
        else:
338
            _cb(True)
339
            return False
340
    
341
    def before_shutdown(self):
342
        """
343
        Last things done before quitting.
344
        """
345
        print("The application is shutting down.")
346
        # TODO: stop streamers
347
        if self.client is not None:
348
            if not self.got_bye:
349
                self.send_bye()
350
                self.stop_streamers()
351
            self.disconnect_client()
352
        print('stopping server')
353
        self.server.close()
354
        
355
    def on_main_window_destroyed(self, *args):
356
        # TODO: confirm dialog!
357
        if reactor.running:
358
            print("reactor.stop()")
359
            reactor.stop()
360

    
361
    # --------------- slots for some widget events ------------
362

    
363
    def on_video_view_preview_toggled(self, widget):
364
        """
365
        Shows a preview of the video input.
366
        """
367
        #TODO: create a new process protocol for the preview window
368
        #TODO: stop it when starting to stream, if running
369
        #TODO: stop it when button is toggled to false.
370
        # It can be the user that pushed the button, or it can be toggled by the software.
371
        print 'video_view_preview toggled', widget.get_active()
372
        if widget.get_active():
373
            command = "milhouse --videosource v4l2src --videodevice %s --localvideo --window-title preview" % (self.config.video_device)
374
            print "spawning", command
375
            process.run_once(*command.split())
376
            dialogs.ErrorDialog.create("You must manually close the preview window.", parent=self.main_window)
377
        else:
378
            print "stopping preview"
379

    
380
    def on_main_tabs_switch_page(self, widget, notebook_page, page_number):
381
        tab = widget.get_nth_page(page_number)
382
        if tab == "localPan":
383
            self.widgets.get_widget("network_admin").grab_default() # FIXME
384
        elif tab == "contactPan":
385
            self.widgets.get_widget("contactJoinBut").grab_default() # FIXME
386

    
387
    def on_contact_list_changed(self, *args):
388
        tree_list, self.selected_contact_row = args[0].get_selected()
389
        if self.selected_contact_row:
390
            self.edit_contact_widget.set_sensitive(True)
391
            self.remove_contact_widget.set_sensitive(True)
392
            self.invite_contact_widget.set_sensitive(True)
393
            self.selected_contact_index = tree_list.get_path(self.selected_contact_row)[0]
394
            self.address_book.selected_contact = self.address_book.contact_list[self.selected_contact_index]
395
            self.address_book.selected_index = self.selected_contact_index
396
        else:
397
            self.edit_contact_widget.set_sensitive(False)
398
            self.remove_contact_widget.set_sensitive(False)
399
            self.invite_contact_widget.set_sensitive(False)
400
            self.address_book.selected_contact = None
401

    
402
    # ---------------------- slots for addressbook widgets events --------
403
    
404
    def on_contact_double_clicked(self, *args):
405
        """
406
        When a contact in the list is double-clicked, 
407
        shows the edit contact dialog.
408
        """
409
        self.on_edit_contact_clicked(args)
410

    
411
    def on_add_contact_clicked(self, *args):
412
        """
413
        Pops up a dialog to be filled with new contact infos.
414
        
415
        The add_contact buttons has been clicked.
416
        """
417
        self.address_book.current_contact_is_new = True
418
        # Update the text in the edit/new contact dialog:
419
        self.contact_name_widget.set_text("")
420
        self.contact_addr_widget.set_text("")
421
        self.contact_port_widget.set_text("")
422
        self.edit_contact_window.show()
423

    
424
    def on_remove_contact_clicked(self, *args):
425
        """
426
        Upon confirmation, the selected contact is removed.
427
        """
428
        def on_confirm_result(result):
429
            if result:
430
                del self.address_book.contact_list[self.selected_contact_index]
431
                self.contact_tree.remove(self.selected_contact_row)
432
                num = self.selected_contact_index - 1
433
                if num < 0:
434
                    num = 0
435
                self.selection.select_path(num)
436
        text = _("<b><big>Delete this contact from the list?</big></b>\n\nAre you sure you want "
437
            "to delete this contact from the list?")
438
        self.show_confirm_dialog(text, on_confirm_result)
439

    
440
    def on_edit_contact_clicked(self, *args):
441
        """
442
        Shows the edit contact dialog.
443
        """
444
        self.contact_name_widget.set_text(self.address_book.selected_contact["name"])
445
        self.contact_addr_widget.set_text(self.address_book.selected_contact["address"])
446
        self.contact_port_widget.set_text(str(self.address_book.selected_contact["port"]))
447
        self.edit_contact_window.show() # addr
448

    
449
    def on_edit_contact_cancel_clicked(self, *args):
450
        """
451
        The cancel button in the "edit_contact" window has been clicked.
452
        Hides the edit_contact window.
453
        """
454
        self.edit_contact_window.hide()
455

    
456
    def on_edit_contact_save_clicked(self, *args):
457
        """
458
        The save button in the "edit_contact" window has been clicked.
459
        Hides the edit_contact window and saves the changes. (new or modified contact)
460
        """
461
        def when_valid_save():
462
            """ Saves contact info after it's been validated and then closes the window"""
463
            contact = {
464
                "name": self.contact_name_widget.get_text(),
465
                "address": addr, 
466
                "port": int(port)
467
                }
468
            contact_markup = format_contact_markup(contact)
469
            if self.address_book.current_contact_is_new:
470
                self.contact_tree.append([contact_markup]) # add it to the tree list
471
                self.address_book.contact_list.append([contact_markup]) # and the internal address book
472
                self.selection.select_path(len(self.address_book.contact_list) - 1) # select it ...?
473
                self.address_book.selected_contact = self.address_book.contact_list[len(self.address_book.contact_list) - 1] #FIXME: we should not copy a dict like that
474
                self.address_book.current_contact_is_new = False # FIXME: what does that mean?
475
            else:
476
                self.contact_tree.set_value(self.selected_contact_row, 0, contact_markup)
477
            self.address_book.selected_contact = contact
478
            self.edit_contact_window.hide()
479

    
480
        # Validate the port number
481
        port = self.contact_port_widget.get_text()
482
        if port == "":
483
            port = str(self.config.negotiation_port) # set port to default
484
        elif not port.isdigit():
485
            text = _("The port number must be an integer.")
486
            self.show_error_dialog(text)
487
            return
488
        elif int(port) not in range(10000, 65535):
489
            text = _("The port number must be in the range of 10000-65535")
490
            self.show_error_dialog(text)
491
            return
492
        # Validate the address
493
        addr = self.contact_addr_widget.get_text()
494
        if len(addr) < 7:
495
            text = _("The address is not valid\n\nEnter a valid address\nExample: 168.123.45.32 or example.org")
496
            self.show_error_dialog(text)
497
            return
498
        # save it.
499
        when_valid_save()
500

    
501
    # ---------------------------- Custom system tab buttons -----------------------
502

    
503
    def on_network_admin_clicked(self, *args):
504
        """
505
        Opens the network-admin Gnome applet.
506
        """
507
        process.run_once("gksudo", "network-admin")
508

    
509
    def on_system_shutdown_clicked(self, *args):
510
        """
511
        Shuts down the computer.
512
        """
513
        def on_confirm_result(result):
514
            if result:
515
                process.run_once("gksudo", "shutdown -h now")
516

    
517
        text = _("<b><big>Shutdown the computer?</big></b>\n\nAre you sure you want to shutdown the computer now?")
518
        self.show_confirm_dialog(text, on_confirm_result)
519

    
520
    def on_system_reboot_clicked(self, *args):
521
        """
522
        Reboots the computer.
523
        """
524
        def on_confirm_result(result):
525
            if result:
526
                process.run_once("gksudo", "shutdown -r now")
527

    
528
        text = _("<b><big>Reboot the computer?</big></b>\n\nAre you sure you want to reboot the computer now?")
529
        self.show_confirm_dialog(text, on_confirm_result)
530

    
531
    def on_maintenance_apt_update_clicked(self, *args):
532
        """
533
        Opens APT update manager.
534
        """
535
        process.run_once("gksudo", "update-manager")
536

    
537
    def on_maintenance_send_info_clicked(self, *args):
538
        """
539
        Sends an email to SAT with this information : 
540
         * milhouse version
541
         * kernel version
542
         * Loaded kernel modules
543
        """
544
        def on_confirm_result(result):
545
            milhouse_version = "unknown"
546
            if result:
547
                msg = "--- milhouse_version ---\n" + milhouse_version + "\n"
548
                msg += "--- uname -a ---\n"
549
                try:
550
                    w, r, err = os.popen3('uname -a')
551
                    msg += r.read() + "\n"
552
                    errRead = err.read()
553
                    if errRead:
554
                        msg += errRead + "\n"
555
                    w.close()
556
                    r.close()
557
                    err.close()
558
                except:
559
                    msg += "Error executing 'uname -a'\n"
560
                msg += "--- lsmod ---\n"
561
                try:
562
                    w, r, err = os.popen3('lsmod')
563
                    msg += r.read()
564
                    errRead = err.read()
565
                    if errRead:
566
                        msg += "\n" + errRead
567
                    w.close()
568
                    r.close()
569
                    err.close()
570
                except:
571
                    msg += "Error executing 'lsmod'"
572
                fromaddr = self.config.email_info
573
                toaddrs  = self.config.email_info
574
                toaddrs = toaddrs.split(', ')
575
                server = smtplib.SMTP(self.config.smtpserver)
576
                server.set_debuglevel(0)
577
                try:
578
                    server.sendmail(fromaddr, toaddrs, msg)
579
                except:
580
                    text = _("Could not send info.\n\nCheck your internet connection.")
581
                    self.show_error_dialog(text)
582
                server.quit()
583
        
584
        text = _("<b><big>Send the settings?</big></b>\n\nAre you sure you want to send your computer settings to the administrator of scenic?")
585
        self.show_confirm_dialog(text, on_confirm_result)
586

    
587
    # ------------------------- session occuring -------------
588
    def has_session(self):
589
        """
590
        @rettype: bool
591
        """
592
        return self._has_session
593
    # -------------------- streamer ports -----------------
594

    
595
    def allocate_ports(self):
596
        # TODO: start_session
597
        self.recv_video_port = self.ports_allocator.allocate()
598
        self.recv_audio_port = self.ports_allocator.allocate()
599

    
600
    def free_ports(self):
601
        # TODO: stop_session
602
        for port in [self.recv_video_port, self.recv_audio_port]:
603
            try:
604
                self.ports_allocator.free(port)
605
            except ports.PortsAllocatorError, e:
606
                print(e)
607
        
608
    # --------------------- configuration and widgets value ------------
609
    def save_configuration(self):
610
        """
611
        Saves the configuration to a file.
612
        Reads the widget value prior to do it.
613
        """
614
        self._gather_configuration() # need to get the value of the configuration widgets.
615
        self.config.save()
616
        self.address_book.save() # addressbook values are already stored.
617

    
618
    def _gather_configuration(self):
619
        """
620
        Updates the configuration with the value of each widget.
621
        """
622
        print("gathering configuration")
623
        # VIDEO SIZE
624
        video_size = _get_combobox_value(self.video_size_widget)
625
        print ' * video_size:', video_size
626
        self.config.video_width = int(video_size.split("x")[0])
627
        self.config.video_height = int(video_size.split("x")[1])
628
        
629
        # DISPLAY
630
        video_display = _get_combobox_value(self.video_display_widget)
631
        print ' * video_display:', video_display
632
        self.config.video_display = video_display
633
        
634
        # BITRATE
635
        video_bitrate = _get_combobox_value(self.video_bitrate_widget)
636
        print ' * video_bitrate:', video_bitrate
637
        self.config.video_bitrate = int(video_bitrate.split(" ")[0]) * 1000000
638
        
639
        # VIDEO SOURCE AND DEVICE
640
        video_source = _get_combobox_value(self.video_source_widget)
641
        if video_source == "Color bars":
642
            self.config.video_source = "videotestsrc"
643
        elif video_source.startswith("/dev/video"): # TODO: firewire!
644
            self.config.video_device = video_source
645
            self.config.video_source = "v4l2src"
646
        print ' * videosource:', video_source
647
        
648
        # CODEC
649
        video_codec = _get_combobox_value(self.video_codec_widget)
650
        self.config.video_codec = VIDEO_CODECS[video_codec]
651
        print ' * video_codec:', video_codec
652
        
653
        #TODO: get toggle fullscreen (milhouse) value
654

    
655
    def _init_widgets_value(self):
656
        """
657
        Called once at startup.
658
         * Once the config file is read, 
659
         * Sets the value of each widget according to the data stored in the configuration file.
660
        """
661
        print("Changing widgets value according to configuration.")
662
        # VIDEO SIZE
663
        video_size = "%sx%s" % (self.config.video_width, self.config.video_height)
664
        _set_combobox_value(self.video_size_widget, video_size)
665
        print ' * video_size:', video_size
666
        
667
        # DISPLAY
668
        video_display = self.config.video_display
669
        _set_combobox_value(self.video_display_widget, video_display)
670
        print ' * video_display:', video_display
671
        
672
        # BITRATE
673
        video_bitrate = "%s Mbps" % (int(self.config.video_bitrate) / 1000000)
674
        _set_combobox_value(self.video_bitrate_widget, video_bitrate)
675
        print ' * video_bitrate:', video_bitrate
676
        
677
        # VIDEO SOURCE AND DEVICE
678
        if self.config.video_source == "videotestsrc":
679
            video_source = "Color bars"
680
        elif self.config.video_source == "v4l2src":
681
            video_source = self.config.video_device
682
        _set_combobox_value(self.video_source_widget, video_source)
683
        print ' * videosource:', video_source
684

    
685
        # CODEC
686
        # gets key for a value
687
        video_codec = VIDEO_CODECS.keys()[VIDEO_CODECS.values().index(self.config.video_codec)]
688
        _set_combobox_value(self.video_codec_widget, video_codec)
689
        print ' * video_codec:', video_codec
690

    
691
        # ADDRESSBOOK
692
        # Init addressbook contact list:
693
        self.address_book.selected_contact = None
694
        self.address_book.current_contact_is_new = False
695
        if len(self.address_book.contact_list) > 0:
696
            for contact in self.address_book.contact_list:
697
                contact_markup = format_contact_markup(contact)
698
                self.contact_tree.append([contact_markup])
699
            self.selection.select_path(self.address_book.selected)
700
        else:
701
            self.edit_contact_widget.set_sensitive(False)
702
            self.remove_contact_widget.set_sensitive(False)
703
            self.invite_contact_widget.set_sensitive(False)
704

    
705
        # ABOUT TAB CONTENTS:
706
        self.about_label_widget.set_markup(ABOUT_LABEL)
707
        about_text_buffer = gtk.TextBuffer()
708
        about_text_buffer.set_text(ABOUT_TEXT_VIEW)
709
        self.about_text_view_widget.set_buffer(about_text_buffer)
710

    
711
    # -------------------------- menu items -----------------
712
    
713
    def on_quit_menu_item_activated(self, menu_item):
714
        """
715
        Quits the application.
716
        """
717
        print menu_item, "chosen"
718
        self._confirm_and_quit()
719
    
720
    def on_help_menu_item_activated(self, menu_item):
721
        """
722
        Opens a web browser to the scenic web site.
723
        """
724
        print menu_item, "chosen"
725
        url = "http://scenic.sat.qc.ca"
726
        webbrowser.open(url)
727

    
728
    def on_save_menu_item_activated(self, menu_item):
729
        """
730
        Saves the addressbook and settings.
731
        """
732
        print menu_item, "chosen"
733
        print("-- Saving addressbook and configuration. -- ")
734
        self.save_configuration()
735

    
736
    # ---------------------- invitation dialogs -------------------
737

    
738
    def on_invite_contact_clicked(self, *args):
739
        """
740
        Sends an INVITE to the remote peer.
741
        """
742
        self.allocate_ports()
743
        if self.streamer_manager.is_busy():
744
            dialogs.ErrorDialog.create("Impossible to invite a contact to start streaming. A streaming session is already in progress.", parent=self.main_window)
745
        else:
746
            # UPDATE when initiating session
747
            self._gather_configuration()
748
            self.send_invite()
749
    
750
    def on_invite_contact_cancelled(self, *args):
751
        """
752
        Sends a CANCEL to the remote peer when invite contact window is closed.
753
        """
754
        # unschedule this timeout as we don't care if our peer answered or not
755
        self._unschedule_offerer_invite_timeout()
756
        self.send_cancel_and_disconnect()
757
        # don't let the delete-event propagate
758
        if self.calling_dialog.get_property('visible'):
759
            self.calling_dialog.hide()
760
        return True
761

    
762
    def show_error_dialog(self, text, callback=None):
763
        def _response_cb(widget, response_id, callback):
764
            widget.hide()
765
            if callback is not None:
766
                callback()
767
            widget.disconnect(slot1)
768

    
769
        self.error_label_widget.set_text(text)
770
        dialog = self.error_dialog
771
        dialog.set_modal(True)
772
        slot1 = dialog.connect('response', _response_cb, callback)
773
        dialog.show()
774
    
775
    def show_confirm_dialog(self, text, callback=None):
776
        def _response_cb(widget, response_id, callback):
777
            widget.hide()
778
            if callback is not None:
779
                callback(response_id == gtk.RESPONSE_OK)
780
            widget.disconnect(slot1)
781

    
782
        self.confirm_label.set_label(text)
783
        dialog = self.confirm_dialog
784
        dialog.set_modal(True)
785
        slot1 = dialog.connect('response', _response_cb, callback)
786
        dialog.show()
787

    
788
    def show_invited_dialog(self, text, callback=None):
789
        """ 
790
        We disconnect and reconnect the callbacks every time
791
        this is called, otherwise we'd would have multiple 
792
        callback invokations per response since the widget 
793
        stays alive 
794
        """
795
        def _response_cb(widget, response_id, callback):
796
            widget.hide()
797
            if callback is not None:
798
                callback(response_id)
799
            widget.disconnect(slot1)
800

    
801
        self.invited_dialog_label_widget.set_label(text)
802
        dialog = self.invited_dialog
803
        dialog.set_modal(True)
804
        slot1 = dialog.connect('response', _response_cb, callback)
805
        dialog.show()
806

    
807
    def hide_calling_dialog(self, msg="", err=""):
808
        """
809
        Hides the "calling_dialog" dialog.
810
        Shows an error dialog if the argument msg is set to "err", "timeout", "answTimeout", "send", "refuse" or "badAnsw".
811
        """
812
        self.calling_dialog.hide()
813
        text = None
814
        if msg == "err":
815
            text = _("Contact unreacheable.\n\nCould not connect to the IP address of this contact.")
816
        elif msg == "answTimeout":
817
            text = _("Contact answer timeout.\n\nThe contact did not answer soon enough.")
818
        elif msg == "send":
819
            text = _("Problem sending command.\n\nError: %s") % err
820
        elif msg == "refuse":
821
            text = _("Connection refused.\n\nThe contact refused the connection.")
822
        elif msg == "badAnsw":
823
            text = _("Invalid answer.\n\nThe answer was not valid.")
824
        if text is not None:
825
            self.show_error_dialog(text)
826

    
827
    def _unschedule_offerer_invite_timeout(self):
828
        """ Unschedules our offer invite timeout function """
829
        if self._offerer_invite_timeout is not None:
830
            gobject.source_remove(self._offerer_invite_timeout)
831
            self._offerer_invite_timeout = None
832
    
833
    def _schedule_offerer_invite_timeout(self, data):
834
        """ Schedules our offer invite timeout function """
835
        if self._offerer_invite_timeout is None:
836
            self._offerer_invite_timeout = gobject.timeout_add(5000, self._cl_offerer_invite_timed_out, data)
837
        else:
838
            print("Warning: Already scheduled a timeout as we're already inviting a contact")
839

    
840
    def _cl_offerer_invite_timed_out(self, client):
841
        # XXX
842
        # in case of invite timeout, act as if we'd cancelled the invite ourselves
843
        self.on_invite_contact_cancelled()
844
        if self.calling_dialog.get_property('visible'):
845
            self.hide_calling_dialog("answTimeout")
846
        # here we return false so that this callback is unregistered
847
        return False
848

    
849
    # --------------------------- network receives ------------
850
    def handle_invite(self, message, addr):
851
        self.got_bye = False
852
        send_to_port = message["please_send_to_port"]
853
        
854
        def _on_contact_request_dialog_response(response):
855
            """
856
            User is accetping or declining an offer.
857
            @param result: Answer to the dialog.
858
            """
859
            if response == gtk.RESPONSE_OK:
860
                if self.client is not None:
861
                    self.send_accept(message, addr)
862
                else:
863
                    print("Error: connection lost, so we could not accept.")# FIXME
864
            elif response == gtk.RESPONSE_CANCEL or gtk.RESPONSE_DELETE_EVENT:
865
                self.send_refuse_and_disconnect() 
866
            else:
867
                pass
868
            return True
869

    
870
        if self.streamer_manager.is_busy():
871
            print("Got invitation, but we are busy.")
872
            communication.connect_send_and_disconnect(addr, send_to_port, {'msg':'REFUSE', 'sid':0}) #FIXME: where do we get the port number from?
873
        else:
874
            self.client = communication.Client(self, send_to_port)
875
            self.client.connect(addr)
876
            # TODO: if a contact in the addressbook has this address, displays it!
877
            text = _("<b><big>%s is inviting you.</big></b>\n\nDo you accept the connection?" % addr)
878
            self.show_invited_dialog(text, _on_contact_request_dialog_response)
879

    
880
    def handle_cancel(self):
881
        if self.client is not None:
882
            self.client.disconnect()
883
            self.client = None
884
        self.invited_dialog.hide()
885
        dialogs.ErrorDialog.create("Remote peer cancelled invitation.", parent=self.main_window)
886

    
887
    def handle_accept(self, message, addr):
888
        self._unschedule_offerer_invite_timeout()
889
        # FIXME: this doesn't make sense here
890
        self.got_bye = False
891
        # TODO: Use session to contain settings and ports
892
        if self.client is not None:
893
            self.hide_calling_dialog("accept")
894
            self.send_video_port = message["videoport"]
895
            self.send_audio_port = message["audioport"]
896
            if self.streamer_manager.is_busy():
897
                dialogs.ErrorDialog.create("A streaming session is already in progress.")
898
            else:
899
                print("Got ACCEPT. Starting streamers as initiator.")
900
                self.start_streamers(addr)
901
                self.send_ack()
902
        else:
903
            print("Error ! Connection lost.") # FIXME
904

    
905
    def handle_refuse(self):
906
        self._unschedule_offerer_invite_timeout()
907
        self.free_ports()
908
        self.hide_calling_dialog("refuse")
909

    
910
    def handle_ack(self, addr):
911
        print("Got ACK. Starting streamers as answerer.")
912
        self.start_streamers(addr)
913

    
914
    def handle_bye(self):
915
        self.got_bye = True
916
        self.stop_streamers()
917
        if self.client is not None:
918
            print('disconnecting client and sending BYE')
919
            self.client.send({"msg":"OK", "sid":0})
920
            self.disconnect_client()
921

    
922
    def handle_ok(self):
923
        print("received ok. Everything has an end.")
924
        if self.client is not None:
925
            print('disconnecting client')
926
            self.disconnect_client()
927

    
928
    def on_server_receive_command(self, message, addr):
929
        # XXX
930
        msg = message["msg"]
931
        print("Got %s from %s" % (msg, addr))
932
        
933
        if msg == "INVITE":
934
            self.handle_invite(message, addr)
935
        elif msg == "CANCEL":
936
            self.handle_cancel()
937
        elif msg == "ACCEPT":
938
            self.handle_accept(message, addr)
939
        elif msg == "REFUSE":
940
            self.handle_refuse()
941
        elif msg == "ACK":
942
            self.handle_ack(addr)
943
        elif msg == "BYE":
944
            self.handle_bye()
945
        elif msg == "OK":
946
            self.handle_ok()
947
        else:
948
            print ('WARNING: Unexpected message %s' % (msg))
949

    
950
    # -------------------------- actions on streamer manager --------
951

    
952
    def start_streamers(self, addr):
953
        self._has_session = True
954
        self.streamer_manager.start(addr, self.config)
955

    
956
    def stop_streamers(self):
957
        self.streamer_manager.stop()
958

    
959
    def on_streamers_stopped(self, addr):
960
        """
961
        We call this when all streamers are stopped.
962
        """
963
        print("on_streamers_stopped got called")
964
        self._has_session = False
965
        self.free_ports()
966

    
967
    # ---------------------- sending messages -----------
968
        
969
    def disconnect_client(self):
970
        """
971
        Disconnects the SIC sender.
972
        @rettype: L{Deferred}
973
        """
974
        def _cb(result, d1):
975
            self.client = None
976
            d1.callback(True)
977
        def _cl(d1):
978
            if self.client is not None:
979
                d2 = self.client.disconnect()
980
                d2.addCallback(_cb, d1)
981
            else:
982
                d1.callback(True)
983
        if self.client is not None:
984
            d = defer.Deferred()
985
            reactor.callLater(0, _cl, d)
986
            return d
987
        else: 
988
            return defer.succeed(True)
989

    
990
    def send_invite(self):
991
        msg = {
992
            "msg":"INVITE",
993
            "sid":0, 
994
            "videoport": self.recv_video_port,
995
            "audioport": self.recv_audio_port,
996
            "please_send_to_port": self.config.negotiation_port
997
            }
998
        port = self.config.negotiation_port
999
        ip = self.address_book.selected_contact["address"]
1000

    
1001
        def _on_connected(proto):
1002
            self._schedule_offerer_invite_timeout(self.client)
1003
            self.client.send(msg)
1004
            return proto
1005
        def _on_error(reason):
1006
            print ("error trying to connect to %s:%s : %s" % (ip, port, reason))
1007
            self.calling_dialog.hide()
1008
            self.client = None
1009
            return None
1010
           
1011
        print ("sending %s to %s:%s" % (msg, ip, port))
1012
        self.client = communication.Client(self, port)
1013
        deferred = self.client.connect(ip)
1014
        deferred.addCallback(_on_connected).addErrback(_on_error)
1015
        self.calling_dialog.show()
1016
        # window will be hidden when we receive ACCEPT or REFUSE
1017
    
1018
    def send_accept(self, message, addr):
1019
        # UPDATE config once we accept the invitie
1020
        self._gather_configuration()
1021
        self.allocate_ports()
1022
        self.client.send({"msg":"ACCEPT", "videoport":self.recv_video_port, "audioport":self.recv_audio_port, "sid":0})
1023
        # TODO: Use session to contain settings and ports
1024
        self.send_video_port = message["videoport"]
1025
        self.send_audio_port = message["audioport"]
1026
        
1027
    def send_ack(self):
1028
        self.client.send({"msg":"ACK", "sid":0})
1029

    
1030
    def send_bye(self):
1031
        """
1032
        Sends BYE
1033
        BYE stops the streaming on the remote host.
1034
        """
1035
        if self.client is not None:
1036
            self.client.send({"msg":"BYE", "sid":0})
1037
    
1038
    def send_cancel_and_disconnect(self):
1039
        """
1040
        Sends CANCEL
1041
        CANCEL cancels the invite on the remote host.
1042
        """
1043
        if self.client is not None:
1044
            self.client.send({"msg":"CANCEL", "sid":0})
1045
            self.client.disconnect()
1046
            self.client = None
1047
        else:
1048
            print('Warning: Trying to send CANCEL even though client is None')
1049
    
1050
    def send_refuse_and_disconnect(self):
1051
        """
1052
        Sends REFUSE 
1053
        REFUSE tells the offerer we can't have a session.
1054
        """
1055
        if self.client is not None:
1056
            self.client.send({"msg":"REFUSE", "sid":0})
1057
            self.client.disconnect()
1058
            self.client = None
1059
        else:
1060
            print('Warning: Trying to send REFUSE even though client is None')
1061

    
1062
    # ------------------- streaming events handlers ----------------
1063
    
1064
    def on_streamer_state_changed(self, streamer, new_state):
1065
        """
1066
        Slot for scenic.streamer.StreamerManager.state_changed_signal
1067
        """
1068
        if new_state in [process.STATE_STOPPED]:
1069
            if not self.got_bye:
1070
                """ got_bye means our peer sent us a BYE, so we shouldn't send one back """
1071
                print("Local StreamerManager stopped. Sending BYE")
1072
                self.send_bye()
1073
            
1074
    def on_client_socket_error(self, client, err, msg):
1075
        # XXX
1076
        self.hide_calling_dialog(msg)
1077
        text = _("%s: %s") % (str(err), str(msg))
1078
        self.show_error_dialog(text)
1079