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
from sipsimple.application import SIPApplication
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.resources import ApplicationData
from blink.sessions import SessionManager
from blink.util import QSingleton, run_in_gui_thread
......@@ -29,6 +32,10 @@ class Blink(QApplication):
self.application = SIPApplication()
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):
from blink.util import call_in_gui_thread as call_later
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
from operator import attrgetter
from zope.interface import implements
from sipsimple.account import BonjourAccount
from sipsimple.account import AccountManager, BonjourAccount
from sipsimple.util import makedirs
from blink.resources import ApplicationData, Resources, IconCache
from blink.sessions import SessionManager
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
......@@ -859,6 +861,12 @@ class ContactModel(QAbstractListModel):
self.deleted_items.append(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):
try:
try:
......@@ -1092,11 +1100,31 @@ class ContactListView(QListView):
menu.addAction(self.actions.delete_item)
menu.addAction(self.actions.undo_last_delete)
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.delete_item.setEnabled(contact.deletable)
self.actions.undo_last_delete.setEnabled(len(model.deleted_items) > 0)
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):
group = ContactGroup("")
model = self.model()
......@@ -1145,6 +1173,8 @@ class ContactListView(QListView):
def _AH_StartAudioCall(self):
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):
contact = self.model().data(self.selectionModel().selectedIndexes()[0])
......@@ -1167,7 +1197,10 @@ class ContactListView(QListView):
for group in self.model().contact_groups:
group.restore_state()
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):
model = self.model()
......@@ -1374,11 +1407,31 @@ class ContactSearchListView(QListView):
menu.addAction(self.actions.delete_item)
menu.addAction(self.actions.undo_last_delete)
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.delete_item.setEnabled(contact.deletable)
self.actions.undo_last_delete.setEnabled(len(source_model.deleted_items) > 0)
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):
model = self.model()
contact = model.data(self.selectionModel().selectedIndexes()[0])
......@@ -1397,6 +1450,8 @@ class ContactSearchListView(QListView):
def _AH_StartAudioCall(self):
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):
contact = self.model().data(self.selectionModel().selectedIndexes()[0])
......@@ -1415,7 +1470,10 @@ class ContactSearchListView(QListView):
def startDrag(self, 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):
model = self.model()
......
......@@ -9,18 +9,25 @@ from PyQt4 import uic
from PyQt4.QtCore import Qt
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.contacts import Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel
from blink.sessions import SessionModel
from blink.contacts import BonjourNeighbour, Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel
from blink.sessions import SessionManager, SessionModel
from blink.resources import Resources
from blink.util import run_in_gui_thread
from blink.widgets.buttons import SwitchViewButton
ui_class, base_class = uic.loadUiType(Resources.get('blink.ui'))
class MainWindow(base_class, ui_class):
implements(IObserver)
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
......@@ -42,7 +49,8 @@ class MainWindow(base_class, ui_class):
self.contact_list.setModel(self.contact_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.contact_model.load()
......@@ -51,41 +59,42 @@ class MainWindow(base_class, ui_class):
self.session_model = SessionModel(self)
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.contacts_view.setCurrentWidget(self.contact_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.contact_model.itemsAdded.connect(self.contact_model_added_items)
self.contact_model.itemsRemoved.connect(self.contact_model_removed_items)
self.search_box.textChanged.connect(self._SH_SearchBoxTextChanged)
self.contact_model.itemsAdded.connect(self._SH_ContactModelAddedItems)
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.add_contact_button.clicked.connect(self.add_contact)
self.add_search_contact_button.clicked.connect(self.add_contact)
self.add_contact_button.clicked.connect(self._SH_AddContactButtonClicked)
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):
model = self.contact_model
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)
self.session_model.sessionAdded.connect(self._SH_SessionModelAddedSession)
self.session_model.structureChanged.connect(self._SH_SessionModelChangedStructure)
self.hangup_all_button.clicked.connect(self._SH_HangupAllButtonClicked)
self.conference_button.makeConference.connect(self._SH_MakeConference)
self.conference_button.breakConference.connect(self._SH_BreakConference)
notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPApplicationWillStart')
def set_user_icon(self, image_file_name):
pixmap = QPixmap(32, 32)
......@@ -109,30 +118,51 @@ class MainWindow(base_class, ui_class):
self.im_session_button.setEnabled(enabled)
self.ds_session_button.setEnabled(enabled)
def set_identity(self, index):
account_manager = AccountManager()
account_manager.default_account = self.identity.itemData(index).toPyObject().account
def _SH_AddContactButtonClicked(self, clicked):
model = self.contact_model
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):
if text:
self.switch_view_button.view = SwitchViewButton.ContactView
self.enable_call_buttons(True)
else:
def _SH_AudioCallButtonClicked(self):
list = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
selected_indexes = list.selectionModel().selectedIndexes()
contact = list.model().data(selected_indexes[0]) if selected_indexes else Null
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()
self.enable_call_buttons(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
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)
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))
def contact_model_added_items(self, items):
def _SH_ContactModelAddedItems(self, items):
if self.search_box.text().isEmpty():
return
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_removed_items(self, items):
def _SH_ContactModelRemovedItems(self, items):
if self.search_box.text().isEmpty():
return
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):
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_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()
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()
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)
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:
active_session = self.session_model.data(selected_indexes[0])
self.conference_button.setChecked(active_session.conference is not None)
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)
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)
@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
......@@ -3,36 +3,136 @@
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 re
from datetime import datetime, timedelta
from functools import partial
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 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.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):
def __init__(self, name, uri, streams):
class Status(unicode):
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.uri = uri
self.streams = streams
self.session = session
self.audio_stream = audio_stream
self.video_stream = video_stream
self.widget = Null
self.conference = None
self.type = None
self.type = 'Video' if video_stream else 'Audio'
self.codec_info = ''
self.tls = False
self.srtp = False
self.duration = timedelta(0)
self.latency = 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):
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):
return self.__dict__['conference']
......@@ -41,11 +141,13 @@ class SessionItem(object):
old_conference = self.__dict__.get('conference', Null)
if old_conference is conference:
return
self.__dict__['conference'] = conference
if old_conference is not None:
old_conference.remove_session(self)
if conference is not None:
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)
del _get_conference, _set_conference
......@@ -98,6 +200,18 @@ class SessionItem(object):
srtp = property(_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):
return self.__dict__['latency']
......@@ -122,10 +236,400 @@ class SessionItem(object):
packet_loss = property(_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):
def __init__(self):
self.sessions = []
self.audio_conference = AudioConference()
self.audio_conference.hold()
def add_session(self, session):
if self.sessions:
......@@ -135,6 +639,9 @@ class Conference(object):
session.widget.conference_position = None
session.widget.mute_button.show()
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):
session.widget.conference_position = None
......@@ -149,6 +656,16 @@ class Conference(object):
self.sessions[-1].widget.conference_position = Bottom
for sessions in self.sessions[1:-1]:
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.
......@@ -183,7 +700,6 @@ class SessionWidget(base_class, ui_class):
self.drop_indicator = False
self.conference_position = None
self._disable_dnd = False
self.setFocusProxy(parent)
self.mute_button.hidden.connect(self._mute_button_hidden)
self.mute_button.shown.connect(self._mute_button_shown)
self.mute_button.pressed.connect(self._tool_button_pressed)
......@@ -191,11 +707,17 @@ class SessionWidget(base_class, ui_class):
self.record_button.pressed.connect(self._tool_button_pressed)
self.hangup_button.pressed.connect(self._tool_button_pressed)
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.codec_info = session.codec_info
self.duration_label.value = session.duration
self.latency_label.value = session.latency
self.packet_loss_label.threshold = 0
self.packet_loss_label.value = session.packet_loss
self.status_label.value = session.status
self.tls_label.setVisible(bool(session.tls))
self.srtp_label.setVisible(bool(session.srtp))
......@@ -360,7 +882,7 @@ class DraggedSessionWidget(base_class, ui_class):
self.status_label.setText(u'Drop outside the conference to detach')
else:
self.status_label.setText(u'Drop over a session to conference them')
self.status_label.show()
def paintEvent(self, event):
painter = QPainter(self)
......@@ -402,6 +924,7 @@ class SessionDelegate(QStyledItemDelegate):
def createEditor(self, parent, options, index):
session = index.model().data(index, Qt.DisplayRole)
session.widget = SessionWidget(session, parent)
session.widget.hold_button.clicked.connect(partial(self._SH_HoldButtonClicked, session))
return session.widget
def updateEditorGeometry(self, editor, option, index):
......@@ -417,10 +940,18 @@ class SessionDelegate(QStyledItemDelegate):
def sizeHint(self, option, index):
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):
sessionAdded = pyqtSignal(SessionItem)
sessionRemoved = pyqtSignal(SessionItem)
structureChanged = pyqtSignal()
# 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']
......@@ -430,6 +961,7 @@ class SessionModel(QAbstractListModel):
self.sessions = []
self.main_window = parent
self.session_list = parent.session_list
self.ignore_selection_changes = False
def flags(self, index):
if index.isValid():
......@@ -480,12 +1012,11 @@ class SessionModel(QAbstractListModel):
selection_model = session_list.selectionModel()
selection_mode = session_list.selectionMode()
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
if source.conference is None:
# the dragged session is not in a conference yet
if target is None:
return False
source_selected = source.widget.selected
target_selected = target.widget.selected
if target.conference is not None:
......@@ -518,13 +1049,16 @@ class SessionModel(QAbstractListModel):
last.conference = conference
if source_selected:
selection_model.select(self.index(self.sessions.index(source)), selection_model.Select)
conference.unhold()
elif target_selected:
selection_model.select(self.index(self.sessions.index(target)), selection_model.Select)
conference.unhold()
session_list.scrollToTop()
active = source.active or target.active
for session in source.conference.sessions:
session.active = active
else:
# the dragged session is in a conference
if target is not None and target.conference is source.conference:
return False
conference = source.conference
if len(conference.sessions) == 2:
conference_selected = source.widget.selected
......@@ -537,7 +1071,7 @@ class SessionModel(QAbstractListModel):
self._add_session(first)
self._add_session(last)
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()
else:
selected_index = selection_model.selectedIndexes()[0]
......@@ -549,18 +1083,28 @@ class SessionModel(QAbstractListModel):
self._add_session(source)
position = self.sessions.index(conference.sessions[0])
session_list.scrollTo(self.index(position), session_list.PositionAtCenter)
source.active = False
self.ignore_selection_changes = False
session_list.setSelectionMode(selection_mode)
self.structureChanged.emit()
return True
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):
position = len(self.sessions)
self.beginInsertRows(QModelIndex(), position, position)
self.sessions.append(session)
self.session_list.openPersistentEditor(self.index(position))
self.endInsertRows()
self.session_list.openPersistentEditor(self.index(position))
def _remove_session(self, session):
position = self.sessions.index(session)
......@@ -572,45 +1116,111 @@ class SessionModel(QAbstractListModel):
if session in self.sessions:
return
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.structureChanged.emit()
def removeSession(self, session):
if session not in self.sessions:
return
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.structureChanged.emit()
def test(self):
self.addSession(SessionItem('Dan Pascu', 'dan@umts.ro', []))
self.addSession(SessionItem('Lucian Stanescu', 'luci@umts.ro', []))
self.addSession(SessionItem('Adrian Georgescu', 'adi@umts.ro', []))
self.addSession(SessionItem('Saul Ibarra', 'saul@umts.ro', []))
self.addSession(SessionItem('Tijmen de Mes', 'tijmen@umts.ro', []))
self.addSession(SessionItem('Test Call', '3333@umts.ro', []))
conference = Conference()
self.sessions[0].conference = conference
self.sessions[1].conference = conference
def conferenceSessions(self, sessions):
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)
selected = any(session.widget.selected for session in sessions)
selected_session = self.data(selection_model.selectedIndexes()[0]) if selected else None
for session in sessions:
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()
self.sessions[2].conference = conference
self.sessions[3].conference = conference
session = self.sessions[0]
session.type, session.codec_info = 'HD Audio', 'speex 32kHz'
session.tls, session.srtp, session.latency, session.packet_loss = True, True, 100, 20
session = self.sessions[1]
session.type, session.codec_info = 'HD Audio', 'speex 32kHz'
session.tls, session.srtp, session.latency, session.packet_loss = True, True, 80, 20
session = self.sessions[2]
session.type, session.codec_info = 'HD Audio', 'speex 32kHz'
session.tls, session.srtp, session.latency, session.packet_loss = True, False, 150, 0
session = self.sessions[3]
session.type, session.codec_info = 'HD Audio', 'speex 32kHz'
session.tls, session.srtp, session.latency, session.packet_loss = False, False, 180, 20
session = self.sessions[4]
session.type, session.codec_info = 'Video', 'H.264 512kbit, PCM 8kHz'
session.tls, session.srtp, session.latency, session.packet_loss = True, True, 0, 0
session = self.sessions[5]
session.type, session.codec_info = 'Audio', 'PCM 8kHz'
session.tls, session.srtp, session.latency, session.packet_loss = True, True, 540, 50
for session in sessions:
session.conference = conference
session.active = selected
if selected_session is not None:
selection_model.select(self.index(self.sessions.index(selected_session)), selection_model.Select)
conference.unhold()
session_list.scrollToTop()
session_list.setSelectionMode(selection_mode)
self.ignore_selection_changes = False
self.structureChanged.emit()
def breakConference(self, conference):
self.ignore_selection_changes = True
sessions = [session for session in self.sessions if session.conference is conference]
session_list = self.session_list
selection_model = session_list.selectionModel()
selection_mode = session_list.selectionMode()
session_list.setSelectionMode(session_list.NoSelection)
active_session = sessions[0]
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):
......@@ -647,14 +1257,43 @@ class SessionListView(QListView):
sibling.widget.selected = True
else:
session.widget.selected = True
if not selected.isEmpty():
self.setCurrentIndex(selected.indexes()[0])
else:
self.setCurrentIndex(model.index(-1))
def contextMenuEvent(self, event):
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):
self._pressed_position = event.pos()
self._pressed_index = self.indexAt(self._pressed_position)
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):
self._pressed_position = None
......@@ -790,7 +1429,8 @@ ui_class, base_class = uic.loadUiType(Resources.get('incoming_dialog.ui'))
class IncomingDialog(base_class, ui_class):
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:
self.setupUi(self)
font = self.username_label.font()
......@@ -811,6 +1451,37 @@ class IncomingDialog(base_class, ui_class):
self.desktopsharing_stream.shown.connect(self.desktopsharing_label.show)
for stream in self.streams:
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
def streams(self):
......@@ -858,3 +1529,362 @@ class IncomingDialog(base_class, ui_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.
#
__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.util import Singleton
......@@ -19,6 +19,11 @@ def call_in_gui_thread(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
def run_in_gui_thread(func):
@preserve_signature(func)
......
# 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.QtGui import QAction, QIcon, QPushButton, QStyle, QStyleOptionToolButton, QStylePainter, QToolButton
......@@ -202,6 +202,52 @@ class SegmentButton(QToolButton):
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):
ContactView = 0
SessionView = 1
......
# Copyright (c) 2010 AG Projects. See LICENSE for details.
#
__all__ = ['IconSelector', 'LatencyLabel', 'PacketLossLabel', 'StreamInfoLabel']
__all__ = ['DurationLabel', 'IconSelector', 'LatencyLabel', 'PacketLossLabel', 'StatusLabel', 'StreamInfoLabel']
import os
from datetime import timedelta
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.widgets.util import QtDynamicProperty
......@@ -86,10 +87,29 @@ class StreamInfoLabel(QLabel):
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):
def __init__(self, parent=None):
super(LatencyLabel, self).__init__(parent)
self.treshold = 99
self.threshold = 99
self.value = 0
def _get_value(self):
......@@ -97,7 +117,7 @@ class LatencyLabel(QLabel):
def _set_value(self, value):
self.__dict__['value'] = value
if value > self.treshold:
if value > self.threshold:
text = u'Latency %sms' % value
self.setMinimumWidth(QFontMetrics(self.font()).width(text))
self.setText(text)
......@@ -112,7 +132,7 @@ class LatencyLabel(QLabel):
class PacketLossLabel(QLabel):
def __init__(self, parent=None):
super(PacketLossLabel, self).__init__(parent)
self.treshold = 0
self.threshold = 0
self.value = 0
def _get_value(self):
......@@ -120,7 +140,7 @@ class PacketLossLabel(QLabel):
def _set_value(self, value):
self.__dict__['value'] = value
if value > self.treshold:
if value > self.threshold:
text = u'Packet loss %s%%' % value
self.setMinimumWidth(QFontMetrics(self.font()).width(text))
self.setText(text)
......@@ -132,3 +152,28 @@ class PacketLossLabel(QLabel):
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 @@
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
......@@ -28,8 +25,9 @@
<property name="windowTitle">
<string>Incoming session request</string>
</property>
<property name="modal">
<bool>true</bool>
<property name="windowIcon">
<iconset>
<normaloff>icons/blink48.png</normaloff>icons/blink48.png</iconset>
</property>
<layout class="QVBoxLayout" name="dialog_layout">
<property name="spacing">
......
......@@ -396,7 +396,7 @@
<number>1</number>
</property>
<item>
<widget class="QLabel" name="duration_label">
<widget class="DurationLabel" name="duration_label">
<property name="minimumSize">
<size>
<width>38</width>
......@@ -409,16 +409,13 @@
</widget>
</item>
<item>
<widget class="QLabel" name="status_label">
<widget class="StatusLabel" name="status_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>On Hold</string>
</property>
</widget>
</item>
<item>
......@@ -524,7 +521,7 @@ QToolButton:pressed {
</widget>
</item>
<item>
<widget class="SegmentButton" name="record_button">
<widget class="RecordButton" name="record_button">
<property name="minimumSize">
<size>
<width>18</width>
......@@ -557,7 +554,8 @@ QToolButton:pressed {
</property>
<property name="icon">
<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 name="iconSize">
<size>
......@@ -565,6 +563,9 @@ QToolButton:pressed {
<height>16</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
......@@ -640,6 +641,21 @@ QToolButton:pressed {
<extends>QLabel</extends>
<header>blink.widgets.labels</header>
</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>
<resources/>
<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