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()
......
This diff is collapsed.
This diff is collapsed.
# 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