Commit ae271926 authored by Dan Pascu's avatar Dan Pascu

Implemented call transfer

parent 71ffd6d9
......@@ -3030,6 +3030,8 @@ class ContactDetailModel(QAbstractListModel):
class ContactListView(QListView):
implements(IObserver)
def __init__(self, parent=None):
super(ContactListView, self).__init__(parent)
self.setItemDelegate(ContactDelegate(self))
......@@ -3053,9 +3055,14 @@ class ContactListView(QListView):
self.actions.send_files = QAction("Send File(s)...", self, triggered=self._AH_SendFiles)
self.actions.request_screen = QAction("Request Screen", self, triggered=self._AH_RequestScreen)
self.actions.share_my_screen = QAction("Share My Screen", self, triggered=self._AH_ShareMyScreen)
self.actions.transfer_call = QAction("Transfer Active Call", self, triggered=self._AH_TransferCall)
self.drop_indicator_index = QModelIndex()
self.needs_restore = False
self.doubleClicked.connect(self._SH_DoubleClicked) # activated is emitted on single click
notification_center = NotificationCenter()
notification_center.add_observer(self, 'BlinkSessionDidChangeState')
notification_center.add_observer(self, 'BlinkSessionDidRemoveStream')
notification_center.add_observer(self, 'BlinkActiveSessionDidChange')
def selectionChanged(self, selected, deselected):
super(ContactListView, self).selectionChanged(selected, deselected)
......@@ -3125,6 +3132,7 @@ class ContactListView(QListView):
menu.addAction(self.actions.send_files)
menu.addAction(self.actions.request_screen)
menu.addAction(self.actions.share_my_screen)
menu.addAction(self.actions.transfer_call)
menu.addSeparator()
menu.addAction(self.actions.add_group)
menu.addAction(self.actions.add_contact)
......@@ -3133,7 +3141,9 @@ class ContactListView(QListView):
menu.addAction(self.actions.undo_last_delete)
self.actions.undo_last_delete.setText(undo_delete_text)
account_manager = AccountManager()
session_manager = SessionManager()
can_call = account_manager.default_account is not None and contact.uri is not None
can_transfer = contact.uri is not None and session_manager.active_session is not None and session_manager.active_session.state == 'connected'
self.actions.start_audio_call.setEnabled(can_call)
self.actions.start_video_call.setEnabled(can_call)
self.actions.start_chat_session.setEnabled(can_call)
......@@ -3141,6 +3151,7 @@ class ContactListView(QListView):
self.actions.send_files.setEnabled(can_call)
self.actions.request_screen.setEnabled(can_call)
self.actions.share_my_screen.setEnabled(can_call)
self.actions.transfer_call.setEnabled(can_transfer)
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)
......@@ -3385,6 +3396,11 @@ class ContactListView(QListView):
session_manager = SessionManager()
session_manager.create_session(contact, contact.uri, [StreamDescription('screen-sharing', mode='server'), StreamDescription('audio')])
def _AH_TransferCall(self):
contact = self.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
session_manager = SessionManager()
session_manager.active_session.transfer(contact.uri)
def _DH_ApplicationXBlinkGroupList(self, event, index, rect, item):
model = self.model()
groups = model.items[GroupList]
......@@ -3455,8 +3471,39 @@ class ContactListView(QListView):
session_manager = SessionManager()
session_manager.create_session(item, item.uri, item.preferred_media.stream_descriptions, connect=item.preferred_media.autoconnect)
@run_in_gui_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_BlinkSessionDidChangeState(self, notification):
session_manager = SessionManager()
if notification.sender is session_manager.active_session and self.context_menu.isVisible():
selected_items = [index.data(Qt.UserRole) for index in self.selectionModel().selectedIndexes()]
if len(selected_items) == 1 and isinstance(selected_items[0], Contact):
contact = selected_items[0]
self.actions.transfer_call.setEnabled(contact.uri is not None and notification.sender.state == 'connected')
def _NH_BlinkSessionDidRemoveStream(self, notification):
session_manager = SessionManager()
if notification.sender is session_manager.active_session and self.context_menu.isVisible():
selected_items = [index.data(Qt.UserRole) for index in self.selectionModel().selectedIndexes()]
if len(selected_items) == 1 and isinstance(selected_items[0], Contact):
contact = selected_items[0]
self.actions.transfer_call.setEnabled(contact.uri is not None and 'audio' in notification.sender.streams)
def _NH_BlinkActiveSessionDidChange(self, notification):
if self.context_menu.isVisible():
selected_items = [index.data(Qt.UserRole) for index in self.selectionModel().selectedIndexes()]
if len(selected_items) == 1 and isinstance(selected_items[0], Contact):
contact = selected_items[0]
active_session = notification.data.active_session
self.actions.transfer_call.setEnabled(contact.uri is not None and active_session is not None and active_session.state == 'connected')
class ContactSearchListView(QListView):
implements(IObserver)
def __init__(self, parent=None):
super(ContactSearchListView, self).__init__(parent)
self.setItemDelegate(ContactDelegate(self))
......@@ -3478,8 +3525,13 @@ class ContactSearchListView(QListView):
self.actions.send_files = QAction("Send File(s)...", self, triggered=self._AH_SendFiles)
self.actions.request_screen = QAction("Request Screen", self, triggered=self._AH_RequestScreen)
self.actions.share_my_screen = QAction("Share My Screen", self, triggered=self._AH_ShareMyScreen)
self.actions.transfer_call = QAction("Transfer Active Call", self, triggered=self._AH_TransferCall)
self.drop_indicator_index = QModelIndex()
self.doubleClicked.connect(self._SH_DoubleClicked) # activated is emitted on single click
notification_center = NotificationCenter()
notification_center.add_observer(self, 'BlinkSessionDidChangeState')
notification_center.add_observer(self, 'BlinkSessionDidRemoveStream')
notification_center.add_observer(self, 'BlinkActiveSessionDidChange')
def selectionChanged(self, selected, deselected):
super(ContactSearchListView, self).selectionChanged(selected, deselected)
......@@ -3536,13 +3588,16 @@ class ContactSearchListView(QListView):
menu.addAction(self.actions.send_files)
menu.addAction(self.actions.request_screen)
menu.addAction(self.actions.share_my_screen)
menu.addAction(self.actions.transfer_call)
menu.addSeparator()
menu.addAction(self.actions.edit_item)
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()
session_manager = SessionManager()
can_call = account_manager.default_account is not None and contact.uri is not None
can_transfer = contact.uri is not None and session_manager.active_session is not None and session_manager.active_session.state == 'connected'
self.actions.start_audio_call.setEnabled(can_call)
self.actions.start_video_call.setEnabled(can_call)
self.actions.start_chat_session.setEnabled(can_call)
......@@ -3550,6 +3605,7 @@ class ContactSearchListView(QListView):
self.actions.send_files.setEnabled(can_call)
self.actions.request_screen.setEnabled(can_call)
self.actions.share_my_screen.setEnabled(can_call)
self.actions.transfer_call.setEnabled(can_transfer)
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)
......@@ -3730,6 +3786,11 @@ class ContactSearchListView(QListView):
session_manager = SessionManager()
session_manager.create_session(contact, contact.uri, [StreamDescription('screen-sharing', mode='server'), StreamDescription('audio')])
def _AH_TransferCall(self):
contact = self.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
session_manager = SessionManager()
session_manager.active_session.transfer(contact.uri)
def _DH_TextUriList(self, event, index, rect, item):
if index.isValid():
event.accept(rect)
......@@ -3746,8 +3807,39 @@ class ContactSearchListView(QListView):
session_manager = SessionManager()
session_manager.create_session(item, item.uri, item.preferred_media.stream_descriptions, connect=item.preferred_media.autoconnect)
@run_in_gui_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_BlinkSessionDidChangeState(self, notification):
session_manager = SessionManager()
if notification.sender is session_manager.active_session and self.context_menu.isVisible():
selected_items = [index.data(Qt.UserRole) for index in self.selectionModel().selectedIndexes()]
if len(selected_items) == 1 and isinstance(selected_items[0], Contact):
contact = selected_items[0]
self.actions.transfer_call.setEnabled(contact.uri is not None and notification.sender.state == 'connected')
def _NH_BlinkSessionDidRemoveStream(self, notification):
session_manager = SessionManager()
if notification.sender is session_manager.active_session and self.context_menu.isVisible():
selected_items = [index.data(Qt.UserRole) for index in self.selectionModel().selectedIndexes()]
if len(selected_items) == 1 and isinstance(selected_items[0], Contact):
contact = selected_items[0]
self.actions.transfer_call.setEnabled(contact.uri is not None and 'audio' in notification.sender.streams)
def _NH_BlinkActiveSessionDidChange(self, notification):
if self.context_menu.isVisible():
selected_items = [index.data(Qt.UserRole) for index in self.selectionModel().selectedIndexes()]
if len(selected_items) == 1 and isinstance(selected_items[0], Contact):
contact = selected_items[0]
active_session = notification.data.active_session
self.actions.transfer_call.setEnabled(contact.uri is not None and active_session is not None and active_session.state == 'connected')
class ContactDetailView(QListView):
implements(IObserver)
def __init__(self, contact_list):
super(ContactDetailView, self).__init__(contact_list.parent())
palette = self.palette()
......@@ -3778,9 +3870,14 @@ class ContactDetailView(QListView):
self.actions.send_files = QAction("Send File(s)...", self, triggered=self._AH_SendFiles)
self.actions.request_screen = QAction("Request Screen", self, triggered=self._AH_RequestScreen)
self.actions.share_my_screen = QAction("Share My Screen", self, triggered=self._AH_ShareMyScreen)
self.actions.transfer_call = QAction("Transfer Active Call", self, triggered=self._AH_TransferCall)
self.drop_indicator_index = QModelIndex()
self.doubleClicked.connect(self._SH_DoubleClicked) # activated is emitted on single click
contact_list.installEventFilter(self)
notification_center = NotificationCenter()
notification_center.add_observer(self, 'BlinkSessionDidChangeState')
notification_center.add_observer(self, 'BlinkSessionDidRemoveStream')
notification_center.add_observer(self, 'BlinkActiveSessionDidChange')
def setModel(self, model):
old_model = self.model() or Null
......@@ -3812,6 +3909,7 @@ class ContactDetailView(QListView):
def contextMenuEvent(self, event):
account_manager = AccountManager()
session_manager = SessionManager()
model = self.model()
selected_indexes = self.selectionModel().selectedIndexes()
selected_item = selected_indexes[0].data(Qt.UserRole) if selected_indexes else None
......@@ -3825,6 +3923,7 @@ class ContactDetailView(QListView):
menu.addAction(self.actions.send_files)
menu.addAction(self.actions.request_screen)
menu.addAction(self.actions.share_my_screen)
menu.addAction(self.actions.transfer_call)
menu.addSeparator()
if isinstance(selected_item, ContactURI) and model.contact_detail.editable:
menu.addAction(self.actions.make_uri_default)
......@@ -3832,6 +3931,7 @@ class ContactDetailView(QListView):
menu.addAction(self.actions.edit_contact)
menu.addAction(self.actions.delete_contact)
can_call = account_manager.default_account is not None and contact_has_uris
can_transfer = contact_has_uris and session_manager.active_session is not None and session_manager.active_session.state == 'connected'
self.actions.start_audio_call.setEnabled(can_call)
self.actions.start_video_call.setEnabled(can_call)
self.actions.start_chat_session.setEnabled(can_call)
......@@ -3839,6 +3939,7 @@ class ContactDetailView(QListView):
self.actions.send_files.setEnabled(can_call)
self.actions.request_screen.setEnabled(can_call)
self.actions.share_my_screen.setEnabled(can_call)
self.actions.transfer_call.setEnabled(can_transfer)
self.actions.edit_contact.setEnabled(model.contact_detail.editable)
self.actions.delete_contact.setEnabled(model.contact_detail.deletable)
menu.exec_(event.globalPos())
......@@ -4004,6 +4105,17 @@ class ContactDetailView(QListView):
session_manager = SessionManager()
session_manager.create_session(contact, selected_uri, [StreamDescription('screen-sharing', mode='server'), StreamDescription('audio')])
def _AH_TransferCall(self):
contact = self.contact_list.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
selected_indexes = self.selectionModel().selectedIndexes()
item = selected_indexes[0].data(Qt.UserRole) if selected_indexes else None
if isinstance(item, ContactURI):
selected_uri = item.uri
else:
selected_uri = contact.uri
session_manager = SessionManager()
session_manager.active_session.transfer(selected_uri)
def _DH_ApplicationXBlinkSession(self, event, index, rect, item):
event.ignore(rect)
......@@ -4044,6 +4156,29 @@ class ContactDetailView(QListView):
session_manager = SessionManager()
session_manager.create_session(contact, selected_uri, contact.preferred_media.stream_descriptions, connect=contact.preferred_media.autoconnect)
@run_in_gui_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_BlinkSessionDidChangeState(self, notification):
session_manager = SessionManager()
if notification.sender is session_manager.active_session and self.context_menu.isVisible():
contact_has_uris = self.model().rowCount() > 1
self.actions.transfer_call.setEnabled(contact_has_uris and notification.sender.state == 'connected')
def _NH_BlinkSessionDidRemoveStream(self, notification):
session_manager = SessionManager()
if notification.sender is session_manager.active_session and self.context_menu.isVisible():
contact_has_uris = self.model().rowCount() > 1
self.actions.transfer_call.setEnabled(contact_has_uris and 'audio' in notification.sender.streams)
def _NH_BlinkActiveSessionDidChange(self, notification):
if self.context_menu.isVisible():
contact_has_uris = self.model().rowCount() > 1
active_session = notification.data.active_session
self.actions.transfer_call.setEnabled(contact_has_uris and active_session is not None and active_session.state == 'connected')
# The contact editor dialog
#
......
......@@ -54,6 +54,7 @@ class MainWindow(base_class, ui_class):
notification_center.add_observer(self, name='SIPAccountGotPendingWatcher')
notification_center.add_observer(self, name='BlinkSessionNewOutgoing')
notification_center.add_observer(self, name='BlinkSessionDidReinitializeForOutgoing')
notification_center.add_observer(self, name='BlinkSessionTransferNewOutgoing')
notification_center.add_observer(self, name='BlinkFileTransferNewIncoming')
notification_center.add_observer(self, name='BlinkFileTransferNewOutgoing')
notification_center.add_observer(self, sender=AccountManager())
......@@ -907,6 +908,10 @@ class MainWindow(base_class, ui_class):
def _NH_BlinkSessionDidReinitializeForOutgoing(self, notification):
self.search_box.clear()
def _NH_BlinkSessionTransferNewOutgoing(self, notification):
self.search_box.clear()
self.switch_view_button.view = SwitchViewButton.SessionView
def _NH_BlinkFileTransferNewIncoming(self, notification):
self.filetransfer_window.show(activate=QApplication.activeWindow() is not None)
......
......@@ -37,7 +37,7 @@ from sipsimple.configuration.datatypes import Path
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.session import Session, IllegalStateError
from sipsimple.streams import MediaStreamRegistry
from sipsimple.streams.msrp.filetransfer import FileSelector
from sipsimple.streams.msrp.screensharing import ExternalVNCServerHandler, ExternalVNCViewerHandler, ScreenSharingStream
......@@ -470,6 +470,9 @@ class BlinkSession(BlinkSessionBase):
self.remote_hold = False
self.recording = False
self.transfer_state = None
self.transfer_direction = None
self.info = SessionInfo()
self._sibling = None
......@@ -655,6 +658,43 @@ class BlinkSession(BlinkSessionBase):
notification_center.post_notification('BlinkSessionNewOutgoing', sender=self)
notification_center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'session', 'media', 'statistics'}))
def init_transfer(self, sip_session, streams, contact, contact_uri, reinitialize=False):
assert self.state in (None, 'initialized', 'ended')
assert self.contact is None or contact.settings is self.contact.settings
notification_center = NotificationCenter()
if reinitialize:
notification_center.post_notification('BlinkSessionWillReinitialize', sender=self)
self._initialize(reinitialize=True)
else:
self._delete_when_done = len(streams) == 1 and streams[0].type == 'audio'
self.direction = 'outgoing'
self.sip_session = sip_session
self.account = sip_session.account
self.contact = contact
self.contact_uri = contact_uri
self.uri = self._normalize_uri(contact_uri.uri)
self.stream_descriptions = StreamSet(StreamDescription(stream.type) for stream in streams)
self.state = 'initialized'
if reinitialize:
notification_center.post_notification('BlinkSessionDidReinitializeForOutgoing', sender=self)
else:
notification_center.post_notification('BlinkSessionNewOutgoing', sender=self)
self.streams.extend(streams)
self.info._update(self)
notification_center.post_notification('BlinkSessionInfoUpdated', sender=self, data=NotificationData(elements={'session', 'media', 'statistics'}))
self.state = 'connecting/dns_lookup'
notification_center.post_notification('BlinkSessionWillConnect', sender=self, data=NotificationData(sibling=None))
notification_center.post_notification('BlinkSessionConnectionProgress', sender=self, data=NotificationData(stage='dns_lookup'))
self.stream_descriptions = None
self.state = 'connecting'
notification_center.post_notification('BlinkSessionConnectionProgress', sender=self, data=NotificationData(stage='connecting'))
def connect(self):
assert self.direction == 'outgoing' and self.state == 'initialized'
notification_center = NotificationCenter()
......@@ -768,6 +808,15 @@ class BlinkSession(BlinkSessionBase):
self.recording = False
audio_stream.stop_recording()
def transfer(self, contact_uri, replaced_session=None):
if self.state != 'connected':
return
replaced_sip_session = None if replaced_session is None else replaced_session.sip_session
try:
self.sip_session.transfer(self._parse_uri(contact_uri.uri), replaced_session=replaced_sip_session)
except IllegalStateError:
pass
def end(self, delete=False):
if self.state == 'ending':
self._delete_requested = delete
......@@ -981,6 +1030,30 @@ class BlinkSession(BlinkSessionBase):
elif self.streams.types.isdisjoint({'audio', 'video'}):
self.unhold()
def _NH_SIPSessionTransferNewIncoming(self, notification):
self.transfer_state = 'active'
self.transfer_direction = 'incoming'
notification.center.post_notification('BlinkSessionTransferNewIncoming', sender=self, data=notification.data)
def _NH_SIPSessionTransferNewOutgoing(self, notification):
self.transfer_state = 'active'
self.transfer_direction = 'outgoing'
notification.center.post_notification('BlinkSessionTransferNewOutgoing', sender=self, data=notification.data)
def _NH_SIPSessionTransferDidStart(self, notification):
notification.center.post_notification('BlinkSessionTransferDidStart', sender=self, data=notification.data)
def _NH_SIPSessionTransferDidEnd(self, notification):
self.transfer_state = 'completed'
notification.center.post_notification('BlinkSessionTransferDidEnd', sender=self, data=notification.data)
def _NH_SIPSessionTransferDidFail(self, notification):
self.transfer_state = 'failed'
notification.center.post_notification('BlinkSessionTransferDidFail', sender=self, data=notification.data)
def _NH_SIPSessionTransferGotProgress(self, notification):
notification.center.post_notification('BlinkSessionTransferGotProgress', sender=self, data=notification.data)
def _NH_MediaStreamDidStart(self, notification):
stream = notification.sender
audio_stream = self.streams.get('audio')
......@@ -1558,7 +1631,7 @@ class DraggedAudioSessionWidget(base_class, ui_class):
if self.in_conference:
self.note_label.setText(u'Drop outside the conference to detach')
else:
self.note_label.setText(u'Drop on a session to conference them')
self.note_label.setText(u'<p><b>Drop</b>:&nbsp;Conference&nbsp; <b>Alt+Drop</b>:&nbsp;Transfer</p>')
def paintEvent(self, event):
painter = QPainter(self)
......@@ -1602,6 +1675,9 @@ class AudioSessionItem(object):
self.blink_session = session
self.blink_session.items.audio = self
self.status_context = None
self.__saved_status = None # to store skipped status messages during a context change
self.widget = Null
self.status = None
self.type = 'Audio'
......@@ -1676,8 +1752,17 @@ class AudioSessionItem(object):
return self.__dict__['status']
def _set_status(self, value):
if self.__dict__.get('status', Null) == value:
old_status = self.__dict__.get('status', Null)
new_status = value
if old_status == new_status:
return
if old_status is not None and old_status is not Null:
context = None if new_status is None else new_status.context
if self.status_context == old_status.context != context:
self.__saved_status = value # preserve the status that is skipped because of context mismatch
return
elif old_status.context != context and context is not None:
self.__saved_status = old_status # preserve the status that was there prior to switching the context
self.__dict__['status'] = value
self.widget.status_label.value = value
......@@ -1796,7 +1881,8 @@ class AudioSessionItem(object):
def _reset_status(self, expected_status):
if self.status == expected_status:
self.status = None
self.status = self.__saved_status
self.__saved_status = None
def _SH_HangupButtonClicked(self):
self.end()
......@@ -1928,6 +2014,26 @@ class AudioSessionItem(object):
self.status = Status(notification.data.reason)
self._cleanup()
def _NH_BlinkSessionTransferNewOutgoing(self, notification):
if self.blink_session.state == 'connected':
self.status_context = 'transfer'
self.status = Status('Transfer: Trying', context='transfer')
def _NH_BlinkSessionTransferDidEnd(self, notification):
if self.blink_session.transfer_direction == 'outgoing':
self.status = Status('Transfer: Succeeded', context='transfer')
def _NH_BlinkSessionTransferDidFail(self, notification):
if self.blink_session.state == 'connected' and self.blink_session.transfer_direction == 'outgoing':
reason = 'Decline' if notification.data.code == 603 else notification.data.reason
self.status = Status("Transfer: {}".format(reason), context='transfer')
call_later(3, self._reset_status, self.status)
self.status_context = None
def _NH_BlinkSessionTransferGotProgress(self, notification):
if self.blink_session.state == 'connected' and notification.data.code < 200: # final answers are handled in DidEnd and DiDFail
self.status = Status("Transfer: {}".format(notification.data.reason), context='transfer')
def _NH_MediaStreamWillEnd(self, notification):
stream = notification.sender
if stream.type == 'audio' and stream.blink_session.items.audio is self:
......@@ -2022,7 +2128,7 @@ class AudioSessionModel(QAbstractListModel):
return None
def supportedDropActions(self):
return Qt.CopyAction | Qt.MoveAction
return Qt.CopyAction | Qt.MoveAction | Qt.LinkAction
def mimeTypes(self):
return ['application/x-blink-session-list']
......@@ -2057,7 +2163,10 @@ class AudioSessionModel(QAbstractListModel):
selection_model = session_list.selectionModel()
source = session_list.dragged_session
target = self.sessions[index.row()] if index.isValid() else None
if source.client_conference is None: # the dragged session is not in a conference yet
if action == Qt.LinkAction: # call transfer
source.blink_session.transfer(target.blink_session.contact_uri, replaced_session=target.blink_session)
elif source.client_conference is None: # the dragged session is not in a conference yet
if target.client_conference is not None:
source_row = self.sessions.index(source)
target_row = self.sessions.index(target.client_conference.sessions[-1].items.audio) + 1
......@@ -2088,6 +2197,7 @@ class AudioSessionModel(QAbstractListModel):
session.active = source.active or target.active
if source.active:
source.client_conference.unhold()
self.structureChanged.emit()
else: # the dragged session is in a conference
dragged = source
sibling = next(session.items.audio for session in dragged.client_conference.sessions if session.items.audio is not dragged)
......@@ -2127,7 +2237,7 @@ class AudioSessionModel(QAbstractListModel):
session_list.scrollTo(self.index(self.sessions.index(sibling)), session_list.PositionAtCenter)
dragged.widget.selected = False
dragged.active = False
self.structureChanged.emit()
self.structureChanged.emit()
return True
def _DH_ApplicationXBlinkContactList(self, mime_data, action, index):
......@@ -2515,10 +2625,8 @@ class AudioSessionListView(QListView):
if not acceptable_mime_types:
event.ignore()
elif event_source is not self and 'application/x-blink-session-list' in provided_mime_types:
event.ignore() # we don't handle drops for blink sessions from other sources
event.ignore() # we don't handle drops for blink sessions from other sources
else:
if event_source is self:
event.setDropAction(Qt.MoveAction)
event.accept()
def dragLeaveEvent(self, event):
......@@ -2528,8 +2636,6 @@ class AudioSessionListView(QListView):
def dragMoveEvent(self, event):
super(AudioSessionListView, self).dragMoveEvent(event)
if event.source() is self:
event.setDropAction(Qt.MoveAction)
model = self.model()
......@@ -2551,7 +2657,10 @@ class AudioSessionListView(QListView):
def dropEvent(self, event):
model = self.model()
if event.source() is self:
event.setDropAction(Qt.MoveAction)
if event.keyboardModifiers() & Qt.AltModifier:
event.setDropAction(Qt.LinkAction)
else:
event.setDropAction(Qt.MoveAction)
for session in self.model().sessions:
session.widget.drop_indicator = False
if model.handleDroppedData(event.mimeData(), event.dropAction(), self.indexAt(event.pos())):
......@@ -2565,9 +2674,19 @@ class AudioSessionListView(QListView):
rect = self.viewport().rect()
rect.setTop(self.visualRect(model.index(len(model.sessions)-1)).bottom())
if dragged_session.client_conference is not None:
event.setDropAction(Qt.MoveAction)
event.accept(rect)
else:
event.ignore(rect)
elif event.keyboardModifiers() & Qt.AltModifier and dragged_session.client_conference is None:
if dragged_session is session or session.client_conference is not None or session.blink_session.state != 'connected':
event.ignore(rect)
elif dragged_session.blink_session.transfer_state in ('active', 'completed') or session.blink_session.transfer_state in ('active', 'completed'):
event.ignore(rect)
else:
session.widget.drop_indicator = True
event.setDropAction(Qt.LinkAction) # it might not be LinkAction if other keyboard modifiers are active
event.accept(rect)
else:
conference = dragged_session.client_conference or Null
if dragged_session is session or session.blink_session in conference.sessions:
......@@ -2579,6 +2698,7 @@ class AudioSessionListView(QListView):
sibling.items.audio.widget.drop_indicator = True
else:
session.widget.drop_indicator = True
event.setDropAction(Qt.MoveAction)
event.accept(rect)
def _DH_ApplicationXBlinkContactList(self, event, index, rect, session):
......@@ -5091,6 +5211,88 @@ class IncomingFileTransferRequest(QObject):
self.rejected.emit(self, self.dialog.reject_mode)
ui_class, base_class = uic.loadUiType(Resources.get('incoming_calltransfer_dialog.ui'))
class IncomingCallTransferDialog(IncomingDialogBase, ui_class):
def __init__(self, parent=None):
super(IncomingCallTransferDialog, self).__init__(parent)
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_DeleteOnClose)
with Resources.directory:
self.setupUi(self)
font = self.username_label.font()
font.setPointSizeF(self.uri_label.fontInfo().pointSizeF() + 3)
font.setFamily("Sans Serif")
self.username_label.setFont(font)
font = self.transfer_label.font()
font.setPointSizeF(self.uri_label.fontInfo().pointSizeF() - 1)
self.transfer_label.setFont(font)
self.slot = None
self.reject_mode = 'reject'
def show(self, activate=True):
self.setAttribute(Qt.WA_ShowWithoutActivating, not activate)
super(IncomingCallTransferDialog, self).show()
del ui_class, base_class
class IncomingCallTransferRequest(QObject):
finished = pyqtSignal(object)
accepted = pyqtSignal(object)
rejected = pyqtSignal(object, str)
priority = 0
stream_types = {'audio'}
def __init__(self, dialog, contact, contact_uri, session):
super(IncomingCallTransferRequest, self).__init__()
self.dialog = dialog
self.contact = contact
self.contact_uri = contact_uri
self.session = session
self.dialog.setWindowTitle(u'Incoming Call Transfer')
self.dialog.setWindowIconText(u'Incoming Call Transfer')
self.dialog.uri_label.setText(contact_uri.uri)
self.dialog.username_label.setText(contact.name)
if contact.pixmap:
self.dialog.user_icon.setPixmap(contact.pixmap)
self.dialog.transfer_label.setText(u'would like to transfer you to {.uri}'.format(contact_uri))
self.dialog.finished.connect(self._SH_DialogFinished)
self.dialog.accepted.connect(self._SH_DialogAccepted)
self.dialog.rejected.connect(self._SH_DialogRejected)
def __eq__(self, other):
return self is other
def __ne__(self, other):
return self is not other
def __lt__(self, other):
return self.priority < other.priority
def __le__(self, other):
return self.priority <= other.priority
def __gt__(self, other):
return self.priority > other.priority
def __ge__(self, other):
return self.priority >= other.priority
def _SH_DialogFinished(self):
self.finished.emit(self)
def _SH_DialogAccepted(self):
self.accepted.emit(self)
def _SH_DialogRejected(self):
self.rejected.emit(self, self.dialog.reject_mode)
ui_class, base_class = uic.loadUiType(Resources.get('conference_dialog.ui'))
class ConferenceDialog(base_class, ui_class):
......@@ -5210,6 +5412,7 @@ class SessionManager(object):
self._filetransfer_tone_timer.setSingleShot(True)
notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPSessionNewOutgoing')
notification_center.add_observer(self, name='SIPSessionNewIncoming')
notification_center.add_observer(self, name='SIPSessionDidFail')
......@@ -5395,6 +5598,18 @@ class SessionManager(object):
if mode == 'reject':
incoming_request.session.reject(603)
def _SH_IncomingCallTransferRequestAccepted(self, incoming_request):
try:
incoming_request.session.accept_transfer()
except IllegalStateError:
pass
def _SH_IncomingCallTransferRequestRejected(self, incoming_request, mode):
try:
incoming_request.session.reject_transfer()
except IllegalStateError:
pass
@run_in_gui_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
......@@ -5457,6 +5672,20 @@ class SessionManager(object):
incoming_request.dialog.show(activate=QApplication.activeWindow() is not None and self.incoming_requests.index(incoming_request) == 0)
self.update_ringtone()
def _NH_SIPSessionNewOutgoing(self, notification):
sip_session = notification.sender
if sip_session.transfer_info is not None:
from blink.contacts import URIUtils
contact, contact_uri = URIUtils.find_contact(sip_session.remote_identity.uri)
try:
blink_session = next(session for session in self.sessions if session.reusable and session.contact.settings is contact.settings)
reinitialize = True
except StopIteration:
blink_session = BlinkSession()
reinitialize = False
blink_session.init_transfer(sip_session, notification.data.streams, contact, contact_uri, reinitialize=reinitialize)
def _NH_SIPSessionDidFail(self, notification):
if notification.sender.direction == 'incoming':
for incoming_request in self.incoming_requests[notification.sender]:
......@@ -5510,6 +5739,28 @@ class SessionManager(object):
self.sessions.remove(notification.sender)
notification.center.remove_observer(self, sender=notification.sender)
def _NH_BlinkSessionTransferNewIncoming(self, notification):
from blink.contacts import URIUtils
session = notification.sender.sip_session
contact, contact_uri = URIUtils.find_contact(notification.data.transfer_destination)
dialog = IncomingCallTransferDialog() # Build the dialog without a parent in order to be displayed on the current workspace on Linux.
incoming_request = IncomingCallTransferRequest(dialog, contact, contact_uri, session)
incoming_request.finished.connect(self._SH_IncomingRequestFinished)
incoming_request.accepted.connect(self._SH_IncomingCallTransferRequestAccepted)
incoming_request.rejected.connect(self._SH_IncomingCallTransferRequestRejected)
bisect.insort_right(self.incoming_requests, incoming_request)
incoming_request.dialog.show(activate=QApplication.activeWindow() is not None and self.incoming_requests.index(incoming_request) == 0)
self.update_ringtone()
def _NH_BlinkSessionTransferDidFail(self, notification):
for request in self.incoming_requests[notification.sender.sip_session, IncomingCallTransferRequest]:
request.dialog.hide()
self.incoming_requests.remove(request)
self.update_ringtone()
def _NH_BlinkFileTransferWasCreated(self, notification):
self.file_transfers.append(notification.sender)
notification.center.add_observer(self, sender=notification.sender)
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>480</width>
<height>165</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>480</width>
<height>165</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>165</height>
</size>
</property>
<property name="windowTitle">
<string>Incoming Call Transfer</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>icons/blink48.png</normaloff>icons/blink48.png</iconset>
</property>
<layout class="QVBoxLayout" name="dialog_layout">
<property name="spacing">
<number>34</number>
</property>
<property name="margin">
<number>8</number>
</property>
<item>
<widget class="QFrame" name="frame">
<property name="styleSheet">
<string>QFrame#frame {
background-color: #f8f8f8;
border-color: #545454;
border-radius: 4px;
border-width: 2px;
border-style: solid;
}
</string>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<layout class="QGridLayout" name="frame_layout">
<property name="topMargin">
<number>7</number>
</property>
<property name="bottomMargin">
<number>7</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="QLabel" name="username_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<family>Sans Serif</family>
<pointsize>12</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Caller name</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="uri_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string>Caller URI</string>
</property>
<property name="indent">
<number>1</number>
</property>
</widget>
</item>
<item row="0" column="0" rowspan="2">
<widget class="QLabel" name="user_icon">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="pixmap">
<pixmap>icons/default-avatar.png</pixmap>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<spacer name="frame_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="transfer_label">
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string extracomment="is offering to share his screen">would like to transfer you to user@domain</string>
</property>
<property name="indent">
<number>3</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="button_layout">
<property name="spacing">
<number>5</number>
</property>
<item>
<widget class="QPushButton" name="reject_button">
<property name="minimumSize">
<size>
<width>85</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>24</height>
</size>
</property>
<property name="text">
<string>Reject</string>
</property>
</widget>
</item>
<item>
<spacer name="button_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="accept_button">
<property name="minimumSize">
<size>
<width>85</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>24</height>
</size>
</property>
<property name="text">
<string>Accept</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>accept_button</tabstop>
<tabstop>reject_button</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>accept_button</sender>
<signal>clicked()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>345</x>
<y>117</y>
</hint>
<hint type="destinationlabel">
<x>196</x>
<y>67</y>
</hint>
</hints>
</connection>
<connection>
<sender>reject_button</sender>
<signal>clicked()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>48</x>
<y>117</y>
</hint>
<hint type="destinationlabel">
<x>196</x>
<y>67</y>
</hint>
</hints>
</connection>
</connections>
</ui>
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