Commit 5c56d0ae authored by Luci Stanescu's avatar Luci Stanescu

Added support for audio calls

parent f465c560
...@@ -13,9 +13,12 @@ from zope.interface import implements ...@@ -13,9 +13,12 @@ from zope.interface import implements
from sipsimple.application import SIPApplication from sipsimple.application import SIPApplication
from sipsimple.configuration.backend.file import FileBackend from sipsimple.configuration.backend.file import FileBackend
from sipsimple.configuration.settings import SIPSimpleSettings
from blink.configuration.settings import SIPSimpleSettingsExtension
from blink.mainwindow import MainWindow from blink.mainwindow import MainWindow
from blink.resources import ApplicationData from blink.resources import ApplicationData
from blink.sessions import SessionManager
from blink.util import QSingleton, run_in_gui_thread from blink.util import QSingleton, run_in_gui_thread
...@@ -29,6 +32,10 @@ class Blink(QApplication): ...@@ -29,6 +32,10 @@ class Blink(QApplication):
self.application = SIPApplication() self.application = SIPApplication()
self.main_window = MainWindow() self.main_window = MainWindow()
SIPSimpleSettings.register_extension(SIPSimpleSettingsExtension)
session_manager = SessionManager()
session_manager.initialize(self.main_window, self.main_window.session_model)
def run(self): def run(self):
from blink.util import call_in_gui_thread as call_later from blink.util import call_in_gui_thread as call_later
call_later(self._initialize_sipsimple) call_later(self._initialize_sipsimple)
......
# Copyright (C) 2010 AG Projects. See LICENSE for details.
#
# Copyright (C) 2010 AG Projects. See LICENSE for details.
#
"""Definitions of datatypes for use in settings extensions."""
__all__ = ['ApplicationDataPath', 'SoundFile']
import os
from blink.resources import ApplicationData
class ApplicationDataPath(unicode):
def __new__(cls, path):
path = os.path.normpath(path)
if path.startswith(ApplicationData.directory+os.path.sep):
path = path[len(ApplicationData.directory+os.path.sep):]
return unicode.__new__(cls, path)
@property
def normalized(self):
return ApplicationData.get(self)
class SoundFile(object):
def __init__(self, path, volume=100):
self.path = path
self.volume = int(volume)
if self.volume < 0 or self.volume > 100:
raise ValueError('illegal volume level: %d' % self.volume)
def __getstate__(self):
return u'%s,%s' % (self.__dict__['path'], self.volume)
def __setstate__(self, state):
try:
path, volume = state.rsplit(u',', 1)
except ValueError:
self.__init__(state)
else:
self.__init__(path, volume)
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.path, self.volume)
def _get_path(self):
return ApplicationData.get(self.__dict__['path'])
def _set_path(self, path):
path = os.path.normpath(path)
if path.startswith(ApplicationData.directory+os.path.sep):
path = path[len(ApplicationData.directory+os.path.sep):]
self.__dict__['path'] = path
path = property(_get_path, _set_path)
del _get_path, _set_path
# Copyright (C) 2010 AG Projects. See LICENSE for details.
#
"""Blink settings extensions."""
__all__ = ['SIPSimpleSettingsExtension']
from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension
from sipsimple.configuration.settings import AudioSettings
from blink.configuration.datatypes import ApplicationDataPath, SoundFile
from blink.resources import Resources
class AudioSettingsExtension(AudioSettings):
recordings_directory = Setting(type=ApplicationDataPath, default=ApplicationDataPath('recordings'), nillable=False)
class SoundSettings(SettingsGroup):
outbound_ringtone = Setting(type=SoundFile, default=SoundFile(Resources.get('sounds/ring_outbound.wav')), nillable=True)
class SIPSimpleSettingsExtension(SettingsObjectExtension):
audio = AudioSettingsExtension
sounds = SoundSettings
...@@ -24,11 +24,13 @@ from functools import partial ...@@ -24,11 +24,13 @@ from functools import partial
from operator import attrgetter from operator import attrgetter
from zope.interface import implements from zope.interface import implements
from sipsimple.account import BonjourAccount from sipsimple.account import AccountManager, BonjourAccount
from sipsimple.util import makedirs from sipsimple.util import makedirs
from blink.resources import ApplicationData, Resources, IconCache from blink.resources import ApplicationData, Resources, IconCache
from blink.sessions import SessionManager
from blink.util import run_in_gui_thread from blink.util import run_in_gui_thread
from blink.widgets.buttons import SwitchViewButton
# Functions decorated with updates_contacts_db or ignore_contacts_db_updates must # Functions decorated with updates_contacts_db or ignore_contacts_db_updates must
...@@ -859,6 +861,12 @@ class ContactModel(QAbstractListModel): ...@@ -859,6 +861,12 @@ class ContactModel(QAbstractListModel):
self.deleted_items.append(items) self.deleted_items.append(items)
self.itemsRemoved.emit(items) self.itemsRemoved.emit(items)
def iter_contacts(self):
return (item for item in self.items if isinstance(item, Contact))
def iter_contact_groups(self):
return (item for item in self.items if isinstance(item, ContactGroup))
def load(self): def load(self):
try: try:
try: try:
...@@ -1092,11 +1100,31 @@ class ContactListView(QListView): ...@@ -1092,11 +1100,31 @@ class ContactListView(QListView):
menu.addAction(self.actions.delete_item) menu.addAction(self.actions.delete_item)
menu.addAction(self.actions.undo_last_delete) menu.addAction(self.actions.undo_last_delete)
self.actions.undo_last_delete.setText(undo_delete_text) self.actions.undo_last_delete.setText(undo_delete_text)
account_manager = AccountManager()
default_account = account_manager.default_account
self.actions.start_audio_session.setEnabled(default_account is not None)
self.actions.start_chat_session.setEnabled(default_account is not None)
self.actions.send_sms.setEnabled(default_account is not None)
self.actions.send_files.setEnabled(default_account is not None)
self.actions.request_remote_desktop.setEnabled(default_account is not None)
self.actions.share_my_desktop.setEnabled(default_account is not None)
self.actions.edit_item.setEnabled(contact.editable) self.actions.edit_item.setEnabled(contact.editable)
self.actions.delete_item.setEnabled(contact.deletable) self.actions.delete_item.setEnabled(contact.deletable)
self.actions.undo_last_delete.setEnabled(len(model.deleted_items) > 0) self.actions.undo_last_delete.setEnabled(len(model.deleted_items) > 0)
menu.exec_(event.globalPos()) menu.exec_(event.globalPos())
def keyPressEvent(self, event):
if event.key() in (Qt.Key_Enter, Qt.Key_Return):
selected_indexes = self.selectionModel().selectedIndexes()
if len(selected_indexes) == 1:
contact = self.model().data(selected_indexes[0])
if not isinstance(contact, Contact):
return
session_manager = SessionManager()
session_manager.start_call(contact.name, contact.uri, account=BonjourAccount() if isinstance(contact, BonjourNeighbour) else None)
else:
super(ContactListView, self).keyPressEvent(event)
def _AH_AddGroup(self): def _AH_AddGroup(self):
group = ContactGroup("") group = ContactGroup("")
model = self.model() model = self.model()
...@@ -1145,6 +1173,8 @@ class ContactListView(QListView): ...@@ -1145,6 +1173,8 @@ class ContactListView(QListView):
def _AH_StartAudioCall(self): def _AH_StartAudioCall(self):
contact = self.model().data(self.selectionModel().selectedIndexes()[0]) contact = self.model().data(self.selectionModel().selectedIndexes()[0])
session_manager = SessionManager()
session_manager.start_call(contact.name, contact.uri, account=BonjourAccount() if isinstance(contact, BonjourNeighbour) else None)
def _AH_StartChatSession(self): def _AH_StartChatSession(self):
contact = self.model().data(self.selectionModel().selectedIndexes()[0]) contact = self.model().data(self.selectionModel().selectedIndexes()[0])
...@@ -1167,7 +1197,10 @@ class ContactListView(QListView): ...@@ -1167,7 +1197,10 @@ class ContactListView(QListView):
for group in self.model().contact_groups: for group in self.model().contact_groups:
group.restore_state() group.restore_state()
self.needs_restore = False self.needs_restore = False
self.model().main_window.switch_view_button.dnd_active = False main_window = self.model().main_window
main_window.switch_view_button.dnd_active = False
if not main_window.session_model.sessions:
main_window.switch_view_button.view = SwitchViewButton.ContactView
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
model = self.model() model = self.model()
...@@ -1374,11 +1407,31 @@ class ContactSearchListView(QListView): ...@@ -1374,11 +1407,31 @@ class ContactSearchListView(QListView):
menu.addAction(self.actions.delete_item) menu.addAction(self.actions.delete_item)
menu.addAction(self.actions.undo_last_delete) menu.addAction(self.actions.undo_last_delete)
self.actions.undo_last_delete.setText(undo_delete_text) self.actions.undo_last_delete.setText(undo_delete_text)
account_manager = AccountManager()
default_account = account_manager.default_account
self.actions.start_audio_session.setEnabled(default_account is not None)
self.actions.start_chat_session.setEnabled(default_account is not None)
self.actions.send_sms.setEnabled(default_account is not None)
self.actions.send_files.setEnabled(default_account is not None)
self.actions.request_remote_desktop.setEnabled(default_account is not None)
self.actions.share_my_desktop.setEnabled(default_account is not None)
self.actions.edit_item.setEnabled(contact.editable) self.actions.edit_item.setEnabled(contact.editable)
self.actions.delete_item.setEnabled(contact.deletable) self.actions.delete_item.setEnabled(contact.deletable)
self.actions.undo_last_delete.setEnabled(len(source_model.deleted_items) > 0) self.actions.undo_last_delete.setEnabled(len(source_model.deleted_items) > 0)
menu.exec_(event.globalPos()) menu.exec_(event.globalPos())
def keyPressEvent(self, event):
if event.key() in (Qt.Key_Enter, Qt.Key_Return):
selected_indexes = self.selectionModel().selectedIndexes()
if len(selected_indexes) == 1:
contact = self.model().data(selected_indexes[0])
if not isinstance(contact, Contact):
return
session_manager = SessionManager()
session_manager.start_call(contact.name, contact.uri, account=BonjourAccount() if isinstance(contact, BonjourNeighbour) else None)
else:
super(ContactSearchListView, self).keyPressEvent(event)
def _AH_EditItem(self): def _AH_EditItem(self):
model = self.model() model = self.model()
contact = model.data(self.selectionModel().selectedIndexes()[0]) contact = model.data(self.selectionModel().selectedIndexes()[0])
...@@ -1397,6 +1450,8 @@ class ContactSearchListView(QListView): ...@@ -1397,6 +1450,8 @@ class ContactSearchListView(QListView):
def _AH_StartAudioCall(self): def _AH_StartAudioCall(self):
contact = self.model().data(self.selectionModel().selectedIndexes()[0]) contact = self.model().data(self.selectionModel().selectedIndexes()[0])
session_manager = SessionManager()
session_manager.start_call(contact.name, contact.uri, account=BonjourAccount() if isinstance(contact, BonjourNeighbour) else None)
def _AH_StartChatSession(self): def _AH_StartChatSession(self):
contact = self.model().data(self.selectionModel().selectedIndexes()[0]) contact = self.model().data(self.selectionModel().selectedIndexes()[0])
...@@ -1415,7 +1470,10 @@ class ContactSearchListView(QListView): ...@@ -1415,7 +1470,10 @@ class ContactSearchListView(QListView):
def startDrag(self, supported_actions): def startDrag(self, supported_actions):
super(ContactSearchListView, self).startDrag(supported_actions) super(ContactSearchListView, self).startDrag(supported_actions)
self.model().main_window.switch_view_button.dnd_active = False main_window = self.model().main_window
main_window.switch_view_button.dnd_active = False
if not main_window.session_model.sessions:
main_window.switch_view_button.view = SwitchViewButton.ContactView
def dragEnterEvent(self, event): def dragEnterEvent(self, event):
model = self.model() model = self.model()
......
...@@ -9,18 +9,25 @@ from PyQt4 import uic ...@@ -9,18 +9,25 @@ from PyQt4 import uic
from PyQt4.QtCore import Qt from PyQt4.QtCore import Qt
from PyQt4.QtGui import QBrush, QColor, QPainter, QPen, QPixmap from PyQt4.QtGui import QBrush, QColor, QPainter, QPen, QPixmap
from sipsimple.account import AccountManager from application.notification import IObserver, NotificationCenter
from application.python.util import Null
from zope.interface import implements
from sipsimple.account import AccountManager, BonjourAccount
from blink.accounts import AccountModel, ActiveAccountModel from blink.accounts import AccountModel, ActiveAccountModel
from blink.contacts import Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel from blink.contacts import BonjourNeighbour, Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel
from blink.sessions import SessionModel from blink.sessions import SessionManager, SessionModel
from blink.resources import Resources from blink.resources import Resources
from blink.util import run_in_gui_thread
from blink.widgets.buttons import SwitchViewButton from blink.widgets.buttons import SwitchViewButton
ui_class, base_class = uic.loadUiType(Resources.get('blink.ui')) ui_class, base_class = uic.loadUiType(Resources.get('blink.ui'))
class MainWindow(base_class, ui_class): class MainWindow(base_class, ui_class):
implements(IObserver)
def __init__(self, parent=None): def __init__(self, parent=None):
super(MainWindow, self).__init__(parent) super(MainWindow, self).__init__(parent)
...@@ -42,7 +49,8 @@ class MainWindow(base_class, ui_class): ...@@ -42,7 +49,8 @@ class MainWindow(base_class, ui_class):
self.contact_list.setModel(self.contact_model) self.contact_list.setModel(self.contact_model)
self.search_list.setModel(self.contact_search_model) self.search_list.setModel(self.contact_search_model)
self.contact_list.selectionModel().selectionChanged.connect(self.contact_list_selection_changed) self.contact_list.selectionModel().selectionChanged.connect(self._SH_ContactListSelectionChanged)
self.search_list.selectionModel().selectionChanged.connect(self._SH_SearchListSelectionChanged)
self.search_box.textChanged.connect(self.contact_search_model.setFilterFixedString) self.search_box.textChanged.connect(self.contact_search_model.setFilterFixedString)
self.contact_model.load() self.contact_model.load()
...@@ -51,41 +59,42 @@ class MainWindow(base_class, ui_class): ...@@ -51,41 +59,42 @@ class MainWindow(base_class, ui_class):
self.session_model = SessionModel(self) self.session_model = SessionModel(self)
self.session_list.setModel(self.session_model) self.session_list.setModel(self.session_model)
self.session_model.test()
self.session_list.selectionModel().selectionChanged.connect(self.session_list_selection_changed) self.session_list.selectionModel().selectionChanged.connect(self._SH_SessionListSelectionChanged)
self.main_view.setCurrentWidget(self.contacts_panel) self.main_view.setCurrentWidget(self.contacts_panel)
self.contacts_view.setCurrentWidget(self.contact_list_panel) self.contacts_view.setCurrentWidget(self.contact_list_panel)
self.search_view.setCurrentWidget(self.search_list_panel) self.search_view.setCurrentWidget(self.search_list_panel)
self.switch_view_button.viewChanged.connect(self.switch_main_view) self.conference_button.setEnabled(False)
self.hangup_all_button.setEnabled(False)
self.switch_view_button.viewChanged.connect(self._SH_SwitchViewButtonChangedView)
self.search_box.textChanged.connect(self.search_box_text_changed) self.search_box.textChanged.connect(self._SH_SearchBoxTextChanged)
self.contact_model.itemsAdded.connect(self.contact_model_added_items) self.contact_model.itemsAdded.connect(self._SH_ContactModelAddedItems)
self.contact_model.itemsRemoved.connect(self.contact_model_removed_items) self.contact_model.itemsRemoved.connect(self._SH_ContactModelRemovedItems)
self.back_to_contacts_button.clicked.connect(self.search_box.clear) # this can be set in designer -Dan self.back_to_contacts_button.clicked.connect(self.search_box.clear) # this can be set in designer -Dan
self.add_contact_button.clicked.connect(self.add_contact) self.add_contact_button.clicked.connect(self._SH_AddContactButtonClicked)
self.add_search_contact_button.clicked.connect(self.add_contact) self.add_search_contact_button.clicked.connect(self._SH_AddContactButtonClicked)
self.identity.activated[int].connect(self.set_identity) self.identity.activated[int].connect(self._SH_IdentityChanged)
#self.connect(self.contact_list, QtCore.SIGNAL("doubleClicked(const QModelIndex &)"), self.double_click_action) self.audio_call_button.clicked.connect(self._SH_AudioCallButtonClicked)
self.contact_list.doubleClicked.connect(self._SH_ContactDoubleClicked) # activated is emitted on single click
self.search_list.doubleClicked.connect(self._SH_ContactDoubleClicked) # activated is emitted on single click
self.search_box.returnPressed.connect(self._SH_SearchBoxReturnPressed)
def add_contact(self, clicked): self.session_model.sessionAdded.connect(self._SH_SessionModelAddedSession)
model = self.contact_model self.session_model.structureChanged.connect(self._SH_SessionModelChangedStructure)
selected_items = ((index.row(), model.data(index)) for index in self.contact_list.selectionModel().selectedIndexes()) self.hangup_all_button.clicked.connect(self._SH_HangupAllButtonClicked)
try: self.conference_button.makeConference.connect(self._SH_MakeConference)
item = (item for row, item in sorted(selected_items) if type(item) in (Contact, ContactGroup)).next() self.conference_button.breakConference.connect(self._SH_BreakConference)
preferred_group = item if type(item) is ContactGroup else item.group
except StopIteration: notification_center = NotificationCenter()
try: notification_center.add_observer(self, name='SIPApplicationWillStart')
preferred_group = (group for group in model.contact_groups if type(group) is ContactGroup).next()
except StopIteration:
preferred_group = None
self.contact_editor.open_for_add(self.search_box.text(), preferred_group)
def set_user_icon(self, image_file_name): def set_user_icon(self, image_file_name):
pixmap = QPixmap(32, 32) pixmap = QPixmap(32, 32)
...@@ -109,30 +118,51 @@ class MainWindow(base_class, ui_class): ...@@ -109,30 +118,51 @@ class MainWindow(base_class, ui_class):
self.im_session_button.setEnabled(enabled) self.im_session_button.setEnabled(enabled)
self.ds_session_button.setEnabled(enabled) self.ds_session_button.setEnabled(enabled)
def set_identity(self, index): def _SH_AddContactButtonClicked(self, clicked):
account_manager = AccountManager() model = self.contact_model
account_manager.default_account = self.identity.itemData(index).toPyObject().account selected_items = ((index.row(), model.data(index)) for index in self.contact_list.selectionModel().selectedIndexes())
try:
item = (item for row, item in sorted(selected_items) if type(item) in (Contact, ContactGroup)).next()
preferred_group = item if type(item) is ContactGroup else item.group
except StopIteration:
try:
preferred_group = (group for group in model.contact_groups if type(group) is ContactGroup).next()
except StopIteration:
preferred_group = None
self.contact_editor.open_for_add(self.search_box.text(), preferred_group)
def search_box_text_changed(self, text): def _SH_AudioCallButtonClicked(self):
if text: list = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
self.switch_view_button.view = SwitchViewButton.ContactView selected_indexes = list.selectionModel().selectedIndexes()
self.enable_call_buttons(True) contact = list.model().data(selected_indexes[0]) if selected_indexes else Null
else: address = contact.uri or unicode(self.search_box.text())
name = contact.name or None
session_manager = SessionManager()
session_manager.start_call(name, address, account=BonjourAccount() if isinstance(contact, BonjourNeighbour) else None)
def _SH_BreakConference(self):
active_session = self.session_model.data(self.session_list.selectionModel().selectedIndexes()[0])
self.session_model.breakConference(active_session.conference)
def _SH_ContactDoubleClicked(self, index):
contact = index.model().data(index)
if not isinstance(contact, Contact):
return
session_manager = SessionManager()
session_manager.start_call(contact.name, contact.uri, account=BonjourAccount() if isinstance(contact, BonjourNeighbour) else None)
def _SH_ContactListSelectionChanged(self, selected, deselected):
account_manager = AccountManager()
selected_items = self.contact_list.selectionModel().selectedIndexes() selected_items = self.contact_list.selectionModel().selectedIndexes()
self.enable_call_buttons(len(selected_items)==1 and type(self.contact_model.data(selected_items[0])) is Contact) self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)==1 and isinstance(self.contact_model.data(selected_items[0]), Contact))
# switch to the sessions panel if there are active sessions, else to the contacts panel -Dan
active_widget = self.contact_list_panel if text.isEmpty() else self.search_panel
self.contacts_view.setCurrentWidget(active_widget)
active_widget = self.search_list_panel if self.contact_search_model.rowCount() else self.not_found_panel
self.search_view.setCurrentWidget(active_widget)
def contact_model_added_items(self, items): def _SH_ContactModelAddedItems(self, items):
if self.search_box.text().isEmpty(): if self.search_box.text().isEmpty():
return return
active_widget = self.search_list_panel if self.contact_search_model.rowCount() else self.not_found_panel active_widget = self.search_list_panel if self.contact_search_model.rowCount() else self.not_found_panel
self.search_view.setCurrentWidget(active_widget) self.search_view.setCurrentWidget(active_widget)
def contact_model_removed_items(self, items): def _SH_ContactModelRemovedItems(self, items):
if self.search_box.text().isEmpty(): if self.search_box.text().isEmpty():
return return
if any(type(item) is Contact for item in items) and self.contact_search_model.rowCount() == 0: if any(type(item) is Contact for item in items) and self.contact_search_model.rowCount() == 0:
...@@ -141,21 +171,89 @@ class MainWindow(base_class, ui_class): ...@@ -141,21 +171,89 @@ class MainWindow(base_class, ui_class):
active_widget = self.search_list_panel if self.contact_search_model.rowCount() else self.not_found_panel active_widget = self.search_list_panel if self.contact_search_model.rowCount() else self.not_found_panel
self.search_view.setCurrentWidget(active_widget) self.search_view.setCurrentWidget(active_widget)
def contact_list_selection_changed(self, selected, deselected): def _SH_HangupAllButtonClicked(self):
for session in self.session_model.sessions:
session.end()
def _SH_IdentityChanged(self, index):
account_manager = AccountManager()
account_manager.default_account = self.identity.itemData(index).toPyObject().account
def _SH_MakeConference(self):
self.session_model.conferenceSessions([session for session in self.session_model.sessions if session.conference is None and not session.pending_removal])
def _SH_SearchBoxReturnPressed(self):
address = unicode(self.search_box.text())
session_manager = SessionManager()
session_manager.start_call(None, address)
def _SH_SearchBoxTextChanged(self, text):
account_manager = AccountManager()
if text:
self.switch_view_button.view = SwitchViewButton.ContactView
selected_items = self.search_list.selectionModel().selectedIndexes()
self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)==1)
else:
selected_items = self.contact_list.selectionModel().selectedIndexes() selected_items = self.contact_list.selectionModel().selectedIndexes()
self.enable_call_buttons(len(selected_items)==1 and isinstance(self.contact_model.data(selected_items[0]), Contact)) self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)==1 and type(self.contact_model.data(selected_items[0])) is Contact)
# switch to the sessions panel if there are active sessions, else to the contacts panel -Dan
active_widget = self.contact_list_panel if text.isEmpty() else self.search_panel
if active_widget is self.search_panel and self.contacts_view.currentWidget() is not self.search_panel:
self.search_list.selectionModel().clearSelection()
self.contacts_view.setCurrentWidget(active_widget)
active_widget = self.search_list_panel if self.contact_search_model.rowCount() else self.not_found_panel
self.search_view.setCurrentWidget(active_widget)
def session_list_selection_changed(self, selected, deselected): def _SH_SearchListSelectionChanged(self, selected, deselected):
account_manager = AccountManager()
selected_items = self.search_list.selectionModel().selectedIndexes()
self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)==1)
def _SH_SessionListSelectionChanged(self, selected, deselected):
selected_indexes = selected.indexes() selected_indexes = selected.indexes()
if not selected_indexes: active_session = self.session_model.data(selected_indexes[0]) if selected_indexes else Null
if active_session.conference:
self.conference_button.setEnabled(True)
self.conference_button.setChecked(True)
else:
self.conference_button.setEnabled(len([session for session in self.session_model.sessions if session.conference is None and not session.pending_removal]) > 1)
self.conference_button.setChecked(False) self.conference_button.setChecked(False)
def _SH_SessionModelAddedSession(self, session_item):
if session_item.session.state is None:
self.search_box.clear()
def _SH_SessionModelChangedStructure(self):
self.hangup_all_button.setEnabled(any(not session.pending_removal for session in self.session_model.sessions))
selected_indexes = self.session_list.selectionModel().selectedIndexes()
active_session = self.session_model.data(selected_indexes[0]) if selected_indexes else Null
if active_session.conference:
self.conference_button.setEnabled(True)
self.conference_button.setChecked(True)
else: else:
active_session = self.session_model.data(selected_indexes[0]) self.conference_button.setEnabled(len([session for session in self.session_model.sessions if session.conference is None and not session.pending_removal]) > 1)
self.conference_button.setChecked(active_session.conference is not None) self.conference_button.setChecked(False)
def switch_main_view(self, view): def _SH_SwitchViewButtonChangedView(self, view):
self.main_view.setCurrentWidget(self.contacts_panel if view is SwitchViewButton.ContactView else self.sessions_panel) self.main_view.setCurrentWidget(self.contacts_panel if view is SwitchViewButton.ContactView else self.sessions_panel)
@run_in_gui_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_SIPApplicationWillStart(self, notification):
account_manager = AccountManager()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=account_manager, name='SIPAccountManagerDidChangeDefaultAccount')
def _NH_SIPAccountManagerDidChangeDefaultAccount(self, notification):
if notification.data.account is None:
self.enable_call_buttons(False)
else:
selected_items = self.contact_list.selectionModel().selectedIndexes()
self.enable_call_buttons(len(selected_items)==1 and isinstance(self.contact_model.data(selected_items[0]), Contact))
del ui_class, base_class del ui_class, base_class
...@@ -3,36 +3,136 @@ ...@@ -3,36 +3,136 @@
from __future__ import with_statement from __future__ import with_statement
__all__ = ['Conference', 'SessionItem', 'SessionModel', 'SessionListView'] __all__ = ['Conference', 'SessionItem', 'SessionModel', 'SessionListView', 'SessionManager']
import bisect
import os
import cPickle as pickle import cPickle as pickle
import re
from datetime import datetime, timedelta
from functools import partial
from PyQt4 import uic from PyQt4 import uic
from PyQt4.QtCore import Qt, QAbstractListModel, QByteArray, QEvent, QMimeData, QModelIndex, QSize, QStringList, QTimer, pyqtSignal from PyQt4.QtCore import Qt, QAbstractListModel, QByteArray, QEvent, QMimeData, QModelIndex, QObject, QSize, QStringList, QTimer, pyqtSignal
from PyQt4.QtGui import QAction, QBrush, QColor, QDrag, QLinearGradient, QListView, QMenu, QPainter, QPen, QPixmap, QStyle, QStyledItemDelegate from PyQt4.QtGui import QAction, QBrush, QColor, QDrag, QLinearGradient, QListView, QMenu, QPainter, QPen, QPixmap, QStyle, QStyledItemDelegate
from application.python.util import Null from application.notification import IObserver, NotificationCenter
from application.python.util import Null, Singleton
from zope.interface import implements
from sipsimple.account import Account, AccountManager
from sipsimple.application import SIPApplication
from sipsimple.audio import WavePlayer
from sipsimple.conference import AudioConference
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.core import SIPCoreError, SIPURI, ToHeader
from sipsimple.lookup import DNSLookup
from sipsimple.session import Session
from sipsimple.streams import MediaStreamRegistry
from sipsimple.util import limit
from blink.resources import Resources from blink.resources import Resources
from blink.widgets.buttons import LeftSegment, MiddleSegment, RightSegment from blink.util import call_later, run_in_gui_thread
from blink.widgets.buttons import LeftSegment, MiddleSegment, RightSegment, SwitchViewButton
class SessionItem(object): class Status(unicode):
def __init__(self, name, uri, streams): def __new__(cls, value, color='black'):
instance = unicode.__new__(cls, value)
instance.color = color
return instance
class SessionItem(QObject):
implements(IObserver)
activated = pyqtSignal()
deactivated = pyqtSignal()
ended = pyqtSignal()
def __init__(self, name, uri, session, audio_stream=None, video_stream=None):
super(SessionItem, self).__init__()
if (audio_stream, video_stream) == (None, None):
raise ValueError('SessionItem must represent at least one audio or video stream')
self.name = name self.name = name
self.uri = uri self.uri = uri
self.streams = streams self.session = session
self.audio_stream = audio_stream
self.video_stream = video_stream
self.widget = Null self.widget = Null
self.conference = None self.conference = None
self.type = None self.type = 'Video' if video_stream else 'Audio'
self.codec_info = '' self.codec_info = ''
self.tls = False self.tls = False
self.srtp = False self.srtp = False
self.duration = timedelta(0)
self.latency = 0 self.latency = 0
self.packet_loss = 0 self.packet_loss = 0
self.status = None
self.active = False
self.timer = QTimer()
self.offer_in_progress = False
self.local_hold = False
self.remote_hold = False
self.terminated = False
self.outbound_ringtone = Null
if self.audio_stream is None:
self.hold_tone = Null
from blink import Blink
self.remote_party_name = None
for contact in Blink().main_window.contact_model.iter_contacts():
if uri.matches(contact.uri):
self.remote_party_name = contact.name
break
if not self.remote_party_name:
address = '%s@%s' % (uri.user, uri.host)
match = re.match(r'^(?P<number>(\+|00)[1-9][0-9]\d{5,15})@(\d{1,3}\.){3}\d{1,3}$', address)
self.remote_party_name = name or (match.group('number') if match else address)
self.timer.timeout.connect(self._SH_TimerFired)
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=session)
def __reduce__(self): def __reduce__(self):
return (self.__class__, (self.name, self.uri, self.streams), None) return (self.__class__, (self.name, self.uri, Null, Null, Null), None)
@property
def pending_removal(self):
return self.audio_stream is None and self.video_stream is None
def _get_audio_stream(self):
return self.__dict__['audio_stream']
def _set_audio_stream(self, stream):
notification_center = NotificationCenter()
old_stream = self.__dict__.get('audio_stream', None)
self.__dict__['audio_stream'] = stream
if old_stream is not None:
notification_center.remove_observer(self, sender=old_stream)
self.hold_tone = Null
if stream is not None:
notification_center.add_observer(self, sender=stream)
self.hold_tone = WavePlayer(stream.bridge.mixer, Resources.get('sounds/hold_tone.wav'), loop_count=0, pause_time=45, volume=30)
stream.bridge.add(self.hold_tone)
audio_stream = property(_get_audio_stream, _set_audio_stream)
del _get_audio_stream, _set_audio_stream
def _get_video_stream(self):
return self.__dict__['video_stream']
def _set_video_stream(self, stream):
notification_center = NotificationCenter()
old_stream = self.__dict__.get('video_stream', None)
self.__dict__['video_stream'] = stream
if old_stream is not None:
notification_center.remove_observer(self, sender=old_stream)
if stream is not None:
notification_center.add_observer(self, sender=stream)
video_stream = property(_get_video_stream, _set_video_stream)
del _get_video_stream, _set_video_stream
def _get_conference(self): def _get_conference(self):
return self.__dict__['conference'] return self.__dict__['conference']
...@@ -41,11 +141,13 @@ class SessionItem(object): ...@@ -41,11 +141,13 @@ class SessionItem(object):
old_conference = self.__dict__.get('conference', Null) old_conference = self.__dict__.get('conference', Null)
if old_conference is conference: if old_conference is conference:
return return
self.__dict__['conference'] = conference
if old_conference is not None: if old_conference is not None:
old_conference.remove_session(self) old_conference.remove_session(self)
if conference is not None: if conference is not None:
conference.add_session(self) conference.add_session(self)
self.__dict__['conference'] = conference elif self.widget.mute_button.isChecked():
self.widget.mute_button.click()
conference = property(_get_conference, _set_conference) conference = property(_get_conference, _set_conference)
del _get_conference, _set_conference del _get_conference, _set_conference
...@@ -98,6 +200,18 @@ class SessionItem(object): ...@@ -98,6 +200,18 @@ class SessionItem(object):
srtp = property(_get_srtp, _set_srtp) srtp = property(_get_srtp, _set_srtp)
del _get_srtp, _set_srtp del _get_srtp, _set_srtp
def _get_duration(self):
return self.__dict__['duration']
def _set_duration(self, value):
if self.__dict__.get('duration', None) == value:
return
self.__dict__['duration'] = value
self.widget.duration_label.value = value
duration = property(_get_duration, _set_duration)
del _get_duration, _set_duration
def _get_latency(self): def _get_latency(self):
return self.__dict__['latency'] return self.__dict__['latency']
...@@ -122,10 +236,400 @@ class SessionItem(object): ...@@ -122,10 +236,400 @@ class SessionItem(object):
packet_loss = property(_get_packet_loss, _set_packet_loss) packet_loss = property(_get_packet_loss, _set_packet_loss)
del _get_packet_loss, _set_packet_loss del _get_packet_loss, _set_packet_loss
def _get_status(self):
return self.__dict__['status']
def _set_status(self, value):
if self.__dict__.get('status', Null) == value:
return
self.__dict__['status'] = value
self.widget.status_label.value = value
status = property(_get_status, _set_status)
del _get_status, _set_status
def _get_active(self):
return self.__dict__['active']
def _set_active(self, value):
value = bool(value)
if self.__dict__.get('active', None) == value:
return
self.__dict__['active'] = value
if self.audio_stream:
self.audio_stream.device.output_muted = not value
if value:
self.activated.emit()
else:
self.deactivated.emit()
active = property(_get_active, _set_active)
del _get_active, _set_active
def _get_widget(self):
return self.__dict__['widget']
def _set_widget(self, widget):
old_widget = self.__dict__.get('widget', Null)
self.__dict__['widget'] = widget
if old_widget is not Null:
old_widget.mute_button.clicked.disconnect(self._SH_MuteButtonClicked)
old_widget.hold_button.clicked.disconnect(self._SH_HoldButtonClicked)
old_widget.record_button.clicked.disconnect(self._SH_RecordButtonClicked)
old_widget.hangup_button.clicked.disconnect(self._SH_HangupButtonClicked)
widget.mute_button.setEnabled(old_widget.mute_button.isEnabled())
widget.mute_button.setChecked(old_widget.mute_button.isChecked())
widget.hold_button.setEnabled(old_widget.hold_button.isEnabled())
widget.hold_button.setChecked(old_widget.hold_button.isChecked())
widget.record_button.setEnabled(old_widget.record_button.isEnabled())
widget.record_button.setChecked(old_widget.record_button.isChecked())
widget.hangup_button.setEnabled(old_widget.hangup_button.isEnabled())
widget.mute_button.clicked.connect(self._SH_MuteButtonClicked)
widget.hold_button.clicked.connect(self._SH_HoldButtonClicked)
widget.record_button.clicked.connect(self._SH_RecordButtonClicked)
widget.hangup_button.clicked.connect(self._SH_HangupButtonClicked)
widget = property(_get_widget, _set_widget)
del _get_widget, _set_widget
def connect(self):
self.offer_in_progress = True
account = self.session.account
settings = SIPSimpleSettings()
if isinstance(account, Account) and account.sip.outbound_proxy is not None:
proxy = account.sip.outbound_proxy
uri = SIPURI(host=proxy.host, port=proxy.port, parameters={'transport': proxy.transport})
else:
uri = self.uri
self.status = Status('Looking up destination')
lookup = DNSLookup()
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=lookup)
lookup.lookup_sip_proxy(uri, settings.sip.transport_list)
def hold(self):
if not self.pending_removal and not self.local_hold:
self.local_hold = True
self.session.hold()
self.hold_tone.start()
self.widget.hold_button.setChecked(True)
if not self.offer_in_progress:
self.status = Status('On hold', color='#000090')
def unhold(self):
if not self.pending_removal and self.local_hold:
self.local_hold = False
self.widget.hold_button.setChecked(False)
self.session.unhold()
def send_dtmf(self, digit):
if self.audio_stream is not None:
try:
self.audio_stream.send_dtmf(digit)
except RuntimeError:
pass
else:
digit_map = {'*': 'star'}
filename = 'sounds/dtmf_%s_tone.wav' % digit_map.get(digit, digit)
player = WavePlayer(SIPApplication.voice_audio_bridge.mixer, Resources.get(filename))
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=player)
SIPApplication.voice_audio_bridge.add(player)
player.start()
def end(self):
if self.session.state is None:
self.audio_stream = None
self.video_stream = None
self.status = Status('Call canceled', color='#900000')
self._cleanup()
else:
self.session.end()
def _cleanup(self):
self.timer.stop()
self.widget.mute_button.setEnabled(False)
self.widget.hold_button.setEnabled(False)
self.widget.record_button.setEnabled(False)
self.widget.hangup_button.setEnabled(False)
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=self.session)
player = WavePlayer(SIPApplication.voice_audio_bridge.mixer, Resources.get('sounds/hangup_tone.wav'), volume=60)
notification_center.add_observer(self, sender=player)
SIPApplication.voice_audio_bridge.add(player)
player.start()
self.ended.emit()
def _reset_status(self):
if self.pending_removal or self.offer_in_progress:
return
if self.local_hold:
self.status = Status('On hold', color='#000090')
elif self.remote_hold:
self.status = Status('Hold by remote', color='#000090')
else:
self.status = None
def _SH_HangupButtonClicked(self):
self.end()
def _SH_HoldButtonClicked(self, checked):
if checked:
self.hold()
else:
self.unhold()
def _SH_MuteButtonClicked(self, checked):
if self.audio_stream is not None:
self.audio_stream.muted = checked
def _SH_RecordButtonClicked(self, checked):
if self.audio_stream is not None:
if checked:
settings = SIPSimpleSettings()
direction = self.session.direction
remote = "%s@%s" % (self.session.remote_identity.uri.user, self.session.remote_identity.uri.host)
filename = "%s-%s-%s.wav" % (datetime.now().strftime("%Y%m%d-%H%M%S"), remote, direction)
path = os.path.join(settings.audio.recordings_directory.normalized, self.session.account.id)
try:
self.audio_stream.start_recording(os.path.join(path, filename))
except (SIPCoreError, IOError, OSError), e:
print 'Failed to record: %s' % e
else:
self.audio_stream.stop_recording()
def _SH_TimerFired(self):
stats = self.video_stream.statistics if self.video_stream else self.audio_stream.statistics
self.latency = stats['rtt']['avg'] / 1000
self.packet_loss = int(stats['rx']['packets_lost']*100.0/stats['rx']['packets']) if stats['rx']['packets'] else 0
self.duration += timedelta(seconds=1)
@run_in_gui_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_AudioStreamGotDTMF(self, notification):
digit_map = {'*': 'star'}
filename = 'sounds/dtmf_%s_tone.wav' % digit_map.get(notification.data.digit, notification.data.digit)
player = WavePlayer(SIPApplication.voice_audio_bridge.mixer, Resources.get(filename))
notification_center = NotificationCenter()
notification_center.add_observer(self, sender=player)
SIPApplication.voice_audio_bridge.add(player)
player.start()
def _NH_AudioStreamDidStartRecordingAudio(self, notification):
self.widget.record_button.setChecked(True)
def _NH_AudioStreamWillStopRecordingAudio(self, notification):
self.widget.record_button.setChecked(False)
def _NH_DNSLookupDidSucceed(self, notification):
settings = SIPSimpleSettings()
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
if self.pending_removal:
return
streams = []
if self.audio_stream:
streams.append(self.audio_stream)
outbound_ringtone = settings.sounds.outbound_ringtone
if outbound_ringtone:
self.outbound_ringtone = WavePlayer(self.audio_stream.mixer, outbound_ringtone.path, outbound_ringtone.volume, loop_count=0, pause_time=5)
self.audio_stream.bridge.add(self.outbound_ringtone)
if self.video_stream:
streams.append(self.video_stream)
self.status = Status('Connecting...')
self.session.connect(ToHeader(self.uri), notification.data.result, streams)
def _NH_DNSLookupDidFail(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
if self.pending_removal:
return
self.audio_stream = None
self.video_stream = None
self.status = Status('Destination not found', color='#900000')
self._cleanup()
def _NH_MediaStreamDidStart(self, notification):
if notification.sender is self.audio_stream:
self.widget.mute_button.setEnabled(True)
self.widget.hold_button.setEnabled(True)
self.widget.record_button.setEnabled(True)
def _NH_SIPSessionGotRingIndication(self, notification):
self.status = Status('Ringing...')
self.outbound_ringtone.start()
def _NH_SIPSessionWillStart(self, notification):
self.outbound_ringtone.stop()
def _NH_SIPSessionDidStart(self, notification):
if self.audio_stream not in notification.data.streams:
self.audio_stream = None
if self.video_stream not in notification.data.streams:
self.video_stream = None
if not self.local_hold:
self.offer_in_progress = False
if not self.pending_removal:
self.timer.start(1000)
self.status = None
if self.video_stream is not None:
self.type = 'HD Video' if self.video_stream.bit_rate/1024 >= 512 else 'Video'
else:
self.type = 'HD Audio' if self.audio_stream.sample_rate/1000 >= 16 else 'Audio'
codecs = []
if self.video_stream is not None:
codecs.append('%s %dkbit' % (self.video_stream.codec, self.video_stream.bit_rate/1024))
if self.audio_stream is not None:
codecs.append('%s %dkHz' % (self.audio_stream.codec, self.audio_stream.sample_rate/1000))
self.codec_info = ', '.join(codecs)
self.status = Status('Connected')
call_later(1, self._reset_status)
else:
self.status = Status('%s refused' % self.type, color='#900000')
self._cleanup()
def _NH_SIPSessionDidFail(self, notification):
self.audio_stream = None
self.video_stream = None
self.offer_in_progress = False
if notification.data.failure_reason == 'user request':
if notification.data.code == 487:
reason = 'Call canceled'
else:
reason = notification.data.reason
else:
reason = notification.data.failure_reason
self.status = Status(reason, color='#900000')
self.outbound_ringtone.stop()
self._cleanup()
def _NH_SIPSessionDidEnd(self, notification):
self.audio_stream = None
self.video_stream = None
self.offer_in_progress = False
self.status = Status('Call ended' if notification.data.originator=='local' else 'Call ended by remote')
self._cleanup()
def _NH_SIPSessionDidChangeHoldState(self, notification):
if notification.data.originator == 'remote':
self.remote_hold = notification.data.on_hold
if self.local_hold:
if not self.offer_in_progress:
self.status = Status('On hold', color='#000090')
elif self.remote_hold:
if not self.offer_in_progress:
self.status = Status('Hold by remote', color='#000090')
self.hold_tone.start()
else:
self.status = None
self.hold_tone.stop()
self.offer_in_progress = False
def _NH_SIPSessionGotAcceptProposal(self, notification):
if self.audio_stream not in notification.data.proposed_streams and self.video_stream not in notification.data.proposed_streams:
return
if self.audio_stream in notification.data.proposed_streams and self.audio_stream not in notification.data.streams:
self.audio_stream = None
if self.video_stream in notification.data.proposed_streams and self.video_stream not in notification.data.streams:
self.video_stream = None
self.offer_in_progress = False
if not self.pending_removal:
if not self.timer.isActive():
self.timer.start()
if self.video_stream is not None:
self.type = 'HD Video' if self.video_stream.bit_rate/1024 >= 512 else 'Video'
else:
self.type = 'HD Audio' if self.audio_stream.sample_rate/1000 >= 16 else 'Audio'
codecs = []
if self.video_stream is not None:
codecs.append('%s %dkbit' % (self.video_stream.codec, self.video_stream.bit_rate/1024))
if self.audio_stream is not None:
codecs.append('%s %dkHz' % (self.audio_stream.codec, self.audio_stream.sample_rate/1000))
self.codec_info = ', '.join(codecs)
self.status = Status('Connected')
call_later(1, self._reset_status)
else:
self.status = Status('%s refused' % self.type, color='#900000')
self._cleanup()
def _NH_SIPSessionGotRejectProposal(self, notification):
if self.audio_stream not in notification.data.streams and self.video_stream not in notification.data.streams:
return
if self.audio_stream in notification.data.streams:
self.audio_stream = None
if self.video_stream in notification.data.streams:
video_refused = True
self.video_stream = None
else:
video_refused = False
self.offer_in_progress = False
if not self.pending_removal:
if self.video_stream is not None:
self.type = 'HD Video' if self.video_stream.bit_rate/1024 >= 512 else 'Video'
else:
self.type = 'HD Audio' if self.audio_stream.sample_rate/1000 >= 16 else 'Audio'
codecs = []
if self.video_stream is not None:
codecs.append('%s %dkbit' % (self.video_stream.codec, self.video_stream.bit_rate/1024))
if self.audio_stream is not None:
codecs.append('%s %dkHz' % (self.audio_stream.codec, self.audio_stream.sample_rate/1000))
self.codec_info = ', '.join(codecs)
self.status = Status('Video refused' if video_refused else 'Audio refused', color='#900000')
call_later(1, self._reset_status)
else:
self.status = Status('%s refused' % self.type, color='#900000')
self._cleanup()
def _NH_SIPSessionDidRenegotiateStreams(self, notification):
if notification.data.action != 'remove':
return
if self.audio_stream not in notification.data.streams and self.video_stream not in notification.data.streams:
return
if self.audio_stream in notification.data.streams:
self.audio_stream = None
if self.video_stream in notification.data.streams:
video_removed = True
self.video_stream = None
else:
video_removed = False
self.offer_in_progress = False
if not self.pending_removal:
if self.video_stream is not None:
self.type = 'HD Video' if self.video_stream.bit_rate/1024 >= 512 else 'Video'
else:
self.type = 'HD Audio' if self.audio_stream.sample_rate/1000 >= 16 else 'Audio'
codecs = []
if self.video_stream is not None:
codecs.append('%s %dkbit' % (self.video_stream.codec, self.video_stream.bit_rate/1024))
if self.audio_stream is not None:
codecs.append('%s %dkHz' % (self.audio_stream.codec, self.audio_stream.sample_rate/1000))
self.codec_info = ', '.join(codecs)
self.status = Status('Video removed' if video_removed else 'Audio removed', color='#900000')
call_later(1, self._reset_status)
else:
self.status = Status('%s removed' % self.type, color='#900000')
self._cleanup()
def _NH_WavePlayerDidFail(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
def _NH_WavePlayerDidEnd(self, notification):
notification_center = NotificationCenter()
notification_center.remove_observer(self, sender=notification.sender)
class Conference(object): class Conference(object):
def __init__(self): def __init__(self):
self.sessions = [] self.sessions = []
self.audio_conference = AudioConference()
self.audio_conference.hold()
def add_session(self, session): def add_session(self, session):
if self.sessions: if self.sessions:
...@@ -135,6 +639,9 @@ class Conference(object): ...@@ -135,6 +639,9 @@ class Conference(object):
session.widget.conference_position = None session.widget.conference_position = None
session.widget.mute_button.show() session.widget.mute_button.show()
self.sessions.append(session) self.sessions.append(session)
if session.audio_stream is not None:
self.audio_conference.add(session.audio_stream)
session.unhold()
def remove_session(self, session): def remove_session(self, session):
session.widget.conference_position = None session.widget.conference_position = None
...@@ -149,6 +656,16 @@ class Conference(object): ...@@ -149,6 +656,16 @@ class Conference(object):
self.sessions[-1].widget.conference_position = Bottom self.sessions[-1].widget.conference_position = Bottom
for sessions in self.sessions[1:-1]: for sessions in self.sessions[1:-1]:
session.widget.conference_position = Middle session.widget.conference_position = Middle
if not session.active:
session.hold()
if session.audio_stream is not None:
self.audio_conference.remove(session.audio_stream)
def hold(self):
self.audio_conference.hold()
def unhold(self):
self.audio_conference.unhold()
# Positions for sessions in conferences. # Positions for sessions in conferences.
...@@ -183,7 +700,6 @@ class SessionWidget(base_class, ui_class): ...@@ -183,7 +700,6 @@ class SessionWidget(base_class, ui_class):
self.drop_indicator = False self.drop_indicator = False
self.conference_position = None self.conference_position = None
self._disable_dnd = False self._disable_dnd = False
self.setFocusProxy(parent)
self.mute_button.hidden.connect(self._mute_button_hidden) self.mute_button.hidden.connect(self._mute_button_hidden)
self.mute_button.shown.connect(self._mute_button_shown) self.mute_button.shown.connect(self._mute_button_shown)
self.mute_button.pressed.connect(self._tool_button_pressed) self.mute_button.pressed.connect(self._tool_button_pressed)
...@@ -191,11 +707,17 @@ class SessionWidget(base_class, ui_class): ...@@ -191,11 +707,17 @@ class SessionWidget(base_class, ui_class):
self.record_button.pressed.connect(self._tool_button_pressed) self.record_button.pressed.connect(self._tool_button_pressed)
self.hangup_button.pressed.connect(self._tool_button_pressed) self.hangup_button.pressed.connect(self._tool_button_pressed)
self.mute_button.hide() self.mute_button.hide()
self.address_label.setText(session.name or session.uri) self.mute_button.setEnabled(False)
self.hold_button.setEnabled(False)
self.record_button.setEnabled(False)
self.address_label.setText(session.remote_party_name)
self.stream_info_label.session_type = session.type self.stream_info_label.session_type = session.type
self.stream_info_label.codec_info = session.codec_info self.stream_info_label.codec_info = session.codec_info
self.duration_label.value = session.duration
self.latency_label.value = session.latency self.latency_label.value = session.latency
self.packet_loss_label.threshold = 0
self.packet_loss_label.value = session.packet_loss self.packet_loss_label.value = session.packet_loss
self.status_label.value = session.status
self.tls_label.setVisible(bool(session.tls)) self.tls_label.setVisible(bool(session.tls))
self.srtp_label.setVisible(bool(session.srtp)) self.srtp_label.setVisible(bool(session.srtp))
...@@ -360,7 +882,7 @@ class DraggedSessionWidget(base_class, ui_class): ...@@ -360,7 +882,7 @@ class DraggedSessionWidget(base_class, ui_class):
self.status_label.setText(u'Drop outside the conference to detach') self.status_label.setText(u'Drop outside the conference to detach')
else: else:
self.status_label.setText(u'Drop over a session to conference them') self.status_label.setText(u'Drop over a session to conference them')
self.status_label.show()
def paintEvent(self, event): def paintEvent(self, event):
painter = QPainter(self) painter = QPainter(self)
...@@ -402,6 +924,7 @@ class SessionDelegate(QStyledItemDelegate): ...@@ -402,6 +924,7 @@ class SessionDelegate(QStyledItemDelegate):
def createEditor(self, parent, options, index): def createEditor(self, parent, options, index):
session = index.model().data(index, Qt.DisplayRole) session = index.model().data(index, Qt.DisplayRole)
session.widget = SessionWidget(session, parent) session.widget = SessionWidget(session, parent)
session.widget.hold_button.clicked.connect(partial(self._SH_HoldButtonClicked, session))
return session.widget return session.widget
def updateEditorGeometry(self, editor, option, index): def updateEditorGeometry(self, editor, option, index):
...@@ -417,10 +940,18 @@ class SessionDelegate(QStyledItemDelegate): ...@@ -417,10 +940,18 @@ class SessionDelegate(QStyledItemDelegate):
def sizeHint(self, option, index): def sizeHint(self, option, index):
return self.size_hint return self.size_hint
def _SH_HoldButtonClicked(self, session, checked):
if session.conference is None and not session.active and not checked:
session_list = self.parent()
model = session_list.model()
selection_model = session_list.selectionModel()
selection_model.select(model.index(model.sessions.index(session)), selection_model.ClearAndSelect)
class SessionModel(QAbstractListModel): class SessionModel(QAbstractListModel):
sessionAdded = pyqtSignal(SessionItem) sessionAdded = pyqtSignal(SessionItem)
sessionRemoved = pyqtSignal(SessionItem) sessionRemoved = pyqtSignal(SessionItem)
structureChanged = pyqtSignal()
# The MIME types we accept in drop operations, in the order they should be handled # The MIME types we accept in drop operations, in the order they should be handled
accepted_mime_types = ['application/x-blink-session-list', 'application/x-blink-contact-list'] accepted_mime_types = ['application/x-blink-session-list', 'application/x-blink-contact-list']
...@@ -430,6 +961,7 @@ class SessionModel(QAbstractListModel): ...@@ -430,6 +961,7 @@ class SessionModel(QAbstractListModel):
self.sessions = [] self.sessions = []
self.main_window = parent self.main_window = parent
self.session_list = parent.session_list self.session_list = parent.session_list
self.ignore_selection_changes = False
def flags(self, index): def flags(self, index):
if index.isValid(): if index.isValid():
...@@ -480,12 +1012,11 @@ class SessionModel(QAbstractListModel): ...@@ -480,12 +1012,11 @@ class SessionModel(QAbstractListModel):
selection_model = session_list.selectionModel() selection_model = session_list.selectionModel()
selection_mode = session_list.selectionMode() selection_mode = session_list.selectionMode()
session_list.setSelectionMode(session_list.NoSelection) session_list.setSelectionMode(session_list.NoSelection)
source = self.session_list.dragged_session self.ignore_selection_changes = True
source = session_list.dragged_session
target = self.sessions[index.row()] if index.isValid() else None target = self.sessions[index.row()] if index.isValid() else None
if source.conference is None: if source.conference is None:
# the dragged session is not in a conference yet # the dragged session is not in a conference yet
if target is None:
return False
source_selected = source.widget.selected source_selected = source.widget.selected
target_selected = target.widget.selected target_selected = target.widget.selected
if target.conference is not None: if target.conference is not None:
...@@ -518,13 +1049,16 @@ class SessionModel(QAbstractListModel): ...@@ -518,13 +1049,16 @@ class SessionModel(QAbstractListModel):
last.conference = conference last.conference = conference
if source_selected: if source_selected:
selection_model.select(self.index(self.sessions.index(source)), selection_model.Select) selection_model.select(self.index(self.sessions.index(source)), selection_model.Select)
conference.unhold()
elif target_selected: elif target_selected:
selection_model.select(self.index(self.sessions.index(target)), selection_model.Select) selection_model.select(self.index(self.sessions.index(target)), selection_model.Select)
conference.unhold()
session_list.scrollToTop() session_list.scrollToTop()
active = source.active or target.active
for session in source.conference.sessions:
session.active = active
else: else:
# the dragged session is in a conference # the dragged session is in a conference
if target is not None and target.conference is source.conference:
return False
conference = source.conference conference = source.conference
if len(conference.sessions) == 2: if len(conference.sessions) == 2:
conference_selected = source.widget.selected conference_selected = source.widget.selected
...@@ -537,7 +1071,7 @@ class SessionModel(QAbstractListModel): ...@@ -537,7 +1071,7 @@ class SessionModel(QAbstractListModel):
self._add_session(first) self._add_session(first)
self._add_session(last) self._add_session(last)
if conference_selected: if conference_selected:
selection_model.select(self.index(self.sessions.index(sibling)), selection_model.ClearAndSelect) selection_model.select(self.index(self.sessions.index(sibling)), selection_model.Select)
session_list.scrollToBottom() session_list.scrollToBottom()
else: else:
selected_index = selection_model.selectedIndexes()[0] selected_index = selection_model.selectedIndexes()[0]
...@@ -549,18 +1083,28 @@ class SessionModel(QAbstractListModel): ...@@ -549,18 +1083,28 @@ class SessionModel(QAbstractListModel):
self._add_session(source) self._add_session(source)
position = self.sessions.index(conference.sessions[0]) position = self.sessions.index(conference.sessions[0])
session_list.scrollTo(self.index(position), session_list.PositionAtCenter) session_list.scrollTo(self.index(position), session_list.PositionAtCenter)
source.active = False
self.ignore_selection_changes = False
session_list.setSelectionMode(selection_mode) session_list.setSelectionMode(selection_mode)
self.structureChanged.emit()
return True return True
def _DH_ApplicationXBlinkContactList(self, mime_data, action, index): def _DH_ApplicationXBlinkContactList(self, mime_data, action, index):
return False if not index.isValid():
return
session = self.sessions[index.row()]
contacts = pickle.loads(str(mime_data.data('application/x-blink-contact-list')))
session_manager = SessionManager()
for contact in contacts:
session_manager.start_call(contact.name, contact.uri, conference_sibling=session)
return True
def _add_session(self, session): def _add_session(self, session):
position = len(self.sessions) position = len(self.sessions)
self.beginInsertRows(QModelIndex(), position, position) self.beginInsertRows(QModelIndex(), position, position)
self.sessions.append(session) self.sessions.append(session)
self.session_list.openPersistentEditor(self.index(position))
self.endInsertRows() self.endInsertRows()
self.session_list.openPersistentEditor(self.index(position))
def _remove_session(self, session): def _remove_session(self, session):
position = self.sessions.index(session) position = self.sessions.index(session)
...@@ -572,45 +1116,111 @@ class SessionModel(QAbstractListModel): ...@@ -572,45 +1116,111 @@ class SessionModel(QAbstractListModel):
if session in self.sessions: if session in self.sessions:
return return
self._add_session(session) self._add_session(session)
session.ended.connect(self.structureChanged.emit)
self.sessionAdded.emit(session)
self.structureChanged.emit()
def addSessionAndConference(self, session, sibling):
if session in self.sessions:
return
if sibling not in self.sessions:
raise ValueError('sibling %r not in sessions list' % sibling)
self.ignore_selection_changes = True
session_list = self.session_list
selection_model = session_list.selectionModel()
selection_mode = session_list.selectionMode()
session_list.setSelectionMode(session_list.NoSelection)
sibling_selected = sibling.widget.selected
if sibling.conference is not None:
position = self.sessions.index(sibling.conference.sessions[-1]) + 1
self.beginInsertRows(QModelIndex(), position, position)
self.sessions.insert(position, session)
self.endInsertRows()
session_list.openPersistentEditor(self.index(position))
session.conference = sibling.conference
if sibling_selected:
session.widget.selected = True
session_list.scrollTo(self.index(position), session_list.EnsureVisible) # or PositionAtBottom
else:
self._remove_session(sibling)
self.beginInsertRows(QModelIndex(), 0, 1)
self.sessions[0:0] = [sibling, session]
self.endInsertRows()
session_list.openPersistentEditor(self.index(0))
session_list.openPersistentEditor(self.index(1))
conference = Conference()
sibling.conference = conference
session.conference = conference
if sibling_selected:
selection_model.select(self.index(self.sessions.index(sibling)), selection_model.Select)
conference.unhold()
session_list.scrollToTop()
session.active = sibling.active
session_list.setSelectionMode(selection_mode)
self.ignore_selection_changes = False
session.ended.connect(self.structureChanged.emit)
self.sessionAdded.emit(session) self.sessionAdded.emit(session)
self.structureChanged.emit()
def removeSession(self, session): def removeSession(self, session):
if session not in self.sessions: if session not in self.sessions:
return return
self._remove_session(session) self._remove_session(session)
if session.conference is not None:
if len(session.conference.sessions) == 2:
first, last = session.conference.sessions
first.conference = None
last.conference = None
else:
session.conference = None
self.sessionRemoved.emit(session) self.sessionRemoved.emit(session)
self.structureChanged.emit()
def test(self): def conferenceSessions(self, sessions):
self.addSession(SessionItem('Dan Pascu', 'dan@umts.ro', [])) self.ignore_selection_changes = True
self.addSession(SessionItem('Lucian Stanescu', 'luci@umts.ro', [])) session_list = self.session_list
self.addSession(SessionItem('Adrian Georgescu', 'adi@umts.ro', [])) selection_model = session_list.selectionModel()
self.addSession(SessionItem('Saul Ibarra', 'saul@umts.ro', [])) selection_mode = session_list.selectionMode()
self.addSession(SessionItem('Tijmen de Mes', 'tijmen@umts.ro', [])) session_list.setSelectionMode(session_list.NoSelection)
self.addSession(SessionItem('Test Call', '3333@umts.ro', [])) selected = any(session.widget.selected for session in sessions)
conference = Conference() selected_session = self.data(selection_model.selectedIndexes()[0]) if selected else None
self.sessions[0].conference = conference for session in sessions:
self.sessions[1].conference = conference self._remove_session(session)
self.beginInsertRows(QModelIndex(), 0, len(sessions)-1)
self.sessions[0:0] = sessions
self.endInsertRows()
for row in xrange(len(sessions)):
session_list.openPersistentEditor(self.index(row))
conference = Conference() conference = Conference()
self.sessions[2].conference = conference for session in sessions:
self.sessions[3].conference = conference session.conference = conference
session = self.sessions[0] session.active = selected
session.type, session.codec_info = 'HD Audio', 'speex 32kHz' if selected_session is not None:
session.tls, session.srtp, session.latency, session.packet_loss = True, True, 100, 20 selection_model.select(self.index(self.sessions.index(selected_session)), selection_model.Select)
session = self.sessions[1] conference.unhold()
session.type, session.codec_info = 'HD Audio', 'speex 32kHz' session_list.scrollToTop()
session.tls, session.srtp, session.latency, session.packet_loss = True, True, 80, 20 session_list.setSelectionMode(selection_mode)
session = self.sessions[2] self.ignore_selection_changes = False
session.type, session.codec_info = 'HD Audio', 'speex 32kHz' self.structureChanged.emit()
session.tls, session.srtp, session.latency, session.packet_loss = True, False, 150, 0
session = self.sessions[3] def breakConference(self, conference):
session.type, session.codec_info = 'HD Audio', 'speex 32kHz' self.ignore_selection_changes = True
session.tls, session.srtp, session.latency, session.packet_loss = False, False, 180, 20 sessions = [session for session in self.sessions if session.conference is conference]
session = self.sessions[4] session_list = self.session_list
session.type, session.codec_info = 'Video', 'H.264 512kbit, PCM 8kHz' selection_model = session_list.selectionModel()
session.tls, session.srtp, session.latency, session.packet_loss = True, True, 0, 0 selection_mode = session_list.selectionMode()
session = self.sessions[5] session_list.setSelectionMode(session_list.NoSelection)
session.type, session.codec_info = 'Audio', 'PCM 8kHz' active_session = sessions[0]
session.tls, session.srtp, session.latency, session.packet_loss = True, True, 540, 50 for session in sessions:
session.conference = None
self._remove_session(session)
self._add_session(session)
session.active = session is active_session
selection_model.select(self.index(self.sessions.index(active_session)), selection_model.Select)
self.ignore_selection_changes = False
session_list.scrollToBottom()
session_list.setSelectionMode(selection_mode)
self.structureChanged.emit()
class ContextMenuActions(object): class ContextMenuActions(object):
...@@ -647,14 +1257,43 @@ class SessionListView(QListView): ...@@ -647,14 +1257,43 @@ class SessionListView(QListView):
sibling.widget.selected = True sibling.widget.selected = True
else: else:
session.widget.selected = True session.widget.selected = True
if not selected.isEmpty():
self.setCurrentIndex(selected.indexes()[0])
else:
self.setCurrentIndex(model.index(-1))
def contextMenuEvent(self, event): def contextMenuEvent(self, event):
pass pass
def keyPressEvent(self, event):
digit = chr(event.key()) if event.key() < 256 else None
selection_model = self.selectionModel()
selected_indexes = selection_model.selectedIndexes()
if digit is not None and digit in '0123456789ABCD#*' and selected_indexes:
self.model().data(selected_indexes[0]).send_dtmf(digit)
elif event.key() in (Qt.Key_Up, Qt.Key_Down):
current_index = selection_model.currentIndex()
if current_index.isValid():
step = 1 if event.key() == Qt.Key_Down else -1
conference = current_index.data().toPyObject().conference
new_index = current_index.sibling(current_index.row()+step, current_index.column())
while conference is not None and new_index.isValid() and new_index.data().toPyObject().conference is conference:
new_index = new_index.sibling(new_index.row()+step, new_index.column())
if new_index.isValid():
selection_model.select(new_index, selection_model.ClearAndSelect)
else:
super(SessionListView, self).keyPressEvent(event)
def mousePressEvent(self, event): def mousePressEvent(self, event):
self._pressed_position = event.pos() self._pressed_position = event.pos()
self._pressed_index = self.indexAt(self._pressed_position) self._pressed_index = self.indexAt(self._pressed_position)
super(SessionListView, self).mousePressEvent(event) super(SessionListView, self).mousePressEvent(event)
selection_model = self.selectionModel()
selected_indexes = selection_model.selectedIndexes()
if selected_indexes:
selection_model.setCurrentIndex(selected_indexes[0], selection_model.Select)
else:
selection_model.setCurrentIndex(self.model().index(-1), selection_model.Select)
def mouseReleaseEvent(self, event): def mouseReleaseEvent(self, event):
self._pressed_position = None self._pressed_position = None
...@@ -790,7 +1429,8 @@ ui_class, base_class = uic.loadUiType(Resources.get('incoming_dialog.ui')) ...@@ -790,7 +1429,8 @@ ui_class, base_class = uic.loadUiType(Resources.get('incoming_dialog.ui'))
class IncomingDialog(base_class, ui_class): class IncomingDialog(base_class, ui_class):
def __init__(self, parent=None): def __init__(self, parent=None):
super(IncomingDialog, self).__init__(parent) super(IncomingDialog, self).__init__(parent, flags=Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_DeleteOnClose)
with Resources.directory: with Resources.directory:
self.setupUi(self) self.setupUi(self)
font = self.username_label.font() font = self.username_label.font()
...@@ -811,6 +1451,37 @@ class IncomingDialog(base_class, ui_class): ...@@ -811,6 +1451,37 @@ class IncomingDialog(base_class, ui_class):
self.desktopsharing_stream.shown.connect(self.desktopsharing_label.show) self.desktopsharing_stream.shown.connect(self.desktopsharing_label.show)
for stream in self.streams: for stream in self.streams:
stream.hide() stream.hide()
self.position = None
def show(self, activate=True, position=1):
from blink import Blink
blink = Blink()
screen_geometry = blink.desktop().screenGeometry(self)
available_geometry = blink.desktop().availableGeometry(self)
main_window_geometry = blink.main_window.geometry()
main_window_framegeometry = blink.main_window.frameGeometry()
horizontal_decorations = main_window_framegeometry.width() - main_window_geometry.width()
vertical_decorations = main_window_framegeometry.height() - main_window_geometry.height()
width = limit(self.sizeHint().width(), min=self.minimumSize().width(), max=min(self.maximumSize().width(), available_geometry.width()-horizontal_decorations))
height = limit(self.sizeHint().height(), min=self.minimumSize().height(), max=min(self.maximumSize().height(), available_geometry.height()-vertical_decorations))
total_width = width + horizontal_decorations
total_height = height + vertical_decorations
x = limit(screen_geometry.center().x() - total_width/2, min=available_geometry.left(), max=available_geometry.right()-total_width)
if position is None:
y = -1
elif position % 2 == 0:
y = screen_geometry.center().y() + (position-1)*total_height/2
else:
y = screen_geometry.center().y() - position*total_height/2
if available_geometry.top() <= y <= available_geometry.bottom() - total_height:
self.setGeometry(x, y, width, height)
else:
self.resize(width, height)
self.position = position
self.setAttribute(Qt.WA_ShowWithoutActivating, not activate)
super(IncomingDialog, self).show()
@property @property
def streams(self): def streams(self):
...@@ -858,3 +1529,362 @@ class IncomingDialog(base_class, ui_class): ...@@ -858,3 +1529,362 @@ class IncomingDialog(base_class, ui_class):
del ui_class, base_class del ui_class, base_class
class IncomingSession(QObject):
accepted = pyqtSignal()
rejected = pyqtSignal(str)
def __init__(self, dialog, session, proposal=False, audio_stream=None, video_stream=None, chat_stream=None, desktopsharing_stream=None):
super(IncomingSession, self).__init__()
self.dialog = dialog
self.session = session
self.proposal = proposal
self.audio_stream = audio_stream
self.video_stream = video_stream
self.chat_stream = chat_stream
self.desktopsharing_stream = desktopsharing_stream
if proposal:
self.dialog.setWindowTitle(u'Incoming session update')
self.dialog.setWindowIconText(u'Incoming session update')
self.dialog.busy_button.hide()
else:
self.dialog.setWindowTitle(u'Incoming session request')
self.dialog.setWindowIconText(u'Incoming session request')
from blink import Blink
address = u'%s@%s' % (session.remote_identity.uri.user, session.remote_identity.uri.host)
self.dialog.uri_label.setText(address)
for contact in Blink().main_window.contact_model.iter_contacts():
if session.remote_identity.uri.matches(contact.uri):
self.dialog.username_label.setText(contact.name or session.remote_identity.display_name or address)
self.dialog.user_icon.setPixmap(contact.icon)
break
else:
self.dialog.username_label.setText(session.remote_identity.display_name or address)
if self.audio_stream:
self.dialog.audio_stream.show()
if self.video_stream:
self.dialog.video_stream.show()
if self.chat_stream:
self.dialog.chat_stream.accepted = False # Remove when implemented later -Luci
self.dialog.chat_stream.show()
if self.desktopsharing_stream:
if self.desktopsharing_stream.handler.type == 'active':
self.dialog.desktopsharing_label.setText(u'is offering to share his desktop')
else:
self.dialog.desktopsharing_label.setText(u'is asking to share your desktop')
self.dialog.desktopsharing_stream.accepted = False # Remove when implemented later -Luci
self.dialog.desktopsharing_stream.show()
self.dialog.audio_device_label.setText(u'Selected audio device is %s' % SIPApplication.voice_audio_bridge.mixer.real_output_device)
self.dialog.accepted.connect(self._SH_DialogAccepted)
self.dialog.rejected.connect(self._SH_DialogRejected)
@property
def accepted_streams(self):
streams = []
if self.audio_accepted:
streams.append(self.audio_stream)
if self.video_accepted:
streams.append(self.video_stream)
if self.chat_accepted:
streams.append(self.chat_stream)
if self.desktopsharing_accepted:
streams.append(self.desktopsharing_stream)
return streams
@property
def audio_accepted(self):
return self.dialog.audio_stream.in_use and self.dialog.audio_stream.accepted
@property
def video_accepted(self):
return self.dialog.video_stream.in_use and self.dialog.video_stream.accepted
@property
def chat_accepted(self):
return self.dialog.chat_stream.in_use and self.dialog.chat_stream.accepted
@property
def desktopsharing_accepted(self):
return self.dialog.desktopsharing_stream.in_use and self.dialog.desktopsharing_stream.accepted
def _SH_DialogAccepted(self):
self.accepted.emit()
def _SH_DialogRejected(self):
self.rejected.emit(self.dialog.reject_mode)
class SessionManager(object):
__metaclass__ = Singleton
implements(IObserver)
def __init__(self):
self.main_window = None
self.session_model = None
self.incoming_sessions = []
self.dialog_positions = range(1, 100)
def initialize(self, main_window, session_model):
self.main_window = main_window
self.session_model = session_model
session_model.session_list.selectionModel().selectionChanged.connect(self._SH_SessionListSelectionChanged)
notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPSessionNewIncoming')
notification_center.add_observer(self, name='SIPSessionGotProposal')
notification_center.add_observer(self, name='SIPSessionDidFail')
notification_center.add_observer(self, name='SIPSessionGotRejectProposal')
notification_center.add_observer(self, name='SIPSessionDidRenegotiateStreams')
def start_call(self, name, address, account=None, conference_sibling=None, audio=True, video=False):
account_manager = AccountManager()
account = account or account_manager.default_account
if account is None or not account.enabled:
return
try:
remote_uri = self.create_uri(account, address)
except Exception, e:
print 'Invalid URI: %s' % e # Replace with pop-up
else:
session = Session(account)
audio_stream = self.create_stream(account, 'audio') if audio else None
video_stream = self.create_stream(account, 'vidoe') if video else None
session_item = SessionItem(name, remote_uri, session, audio_stream=audio_stream, video_stream=video_stream)
session_item.activated.connect(partial(self._SH_SessionActivated, session_item))
session_item.deactivated.connect(partial(self._SH_SessionDeactivated, session_item))
session_item.ended.connect(partial(self._SH_SessionEnded, session_item))
if conference_sibling is not None:
self.session_model.addSessionAndConference(session_item, conference_sibling)
else:
self.session_model.addSession(session_item)
selection_model = self.session_model.session_list.selectionModel()
selection_model.select(self.session_model.index(self.session_model.rowCount()-1), selection_model.ClearAndSelect)
self.main_window.switch_view_button.view = SwitchViewButton.SessionView
self.session_model.session_list.setFocus()
self.main_window.search_box.update()
session_item.connect()
@staticmethod
def create_stream(account, type):
for cls in MediaStreamRegistry():
if cls.type == type:
return cls(account)
else:
raise ValueError('unknown stream type: %s' % type)
@staticmethod
def create_uri(account, address):
if not address.startswith('sip:') and not address.startswith('sips:'):
address = 'sip:' + address
if '@' not in address:
if isinstance(account, Account):
address = address + '@' + account.id.domain
else:
raise ValueError('SIP address without domain')
return SIPURI.parse(str(address))
def _remove_session(self, session):
session_list = self.session_model.session_list
selection_mode = session_list.selectionMode()
session_list.setSelectionMode(session_list.NoSelection)
if session.conference is not None:
sibling = (s for s in session.conference.sessions if s is not session).next()
session_index = self.session_model.index(self.session_model.sessions.index(session))
sibling_index = self.session_model.index(self.session_model.sessions.index(sibling))
selection_model = session_list.selectionModel()
if selection_model.isSelected(session_index):
selection_model.select(sibling_index, selection_model.ClearAndSelect)
self.session_model.removeSession(session)
session_list.setSelectionMode(selection_mode)
if not self.session_model.rowCount():
self.main_window.switch_view_button.view = SwitchViewButton.ContactView
def _SH_IncomingSessionAccepted(self, incoming_session):
if incoming_session.dialog.position is not None:
bisect.insort_left(self.dialog_positions, incoming_session.dialog.position)
self.incoming_sessions.remove(incoming_session)
session = incoming_session.session
if incoming_session.audio_accepted and incoming_session.video_accepted:
session_item = SessionItem(session.remote_identity.display_name, session.remote_identity.uri, session, audio_stream=incoming_session.audio_stream, video_stream=incoming_session.video_stream)
elif incoming_session.audio_accepted:
try:
session_item = (session_item for session_item in self.session_model.sessions if session_item.session is session and session_item.audio_stream is None and not session_item.pending_removal).next()
session_item.audio_stream = incoming_session.audio_stream
except StopIteration:
session_item = SessionItem(session.remote_identity.display_name, session.remote_identity.uri, session, audio_stream=incoming_session.audio_stream)
elif incoming_session.video_accepted:
try:
session_item = (session_item for session_item in self.session_model.sessions if session_item.session is session and session_item.video_stream is None and not session_item.pending_removal).next()
session_item.video_stream = incoming_session.video_stream
except StopIteration:
session_item = SessionItem(session.remote_identity.display_name, session.remote_identity.uri, session, video_stream=incoming_session.video_stream)
else: # Handle other streams -Luci
if incoming_session.proposal:
session.reject_proposal(488)
else:
session.reject(488)
return
session_item.activated.connect(partial(self._SH_SessionActivated, session_item))
session_item.deactivated.connect(partial(self._SH_SessionDeactivated, session_item))
session_item.ended.connect(partial(self._SH_SessionEnded, session_item))
selection_model = self.session_model.session_list.selectionModel()
if session_item in self.session_model.sessions:
selection_model.select(self.session_model.index(self.session_model.sessions.index(session_item)), selection_model.ClearAndSelect)
else:
self.session_model.addSession(session_item)
selection_model.select(self.session_model.index(self.session_model.rowCount()-1), selection_model.ClearAndSelect)
self.main_window.switch_view_button.view = SwitchViewButton.SessionView
self.session_model.session_list.setFocus()
# Remove when implemented later -Luci
accepted_streams = incoming_session.accepted_streams
if incoming_session.chat_stream in accepted_streams:
accepted_streams.remove(incoming_session.chat_stream)
if incoming_session.desktopsharing_stream in accepted_streams:
accepted_streams.remove(incoming_session.desktopsharing_stream)
if incoming_session.proposal:
session.accept_proposal(accepted_streams)
else:
session.accept(accepted_streams)
self.main_window.activateWindow()
self.main_window.raise_()
def _SH_IncomingSessionRejected(self, incoming_session, mode):
if incoming_session.dialog.position is not None:
bisect.insort_left(self.dialog_positions, incoming_session.dialog.position)
self.incoming_sessions.remove(incoming_session)
if incoming_session.proposal:
incoming_session.session.reject_proposal(488)
elif mode == 'busy':
incoming_session.session.reject(486)
elif mode == 'reject':
incoming_session.session.reject(603)
def _SH_SessionActivated(self, session):
item = session.conference if session.conference is not None else session
item.unhold()
def _SH_SessionDeactivated(self, session):
item = session.conference if session.conference is not None else session
item.hold()
def _SH_SessionEnded(self, session):
call_later(5, self._remove_session, session)
def _SH_SessionListSelectionChanged(self, selected, deselected):
if self.session_model.ignore_selection_changes:
return
selected_indexes = selected.indexes()
deselected_indexes = deselected.indexes()
old_active_session = self.session_model.data(deselected_indexes[0]) if deselected_indexes else Null
new_active_session = self.session_model.data(selected_indexes[0]) if selected_indexes else Null
if old_active_session.conference and old_active_session.conference is not new_active_session.conference:
for session in old_active_session.conference.sessions:
session.active = False
elif old_active_session.conference is None:
old_active_session.active = False
if new_active_session.conference and new_active_session.conference is not old_active_session.conference:
for session in new_active_session.conference.sessions:
session.active = True
elif new_active_session.conference is None:
new_active_session.active = True
@run_in_gui_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_SIPSessionNewIncoming(self, notification):
session = notification.sender
audio_streams = [stream for stream in notification.data.streams if stream.type=='audio']
video_streams = [stream for stream in notification.data.streams if stream.type=='video']
chat_streams = [stream for stream in notification.data.streams if stream.type=='chat']
desktopsharing_streams = [stream for stream in notification.data.streams if stream.type=='desktop-sharing']
filetransfer_streams = [stream for stream in notification.data.streams if stream.type=='file-transfer']
if not audio_streams and not video_streams and not chat_streams and not desktopsharing_streams and not filetransfer_streams:
session.reject(488)
return
if filetransfer_streams and (audio_streams or video_streams or chat_streams or desktopsharing_streams):
session.reject(488)
return
session.send_ring_indication()
if filetransfer_streams:
filetransfer_stream = filetransfer_streams[0]
else:
audio_stream = audio_streams[0] if audio_streams else None
video_stream = video_streams[0] if video_streams else None
chat_stream = chat_streams[0] if chat_streams else None
desktopsharing_stream = desktopsharing_streams[0] if desktopsharing_streams else None
dialog = IncomingDialog()
incoming_session = IncomingSession(dialog, session, proposal=False, audio_stream=audio_stream, video_stream=video_stream, chat_stream=chat_stream, desktopsharing_stream=desktopsharing_stream)
self.incoming_sessions.append(incoming_session)
incoming_session.accepted.connect(partial(self._SH_IncomingSessionAccepted, incoming_session))
incoming_session.rejected.connect(partial(self._SH_IncomingSessionRejected, incoming_session))
from blink import Blink
try:
position = self.dialog_positions.pop(0)
except IndexError:
position = None
incoming_session.dialog.show(activate=Blink().activeWindow() is not None, position=position)
def _NH_SIPSessionGotProposal(self, notification):
session = notification.sender
audio_streams = [stream for stream in notification.data.streams if stream.type=='audio']
video_streams = [stream for stream in notification.data.streams if stream.type=='video']
chat_streams = [stream for stream in notification.data.streams if stream.type=='chat']
desktopsharing_streams = [stream for stream in notification.data.streams if stream.type=='desktop-sharing']
filetransfer_streams = [stream for stream in notification.data.streams if stream.type=='file-transfer']
if not audio_streams and not video_streams and not chat_streams and not desktopsharing_streams and not filetransfer_streams:
session.reject_proposal(488)
return
if filetransfer_streams and (audio_streams or video_streams or chat_streams or desktopsharing_streams):
session.reject_proposal(488)
return
session.send_ring_indication()
if filetransfer_streams:
filetransfer_stream = filetransfer_streams[0]
else:
audio_stream = audio_streams[0] if audio_streams else None
video_stream = video_streams[0] if video_streams else None
chat_stream = chat_streams[0] if chat_streams else None
desktopsharing_stream = desktopsharing_streams[0] if desktopsharing_streams else None
dialog = IncomingDialog()
incoming_session = IncomingSession(dialog, session, proposal=True, audio_stream=audio_stream, video_stream=video_stream, chat_stream=chat_stream, desktopsharing_stream=desktopsharing_stream)
self.incoming_sessions.append(incoming_session)
incoming_session.accepted.connect(partial(self._SH_IncomingSessionAccepted, incoming_session))
incoming_session.rejected.connect(partial(self._SH_IncomingSessionRejected, incoming_session))
from blink import Blink
try:
position = self.dialog_positions.pop(0)
except IndexError:
position = None
incoming_session.dialog.show(activate=Blink().activeWindow() is not None, position=position)
def _NH_SIPSessionDidFail(self, notification):
if notification.data.code != 487:
return
try:
incoming_session = (incoming_session for incoming_session in self.incoming_sessions if incoming_session.session is notification.sender).next()
except StopIteration:
pass
else:
if incoming_session.dialog.position is not None:
bisect.insort_left(self.dialog_positions, incoming_session.dialog.position)
incoming_session.dialog.hide()
self.incoming_sessions.remove(incoming_session)
def _NH_SIPSessionGotRejectProposal(self, notification):
if notification.data.code != 487:
return
try:
incoming_session = (incoming_session for incoming_session in self.incoming_sessions if incoming_session.session is notification.sender).next()
except StopIteration:
pass
else:
if incoming_session.dialog.position is not None:
bisect.insort_left(self.dialog_positions, incoming_session.dialog.position)
incoming_session.dialog.hide()
self.incoming_sessions.remove(incoming_session)
# Copyright (C) 2010 AG Projects. See LICENSE for details. # Copyright (C) 2010 AG Projects. See LICENSE for details.
# #
__all__ = ['QSingleton', 'call_in_gui_thread', 'run_in_gui_thread'] __all__ = ['QSingleton', 'call_in_gui_thread', 'call_later', 'run_in_gui_thread']
from PyQt4.QtCore import QObject from PyQt4.QtCore import QObject, QTimer
from application.python.decorator import decorator, preserve_signature from application.python.decorator import decorator, preserve_signature
from application.python.util import Singleton from application.python.util import Singleton
...@@ -19,6 +19,11 @@ def call_in_gui_thread(function, *args, **kw): ...@@ -19,6 +19,11 @@ def call_in_gui_thread(function, *args, **kw):
blink.postEvent(blink, CallFunctionEvent(function, args, kw)) blink.postEvent(blink, CallFunctionEvent(function, args, kw))
def call_later(interval, function, *args, **kw):
interval = int(interval*1000)
QTimer.singleShot(interval, lambda: function(*args, **kw))
@decorator @decorator
def run_in_gui_thread(func): def run_in_gui_thread(func):
@preserve_signature(func) @preserve_signature(func)
......
# Copyright (c) 2010 AG Projects. See LICENSE for details. # Copyright (c) 2010 AG Projects. See LICENSE for details.
# #
__all__ = ['ToolButton', 'ConferenceButton', 'StreamButton', 'SegmentButton', 'SingleSegment', 'LeftSegment', 'MiddleSegment', 'RightSegment', 'SwitchViewButton'] __all__ = ['ToolButton', 'ConferenceButton', 'StreamButton', 'SegmentButton', 'SingleSegment', 'LeftSegment', 'MiddleSegment', 'RightSegment', 'RecordButton', 'SwitchViewButton']
from PyQt4.QtCore import QTimer, pyqtSignal from PyQt4.QtCore import QTimer, pyqtSignal
from PyQt4.QtGui import QAction, QIcon, QPushButton, QStyle, QStyleOptionToolButton, QStylePainter, QToolButton from PyQt4.QtGui import QAction, QIcon, QPushButton, QStyle, QStyleOptionToolButton, QStylePainter, QToolButton
...@@ -202,6 +202,52 @@ class SegmentButton(QToolButton): ...@@ -202,6 +202,52 @@ class SegmentButton(QToolButton):
signal.emit() signal.emit()
class RecordButton(SegmentButton):
def __init__(self, parent=None):
super(RecordButton, self).__init__(parent)
self.timer_id = None
self.toggled.connect(self._SH_Toggled)
self.animation_icons = []
self.animation_icon_index = 0
def _get_animation_icon_index(self):
return self.__dict__['animation_icon_index']
def _set_animation_icon_index(self, index):
self.__dict__['animation_icon_index'] = index
self.update()
animation_icon_index = property(_get_animation_icon_index, _set_animation_icon_index)
del _get_animation_icon_index, _set_animation_icon_index
def setIcon(self, icon):
super(RecordButton, self).setIcon(icon)
on_icon = QIcon(icon)
off_icon = QIcon(icon)
for size in off_icon.availableSizes(QIcon.Normal, QIcon.On):
pixmap = off_icon.pixmap(size, QIcon.Normal, QIcon.Off)
off_icon.addPixmap(pixmap, QIcon.Normal, QIcon.On)
self.animation_icons = [on_icon, off_icon]
def paintEvent(self, event):
painter = QStylePainter(self)
option = QStyleOptionToolButton()
self.initStyleOption(option)
option.icon = self.animation_icons[self.animation_icon_index]
painter.drawComplexControl(QStyle.CC_ToolButton, option)
def timerEvent(self, event):
self.animation_icon_index = (self.animation_icon_index+1) % len(self.animation_icons)
def _SH_Toggled(self, checked):
if checked:
self.timer_id = self.startTimer(1000)
self.animation_icon_index = 0
else:
self.killTimer(self.timer_id)
self.timer_id = None
class SwitchViewButton(QPushButton): class SwitchViewButton(QPushButton):
ContactView = 0 ContactView = 0
SessionView = 1 SessionView = 1
......
# Copyright (c) 2010 AG Projects. See LICENSE for details. # Copyright (c) 2010 AG Projects. See LICENSE for details.
# #
__all__ = ['IconSelector', 'LatencyLabel', 'PacketLossLabel', 'StreamInfoLabel'] __all__ = ['DurationLabel', 'IconSelector', 'LatencyLabel', 'PacketLossLabel', 'StatusLabel', 'StreamInfoLabel']
import os import os
from datetime import timedelta
from PyQt4.QtCore import Qt from PyQt4.QtCore import Qt
from PyQt4.QtGui import QFileDialog, QFontMetrics, QLabel, QPixmap from PyQt4.QtGui import QColor, QFileDialog, QFontMetrics, QLabel, QPalette, QPixmap
from blink.resources import ApplicationData, Resources from blink.resources import ApplicationData, Resources
from blink.widgets.util import QtDynamicProperty from blink.widgets.util import QtDynamicProperty
...@@ -86,10 +87,29 @@ class StreamInfoLabel(QLabel): ...@@ -86,10 +87,29 @@ class StreamInfoLabel(QLabel):
self.setText(text) self.setText(text)
class DurationLabel(QLabel):
def __init__(self, parent=None):
super(DurationLabel, self).__init__(parent)
self.value = timedelta(0)
def _get_value(self):
return self.__dict__['value']
def _set_value(self, value):
self.__dict__['value'] = value
seconds = value.seconds % 60
minutes = value.seconds // 60 % 60
hours = value.seconds//3600 + value.days*24
self.setText(u'%d:%02d:%02d' % (hours, minutes, seconds))
value = property(_get_value, _set_value)
del _get_value, _set_value
class LatencyLabel(QLabel): class LatencyLabel(QLabel):
def __init__(self, parent=None): def __init__(self, parent=None):
super(LatencyLabel, self).__init__(parent) super(LatencyLabel, self).__init__(parent)
self.treshold = 99 self.threshold = 99
self.value = 0 self.value = 0
def _get_value(self): def _get_value(self):
...@@ -97,7 +117,7 @@ class LatencyLabel(QLabel): ...@@ -97,7 +117,7 @@ class LatencyLabel(QLabel):
def _set_value(self, value): def _set_value(self, value):
self.__dict__['value'] = value self.__dict__['value'] = value
if value > self.treshold: if value > self.threshold:
text = u'Latency %sms' % value text = u'Latency %sms' % value
self.setMinimumWidth(QFontMetrics(self.font()).width(text)) self.setMinimumWidth(QFontMetrics(self.font()).width(text))
self.setText(text) self.setText(text)
...@@ -112,7 +132,7 @@ class LatencyLabel(QLabel): ...@@ -112,7 +132,7 @@ class LatencyLabel(QLabel):
class PacketLossLabel(QLabel): class PacketLossLabel(QLabel):
def __init__(self, parent=None): def __init__(self, parent=None):
super(PacketLossLabel, self).__init__(parent) super(PacketLossLabel, self).__init__(parent)
self.treshold = 0 self.threshold = 0
self.value = 0 self.value = 0
def _get_value(self): def _get_value(self):
...@@ -120,7 +140,7 @@ class PacketLossLabel(QLabel): ...@@ -120,7 +140,7 @@ class PacketLossLabel(QLabel):
def _set_value(self, value): def _set_value(self, value):
self.__dict__['value'] = value self.__dict__['value'] = value
if value > self.treshold: if value > self.threshold:
text = u'Packet loss %s%%' % value text = u'Packet loss %s%%' % value
self.setMinimumWidth(QFontMetrics(self.font()).width(text)) self.setMinimumWidth(QFontMetrics(self.font()).width(text))
self.setText(text) self.setText(text)
...@@ -132,3 +152,28 @@ class PacketLossLabel(QLabel): ...@@ -132,3 +152,28 @@ class PacketLossLabel(QLabel):
del _get_value, _set_value del _get_value, _set_value
class StatusLabel(QLabel):
def __init__(self, parent=None):
super(StatusLabel, self).__init__(parent)
self.value = None
def _get_value(self):
return self.__dict__['value']
def _set_value(self, value):
self.__dict__['value'] = value
if value is not None:
color = QColor(value.color)
palette = self.palette()
palette.setColor(QPalette.WindowText, color)
palette.setColor(QPalette.Text, color)
self.setPalette(palette)
self.setText(unicode(value))
self.show()
else:
self.hide()
value = property(_get_value, _set_value)
del _get_value, _set_value
...@@ -2,9 +2,6 @@ ...@@ -2,9 +2,6 @@
<ui version="4.0"> <ui version="4.0">
<class>Dialog</class> <class>Dialog</class>
<widget class="QDialog" name="Dialog"> <widget class="QDialog" name="Dialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry"> <property name="geometry">
<rect> <rect>
<x>0</x> <x>0</x>
...@@ -28,8 +25,9 @@ ...@@ -28,8 +25,9 @@
<property name="windowTitle"> <property name="windowTitle">
<string>Incoming session request</string> <string>Incoming session request</string>
</property> </property>
<property name="modal"> <property name="windowIcon">
<bool>true</bool> <iconset>
<normaloff>icons/blink48.png</normaloff>icons/blink48.png</iconset>
</property> </property>
<layout class="QVBoxLayout" name="dialog_layout"> <layout class="QVBoxLayout" name="dialog_layout">
<property name="spacing"> <property name="spacing">
......
...@@ -396,7 +396,7 @@ ...@@ -396,7 +396,7 @@
<number>1</number> <number>1</number>
</property> </property>
<item> <item>
<widget class="QLabel" name="duration_label"> <widget class="DurationLabel" name="duration_label">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>38</width> <width>38</width>
...@@ -409,16 +409,13 @@ ...@@ -409,16 +409,13 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="status_label"> <widget class="StatusLabel" name="status_label">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch> <horstretch>0</horstretch>
<verstretch>0</verstretch> <verstretch>0</verstretch>
</sizepolicy> </sizepolicy>
</property> </property>
<property name="text">
<string>On Hold</string>
</property>
</widget> </widget>
</item> </item>
<item> <item>
...@@ -524,7 +521,7 @@ QToolButton:pressed { ...@@ -524,7 +521,7 @@ QToolButton:pressed {
</widget> </widget>
</item> </item>
<item> <item>
<widget class="SegmentButton" name="record_button"> <widget class="RecordButton" name="record_button">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>18</width> <width>18</width>
...@@ -557,7 +554,8 @@ QToolButton:pressed { ...@@ -557,7 +554,8 @@ QToolButton:pressed {
</property> </property>
<property name="icon"> <property name="icon">
<iconset> <iconset>
<normaloff>icons/record.png</normaloff>icons/record.png</iconset> <normaloff>icons/record.png</normaloff>
<normalon>../../../blink-qt/resources/icons/recording.png</normalon>icons/record.png</iconset>
</property> </property>
<property name="iconSize"> <property name="iconSize">
<size> <size>
...@@ -565,6 +563,9 @@ QToolButton:pressed { ...@@ -565,6 +563,9 @@ QToolButton:pressed {
<height>16</height> <height>16</height>
</size> </size>
</property> </property>
<property name="checkable">
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>
...@@ -640,6 +641,21 @@ QToolButton:pressed { ...@@ -640,6 +641,21 @@ QToolButton:pressed {
<extends>QLabel</extends> <extends>QLabel</extends>
<header>blink.widgets.labels</header> <header>blink.widgets.labels</header>
</customwidget> </customwidget>
<customwidget>
<class>DurationLabel</class>
<extends>QLabel</extends>
<header>blink.widgets.labels</header>
</customwidget>
<customwidget>
<class>StatusLabel</class>
<extends>QLabel</extends>
<header>blink.widgets.labels</header>
</customwidget>
<customwidget>
<class>RecordButton</class>
<extends>QToolButton</extends>
<header>blink.widgets.buttons</header>
</customwidget>
</customwidgets> </customwidgets>
<resources/> <resources/>
<connections/> <connections/>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment