mainwindow.py 46.7 KB
Newer Older
Dan Pascu's avatar
Dan Pascu committed
1

2 3 4
import hashlib
import os

5 6
from functools import partial

Dan Pascu's avatar
Dan Pascu committed
7 8 9 10
from PyQt5 import uic
from PyQt5.QtCore import Qt, QSettings, QUrl
from PyQt5.QtGui import QDesktopServices, QIcon
from PyQt5.QtWidgets import QAction, QActionGroup, QApplication, QFileDialog, QMenu, QShortcut, QStyle, QStyleOptionComboBox, QStyleOptionFrame, QSystemTrayIcon
11

12
from application.notification import IObserver, NotificationCenter
13
from application.python import Null, limit
14
from application.system import makedirs
15 16
from zope.interface import implements

17
from sipsimple.account import Account, AccountManager, BonjourAccount
18
from sipsimple.application import SIPApplication
19
from sipsimple.configuration.datatypes import Path
20
from sipsimple.configuration.settings import SIPSimpleSettings
Dan Pascu's avatar
Dan Pascu committed
21

Dan Pascu's avatar
Dan Pascu committed
22
from blink.aboutpanel import AboutPanel
Dan Pascu's avatar
Dan Pascu committed
23
from blink.accounts import AccountModel, ActiveAccountModel, ServerToolsAccountModel, ServerToolsWindow
Dan Pascu's avatar
Dan Pascu committed
24
from blink.contacts import Contact, ContactEditorDialog, ContactModel, ContactSearchModel, URIUtils
Saul Ibarra's avatar
Saul Ibarra committed
25
from blink.filetransferwindow import FileTransferWindow
26
from blink.history import HistoryManager
Dan Pascu's avatar
Dan Pascu committed
27
from blink.preferences import PreferencesWindow
28
from blink.sessions import ConferenceDialog, SessionManager, AudioSessionModel, StreamDescription
Dan Pascu's avatar
Dan Pascu committed
29
from blink.configuration.datatypes import IconDescriptor, FileURL, PresenceState
30
from blink.configuration.settings import BlinkSettings
31
from blink.presence import PendingWatcherDialog
32
from blink.resources import ApplicationData, IconManager, Resources
33
from blink.util import run_in_gui_thread
34
from blink.widgets.buttons import AccountState, SwitchViewButton
Dan Pascu's avatar
Dan Pascu committed
35 36


37 38 39
__all__ = ['MainWindow']


Dan Pascu's avatar
Dan Pascu committed
40 41
ui_class, base_class = uic.loadUiType(Resources.get('blink.ui'))

42

Dan Pascu's avatar
Dan Pascu committed
43
class MainWindow(base_class, ui_class):
44 45
    implements(IObserver)

Dan Pascu's avatar
Dan Pascu committed
46 47
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
48
        self.saved_account_state = None
49 50 51

        notification_center = NotificationCenter()
        notification_center.add_observer(self, name='SIPApplicationWillStart')
52
        notification_center.add_observer(self, name='SIPApplicationDidStart')
53
        notification_center.add_observer(self, name='SIPAccountGotMessageSummary')
54
        notification_center.add_observer(self, name='SIPAccountGotPendingWatcher')
55 56
        notification_center.add_observer(self, name='BlinkSessionNewOutgoing')
        notification_center.add_observer(self, name='BlinkSessionDidReinitializeForOutgoing')
Dan Pascu's avatar
Dan Pascu committed
57
        notification_center.add_observer(self, name='BlinkSessionTransferNewOutgoing')
58 59
        notification_center.add_observer(self, name='BlinkFileTransferNewIncoming')
        notification_center.add_observer(self, name='BlinkFileTransferNewOutgoing')
60 61
        notification_center.add_observer(self, sender=AccountManager())

Dan Pascu's avatar
Dan Pascu committed
62 63
        icon_manager = IconManager()

64 65
        self.pending_watcher_dialogs = []

66 67
        self.mwi_icons = [QIcon(Resources.get('icons/mwi-%d.png' % i)) for i in xrange(0, 11)]
        self.mwi_icons.append(QIcon(Resources.get('icons/mwi-many.png')))
Dan Pascu's avatar
Dan Pascu committed
68 69

        with Resources.directory:
70
            self.setupUi()
71

Dan Pascu's avatar
Dan Pascu committed
72 73
        self.setWindowTitle('Blink')
        self.setWindowIconText('Blink')
74

75
        geometry = QSettings().value("main_window/geometry")
76 77 78
        if geometry:
            self.restoreGeometry(geometry)

79
        self.default_icon_path = Resources.get('icons/default-avatar.png')
80
        self.default_icon = QIcon(self.default_icon_path)
81
        self.last_icon_directory = Path('~').normalized
Dan Pascu's avatar
Dan Pascu committed
82
        self.set_user_icon(icon_manager.get('avatar'))
83

84
        self.active_sessions_label.hide()
85 86 87 88 89 90 91 92 93
        self.enable_call_buttons(False)
        self.conference_button.setEnabled(False)
        self.hangup_all_button.setEnabled(False)
        self.sip_server_settings_action.setEnabled(False)
        self.search_for_people_action.setEnabled(False)
        self.history_on_server_action.setEnabled(False)
        self.main_view.setCurrentWidget(self.contacts_panel)
        self.contacts_view.setCurrentWidget(self.contact_list_panel)
        self.search_view.setCurrentWidget(self.search_list_panel)
Dan Pascu's avatar
Dan Pascu committed
94

95
        # System tray
96
        if QSystemTrayIcon.isSystemTrayAvailable():
97 98 99
            self.system_tray_icon = QSystemTrayIcon(QIcon(Resources.get('icons/blink.png')), self)
            self.system_tray_icon.activated.connect(self._SH_SystemTrayIconActivated)
            menu = QMenu(self)
100 101
            menu.addAction("Show", self._AH_SystemTrayShowWindow)
            menu.addAction(QIcon(Resources.get('icons/application-exit.png')), "Quit", self._AH_QuitActionTriggered)
102 103 104 105 106
            self.system_tray_icon.setContextMenu(menu)
            self.system_tray_icon.show()
        else:
            self.system_tray_icon = None

107
        # Accounts
108 109
        self.account_model = AccountModel(self)
        self.enabled_account_model = ActiveAccountModel(self.account_model, self)
110
        self.server_tools_account_model = ServerToolsAccountModel(self.account_model, self)
111 112
        self.identity.setModel(self.enabled_account_model)

113
        # Contacts
Dan Pascu's avatar
Dan Pascu committed
114 115
        self.contact_model = ContactModel(self)
        self.contact_search_model = ContactSearchModel(self.contact_model, self)
116
        self.contact_list.setModel(self.contact_model)
Dan Pascu's avatar
Dan Pascu committed
117
        self.search_list.setModel(self.contact_search_model)
118

119 120
        # Sessions (audio)
        self.session_model = AudioSessionModel(self)
Dan Pascu's avatar
Dan Pascu committed
121
        self.session_list.setModel(self.session_model)
122
        self.session_list.selectionModel().selectionChanged.connect(self._SH_SessionListSelectionChanged)
123

124 125 126
        # History
        self.history_manager = HistoryManager()

127 128
        # Windows, dialogs and panels
        self.about_panel = AboutPanel(self)
129
        self.conference_dialog = ConferenceDialog(self)
130
        self.contact_editor_dialog = ContactEditorDialog(self)
Saul Ibarra's avatar
Saul Ibarra committed
131
        self.filetransfer_window = FileTransferWindow()
Dan Pascu's avatar
Dan Pascu committed
132
        self.preferences_window = PreferencesWindow(self.account_model, None)
133
        self.server_tools_window = ServerToolsWindow(self.server_tools_account_model, None)
134

135
        # Signals
136
        self.account_state.stateChanged.connect(self._SH_AccountStateChanged)
137
        self.account_state.clicked.connect(self._SH_AccountStateClicked)
138
        self.activity_note.editingFinished.connect(self._SH_ActivityNoteEditingFinished)
139 140 141
        self.add_contact_button.clicked.connect(self._SH_AddContactButtonClicked)
        self.add_search_contact_button.clicked.connect(self._SH_AddContactButtonClicked)
        self.audio_call_button.clicked.connect(self._SH_AudioCallButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
142
        self.video_call_button.clicked.connect(self._SH_VideoCallButtonClicked)
143
        self.chat_session_button.clicked.connect(self._SH_ChatSessionButtonClicked)
144
        self.back_to_contacts_button.clicked.connect(self.search_box.clear)  # this can be set in designer -Dan
145 146
        self.conference_button.makeConference.connect(self._SH_MakeConference)
        self.conference_button.breakConference.connect(self._SH_BreakConference)
Dan Pascu's avatar
Dan Pascu committed
147

148
        self.contact_list.selectionModel().selectionChanged.connect(self._SH_ContactListSelectionChanged)
149 150
        self.contact_model.itemsAdded.connect(self._SH_ContactModelAddedItems)
        self.contact_model.itemsRemoved.connect(self._SH_ContactModelRemovedItems)
Dan Pascu's avatar
Dan Pascu committed
151

152 153
        self.display_name.editingFinished.connect(self._SH_DisplayNameEditingFinished)
        self.hangup_all_button.clicked.connect(self._SH_HangupAllButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
154

155
        self.identity.activated[int].connect(self._SH_IdentityChanged)
156 157
        self.identity.currentIndexChanged[int].connect(self._SH_IdentityCurrentIndexChanged)

158
        self.mute_button.clicked.connect(self._SH_MuteButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
159

160 161 162
        self.search_box.textChanged.connect(self._SH_SearchBoxTextChanged)
        self.search_box.returnPressed.connect(self._SH_SearchBoxReturnPressed)
        self.search_box.shortcut.activated.connect(self.search_box.setFocus)
163

164 165 166 167
        self.search_list.selectionModel().selectionChanged.connect(self._SH_SearchListSelectionChanged)

        self.server_tools_account_model.rowsInserted.connect(self._SH_ServerToolsAccountModelChanged)
        self.server_tools_account_model.rowsRemoved.connect(self._SH_ServerToolsAccountModelChanged)
Dan Pascu's avatar
Dan Pascu committed
168

169 170 171
        self.session_model.sessionAdded.connect(self._SH_AudioSessionModelAddedSession)
        self.session_model.sessionRemoved.connect(self._SH_AudioSessionModelRemovedSession)
        self.session_model.structureChanged.connect(self._SH_AudioSessionModelChangedStructure)
172

173 174
        self.silent_button.clicked.connect(self._SH_SilentButtonClicked)
        self.switch_view_button.viewChanged.connect(self._SH_SwitchViewButtonChangedView)
175

176
        # Blink menu actions
Dan Pascu's avatar
Dan Pascu committed
177
        self.about_action.triggered.connect(self.about_panel.show)
Dan Pascu's avatar
Dan Pascu committed
178 179
        self.add_account_action.triggered.connect(self.preferences_window.show_add_account_dialog)
        self.manage_accounts_action.triggered.connect(self.preferences_window.show_for_accounts)
180
        self.help_action.triggered.connect(partial(QDesktopServices.openUrl, QUrl(u'http://icanblink.com/help/')))
Dan Pascu's avatar
Dan Pascu committed
181
        self.preferences_action.triggered.connect(self.preferences_window.show)
182
        self.auto_accept_chat_action.triggered.connect(self._AH_AutoAcceptChatActionTriggered)
183
        self.received_messages_sound_action.triggered.connect(self._AH_ReceivedMessagesSoundActionTriggered)
184
        self.answering_machine_action.triggered.connect(self._AH_EnableAnsweringMachineActionTriggered)
185
        self.release_notes_action.triggered.connect(partial(QDesktopServices.openUrl, QUrl(u'http://icanblink.com/changelog/')))
186
        self.quit_action.triggered.connect(self._AH_QuitActionTriggered)
187

Dan Pascu's avatar
Dan Pascu committed
188 189
        # Call menu actions
        self.redial_action.triggered.connect(self._AH_RedialActionTriggered)
190
        self.join_conference_action.triggered.connect(self.conference_dialog.show)
191 192
        self.history_menu.aboutToShow.connect(self._SH_HistoryMenuAboutToShow)
        self.history_menu.triggered.connect(self._AH_HistoryMenuTriggered)
193 194 195
        self.output_devices_group.triggered.connect(self._AH_AudioOutputDeviceChanged)
        self.input_devices_group.triggered.connect(self._AH_AudioInputDeviceChanged)
        self.alert_devices_group.triggered.connect(self._AH_AudioAlertDeviceChanged)
Dan Pascu's avatar
Dan Pascu committed
196
        self.video_devices_group.triggered.connect(self._AH_VideoDeviceChanged)
197 198
        self.mute_action.triggered.connect(self._SH_MuteButtonClicked)
        self.silent_action.triggered.connect(self._SH_SilentButtonClicked)
199

200 201 202 203
        # Tools menu actions
        self.sip_server_settings_action.triggered.connect(self._AH_SIPServerSettings)
        self.search_for_people_action.triggered.connect(self._AH_SearchForPeople)
        self.history_on_server_action.triggered.connect(self._AH_HistoryOnServer)
204 205 206 207 208

        # Window menu actions
        self.chat_window_action.triggered.connect(self._AH_ChatWindowActionTriggered)
        self.transfers_window_action.triggered.connect(self._AH_TransfersWindowActionTriggered)
        self.logs_window_action.triggered.connect(self._AH_LogsWindowActionTriggered)
Dan Pascu's avatar
Dan Pascu committed
209 210
        self.received_files_window_action.triggered.connect(self._AH_ReceivedFilesWindowActionTriggered)
        self.screenshots_window_action.triggered.connect(self._AH_ScreenshotsWindowActionTriggered)
211

212 213 214
    def setupUi(self):
        super(MainWindow, self).setupUi(self)

215
        self.search_box.shortcut = QShortcut(self.search_box)
Dan Pascu's avatar
Dan Pascu committed
216
        self.search_box.shortcut.setKey('Ctrl+F')
217 218 219 220

        self.output_devices_group = QActionGroup(self)
        self.input_devices_group = QActionGroup(self)
        self.alert_devices_group = QActionGroup(self)
Dan Pascu's avatar
Dan Pascu committed
221
        self.video_devices_group = QActionGroup(self)
222

223 224
        self.screen_sharing_button.addAction(QAction('Request screen', self.screen_sharing_button, triggered=self._AH_RequestScreenActionTriggered))
        self.screen_sharing_button.addAction(QAction('Share my screen', self.screen_sharing_button, triggered=self._AH_ShareMyScreenActionTriggered))
Dan Pascu's avatar
Dan Pascu committed
225

226 227
        # adjust search box height depending on theme as the value set in designer isn't suited for all themes
        search_box = self.search_box
Dan Pascu's avatar
Dan Pascu committed
228
        option = QStyleOptionFrame()
229 230 231 232 233
        search_box.initStyleOption(option)
        frame_width = search_box.style().pixelMetric(QStyle.PM_DefaultFrameWidth, option, search_box)
        if frame_width < 4:
            search_box.setMinimumHeight(20 + 2*frame_width)

234 235
        # adjust the combo boxes for themes with too much padding (like the default theme on Ubuntu 10.04)
        option = QStyleOptionComboBox()
236 237
        self.identity.initStyleOption(option)
        wide_padding = self.identity.style().subControlRect(QStyle.CC_ComboBox, option, QStyle.SC_ComboBoxEditField, self.identity).height() < 10
238 239
        self.identity.setStyleSheet("""QComboBox { padding: 0px 4px 0px 4px; }""" if wide_padding else "")

240
    def closeEvent(self, event):
241
        QSettings().setValue("main_window/geometry", self.saveGeometry())
242
        super(MainWindow, self).closeEvent(event)
Dan Pascu's avatar
Dan Pascu committed
243
        self.about_panel.close()
Dan Pascu's avatar
Dan Pascu committed
244
        self.contact_editor_dialog.close()
245
        self.server_tools_window.close()
246 247
        for dialog in self.pending_watcher_dialogs[:]:
            dialog.close()
248

249 250 251 252 253
    def show(self):
        super(MainWindow, self).show()
        self.raise_()
        self.activateWindow()

254 255
    def set_user_icon(self, icon):
        self.account_state.setIcon(icon or self.default_icon)
Dan Pascu's avatar
Dan Pascu committed
256

257
    def enable_call_buttons(self, enabled):
Dan Pascu's avatar
Dan Pascu committed
258
        self.audio_call_button.setEnabled(enabled)
Dan Pascu's avatar
Dan Pascu committed
259
        self.video_call_button.setEnabled(enabled)
260
        self.chat_session_button.setEnabled(enabled)
Dan Pascu's avatar
Dan Pascu committed
261
        self.screen_sharing_button.setEnabled(enabled)
262

263 264 265
    def load_audio_devices(self):
        settings = SIPSimpleSettings()

266 267 268
        action_map = {}

        action = action_map[u'system_default'] = self.output_device_menu.addAction(u'System default')
269
        action.setData(u'system_default')
270 271 272
        action.setCheckable(True)
        self.output_devices_group.addAction(action)

273
        self.output_device_menu.addSeparator()
274

275
        for device in SIPApplication.engine.output_devices:
276
            action = action_map[device] = self.output_device_menu.addAction(device)
277
            action.setData(device)
278
            action.setCheckable(True)
279 280 281 282 283 284 285 286 287
            self.output_devices_group.addAction(action)

        action = action_map[None] = self.output_device_menu.addAction(u'None')
        action.setData(None)
        action.setCheckable(True)
        self.output_devices_group.addAction(action)

        active_action = action_map.get(settings.audio.output_device, Null)
        active_action.setChecked(True)
288

289 290 291
        action_map = {}

        action = action_map[u'system_default'] = self.input_device_menu.addAction(u'System default')
292
        action.setData(u'system_default')
293 294 295
        action.setCheckable(True)
        self.input_devices_group.addAction(action)

296
        self.input_device_menu.addSeparator()
297

298
        for device in SIPApplication.engine.input_devices:
299
            action = action_map[device] = self.input_device_menu.addAction(device)
300
            action.setData(device)
301
            action.setCheckable(True)
302 303 304 305 306 307 308 309 310
            self.input_devices_group.addAction(action)

        action = action_map[None] = self.input_device_menu.addAction(u'None')
        action.setData(None)
        action.setCheckable(True)
        self.input_devices_group.addAction(action)

        active_action = action_map.get(settings.audio.input_device, Null)
        active_action.setChecked(True)
311

312 313 314
        action_map = {}

        action = action_map[u'system_default'] = self.alert_device_menu.addAction(u'System default')
315
        action.setData(u'system_default')
316 317 318
        action.setCheckable(True)
        self.alert_devices_group.addAction(action)

319
        self.alert_device_menu.addSeparator()
320

321
        for device in SIPApplication.engine.output_devices:
322
            action = action_map[device] = self.alert_device_menu.addAction(device)
323
            action.setData(device)
324
            action.setCheckable(True)
325 326 327 328 329 330 331 332 333
            self.alert_devices_group.addAction(action)

        action = action_map[None] = self.alert_device_menu.addAction(u'None')
        action.setData(None)
        action.setCheckable(True)
        self.alert_devices_group.addAction(action)

        active_action = action_map.get(settings.audio.alert_device, Null)
        active_action.setChecked(True)
334

Dan Pascu's avatar
Dan Pascu committed
335 336 337
    def load_video_devices(self):
        settings = SIPSimpleSettings()

338 339 340
        action_map = {}

        action = action_map[u'system_default'] = self.video_camera_menu.addAction(u'System default')
Dan Pascu's avatar
Dan Pascu committed
341
        action.setData(u'system_default')
342 343 344
        action.setCheckable(True)
        self.video_devices_group.addAction(action)

Dan Pascu's avatar
Dan Pascu committed
345
        self.video_camera_menu.addSeparator()
346

Dan Pascu's avatar
Dan Pascu committed
347
        for device in SIPApplication.engine.video_devices:
348
            action = action_map[device] = self.video_camera_menu.addAction(device)
Dan Pascu's avatar
Dan Pascu committed
349 350
            action.setData(device)
            action.setCheckable(True)
351 352 353 354 355 356 357 358 359
            self.video_devices_group.addAction(action)

        action = action_map[None] = self.video_camera_menu.addAction(u'None')
        action.setData(None)
        action.setCheckable(True)
        self.video_devices_group.addAction(action)

        active_action = action_map.get(settings.video.device, Null)
        active_action.setChecked(True)
Dan Pascu's avatar
Dan Pascu committed
360

361 362
    def _AH_AccountActionTriggered(self, enabled):
        account = self.sender().data()
363 364 365
        account.enabled = enabled
        account.save()

366 367
    def _AH_AudioAlertDeviceChanged(self, action):
        settings = SIPSimpleSettings()
368
        settings.audio.alert_device = action.data()
369
        settings.save()
370 371 372

    def _AH_AudioInputDeviceChanged(self, action):
        settings = SIPSimpleSettings()
373
        settings.audio.input_device = action.data()
374
        settings.save()
375 376 377

    def _AH_AudioOutputDeviceChanged(self, action):
        settings = SIPSimpleSettings()
378
        settings.audio.output_device = action.data()
379
        settings.save()
380

Dan Pascu's avatar
Dan Pascu committed
381 382 383 384 385
    def _AH_VideoDeviceChanged(self, action):
        settings = SIPSimpleSettings()
        settings.video.device = action.data()
        settings.save()

386
    def _AH_AutoAcceptChatActionTriggered(self, checked):
Dan Pascu's avatar
Dan Pascu committed
387 388 389 390
        settings = SIPSimpleSettings()
        settings.chat.auto_accept = checked
        settings.save()

391 392 393 394 395
    def _AH_ReceivedMessagesSoundActionTriggered(self, checked):
        settings = SIPSimpleSettings()
        settings.sounds.play_message_alerts = checked
        settings.save()

396
    def _AH_EnableAnsweringMachineActionTriggered(self, checked):
Dan Pascu's avatar
Dan Pascu committed
397 398 399 400
        settings = SIPSimpleSettings()
        settings.answering_machine.enabled = checked
        settings.save()

401 402
    def _AH_GoogleContactsActionTriggered(self):
        settings = SIPSimpleSettings()
Dan Pascu's avatar
Dan Pascu committed
403 404
        settings.google_contacts.enabled = not settings.google_contacts.enabled
        settings.save()
405

406 407 408
    def _AH_RedialActionTriggered(self):
        session_manager = SessionManager()
        if session_manager.last_dialed_uri is not None:
409
            contact, contact_uri = URIUtils.find_contact(session_manager.last_dialed_uri)
410
            session_manager.create_session(contact, contact_uri, [StreamDescription('audio')])  # TODO: remember used media types and redial with them. -Saul
411 412

    def _AH_SIPServerSettings(self, checked):
413
        account = self.identity.itemData(self.identity.currentIndex()).account
414 415 416 417
        account = account if account is not BonjourAccount() and account.server.settings_url else None
        self.server_tools_window.open_settings_page(account)

    def _AH_SearchForPeople(self, checked):
418
        account = self.identity.itemData(self.identity.currentIndex()).account
419 420 421 422
        account = account if account is not BonjourAccount() and account.server.settings_url else None
        self.server_tools_window.open_search_for_people_page(account)

    def _AH_HistoryOnServer(self, checked):
423
        account = self.identity.itemData(self.identity.currentIndex()).account
424 425 426
        account = account if account is not BonjourAccount() and account.server.settings_url else None
        self.server_tools_window.open_history_page(account)

427 428 429 430 431
    def _AH_ChatWindowActionTriggered(self, checked):
        blink = QApplication.instance()
        blink.chat_window.show()

    def _AH_TransfersWindowActionTriggered(self, checked):
Saul Ibarra's avatar
Saul Ibarra committed
432 433
        self.filetransfer_window.show()

434
    def _AH_LogsWindowActionTriggered(self, checked):
435 436
        directory = ApplicationData.get('logs')
        makedirs(directory)
437
        QDesktopServices.openUrl(QUrl.fromLocalFile(directory))
438

Dan Pascu's avatar
Dan Pascu committed
439
    def _AH_ReceivedFilesWindowActionTriggered(self, checked):
Dan Pascu's avatar
Dan Pascu committed
440 441
        settings = BlinkSettings()
        directory = settings.transfers_directory.normalized
Dan Pascu's avatar
Dan Pascu committed
442 443 444 445 446
        makedirs(directory)
        QDesktopServices.openUrl(QUrl.fromLocalFile(directory))

    def _AH_ScreenshotsWindowActionTriggered(self, checked):
        settings = BlinkSettings()
Dan Pascu's avatar
Dan Pascu committed
447
        directory = settings.screenshots_directory.normalized
Dan Pascu's avatar
Dan Pascu committed
448 449 450
        makedirs(directory)
        QDesktopServices.openUrl(QUrl.fromLocalFile(directory))

451 452
    def _AH_VoicemailActionTriggered(self, checked):
        account = self.sender().data()
453 454 455
        contact, contact_uri = URIUtils.find_contact(account.voicemail_uri, display_name='Voicemail')
        session_manager = SessionManager()
        session_manager.create_session(contact, contact_uri, [StreamDescription('audio')], account=account)
456

457 458 459 460
    def _SH_HistoryMenuAboutToShow(self):
        self.history_menu.clear()
        if self.history_manager.calls:
            for entry in reversed(self.history_manager.calls):
461
                action = self.history_menu.addAction(entry.icon, entry.text)
462
                action.entry = entry
463
                action.setToolTip(entry.uri)
464 465 466 467
        else:
            action = self.history_menu.addAction("Call history is empty")
            action.setEnabled(False)

468
    def _AH_HistoryMenuTriggered(self, action):
469 470 471 472 473 474
        account_manager = AccountManager()
        session_manager = SessionManager()
        try:
            account = account_manager.get_account(action.entry.account_id)
        except KeyError:
            account = None
475
        contact, contact_uri = URIUtils.find_contact(action.entry.uri)
476
        session_manager.create_session(contact, contact_uri, [StreamDescription('audio')], account=account)  # TODO: memorize media type and use it? -Saul (not sure about history in/out -Dan)
477

478
    def _AH_SystemTrayShowWindow(self):
479 480 481 482
        self.show()
        self.raise_()
        self.activateWindow()

483
    def _AH_QuitActionTriggered(self):
484 485
        if self.system_tray_icon is not None:
            self.system_tray_icon.hide()
486
        QApplication.instance().quit()
487

488 489 490 491 492 493 494 495 496 497 498
    def _SH_AccountStateChanged(self):
        self.activity_note.setText(self.account_state.note)
        if self.account_state.state is AccountState.Invisible:
            self.activity_note.inactiveText = u'(invisible)'
            self.activity_note.setEnabled(False)
        else:
            if not self.activity_note.isEnabled():
                self.activity_note.inactiveText = u'Add an activity note here'
                self.activity_note.setEnabled(True)
        if not self.account_state.state.internal:
            self.saved_account_state = None
Dan Pascu's avatar
Dan Pascu committed
499 500 501 502
        blink_settings = BlinkSettings()
        blink_settings.presence.current_state = PresenceState(self.account_state.state, self.account_state.note)
        blink_settings.presence.state_history = [PresenceState(state, note) for state, note in self.account_state.history]
        blink_settings.save()
503 504

    def _SH_AccountStateClicked(self, checked):
Dan Pascu's avatar
Dan Pascu committed
505
        filename = QFileDialog.getOpenFileName(self, u'Select Icon', self.last_icon_directory, u"Images (*.png *.tiff *.jpg *.xmp *.svg)")[0]
506 507 508
        if filename:
            self.last_icon_directory = os.path.dirname(filename)
            filename = filename if os.path.realpath(filename) != os.path.realpath(self.default_icon_path) else None
Dan Pascu's avatar
Dan Pascu committed
509
            blink_settings = BlinkSettings()
510 511
            icon_manager = IconManager()
            if filename is not None:
Dan Pascu's avatar
Dan Pascu committed
512
                icon = icon_manager.store_file('avatar', filename)
Dan Pascu's avatar
Dan Pascu committed
513 514
                if icon is not None:
                    blink_settings.presence.icon = IconDescriptor(FileURL(icon.filename), hashlib.sha1(icon.content).hexdigest())
515
                else:
Dan Pascu's avatar
Dan Pascu committed
516 517
                    icon_manager.remove('avatar')
                    blink_settings.presence.icon = None
518
            else:
Dan Pascu's avatar
Dan Pascu committed
519
                icon_manager.remove('avatar')
Dan Pascu's avatar
Dan Pascu committed
520 521
                blink_settings.presence.icon = None
            blink_settings.save()
522 523 524

    def _SH_ActivityNoteEditingFinished(self):
        self.activity_note.clearFocus()
525 526
        note = self.activity_note.text()
        if note != self.account_state.note:
527
            self.account_state.state.internal = False
528
            self.account_state.setState(self.account_state.state, note)
529

530
    def _SH_AddContactButtonClicked(self, clicked):
531
        self.contact_editor_dialog.open_for_add(self.search_box.text(), None)
Dan Pascu's avatar
Dan Pascu committed
532

533
    def _SH_AudioCallButtonClicked(self):
534
        list_view = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
535 536 537 538
        if list_view.detail_view.isVisible():
            list_view.detail_view._AH_StartAudioCall()
        else:
            selected_indexes = list_view.selectionModel().selectedIndexes()
539 540 541 542 543 544 545 546
            if selected_indexes:
                contact = selected_indexes[0].data(Qt.UserRole)
                contact_uri = contact.uri
            else:
                contact, contact_uri = URIUtils.find_contact(self.search_box.text())
            session_manager = SessionManager()
            session_manager.create_session(contact, contact_uri, [StreamDescription('audio')])

Dan Pascu's avatar
Dan Pascu committed
547 548 549 550 551 552 553 554 555 556 557 558 559 560
    def _SH_VideoCallButtonClicked(self):
        list_view = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
        if list_view.detail_view.isVisible():
            list_view.detail_view._AH_StartVideoCall()
        else:
            selected_indexes = list_view.selectionModel().selectedIndexes()
            if selected_indexes:
                contact = selected_indexes[0].data(Qt.UserRole)
                contact_uri = contact.uri
            else:
                contact, contact_uri = URIUtils.find_contact(self.search_box.text())
            session_manager = SessionManager()
            session_manager.create_session(contact, contact_uri, [StreamDescription('audio'), StreamDescription('video')])

561 562 563 564 565 566 567 568 569 570 571
    def _SH_ChatSessionButtonClicked(self):
        list_view = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
        if list_view.detail_view.isVisible():
            list_view.detail_view._AH_StartChatSession()
        else:
            selected_indexes = list_view.selectionModel().selectedIndexes()
            if selected_indexes:
                contact = selected_indexes[0].data(Qt.UserRole)
                contact_uri = contact.uri
            else:
                contact, contact_uri = URIUtils.find_contact(self.search_box.text())
572
            session_manager = SessionManager()
573
            session_manager.create_session(contact, contact_uri, [StreamDescription('chat')], connect=False)
574

Dan Pascu's avatar
Dan Pascu committed
575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602
    def _AH_RequestScreenActionTriggered(self):
        list_view = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
        if list_view.detail_view.isVisible():
            list_view.detail_view._AH_RequestScreen()
        else:
            selected_indexes = list_view.selectionModel().selectedIndexes()
            if selected_indexes:
                contact = selected_indexes[0].data(Qt.UserRole)
                contact_uri = contact.uri
            else:
                contact, contact_uri = URIUtils.find_contact(self.search_box.text())
            session_manager = SessionManager()
            session_manager.create_session(contact, contact_uri, [StreamDescription('screen-sharing', mode='viewer'), StreamDescription('audio')])

    def _AH_ShareMyScreenActionTriggered(self):
        list_view = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
        if list_view.detail_view.isVisible():
            list_view.detail_view._AH_ShareMyScreen()
        else:
            selected_indexes = list_view.selectionModel().selectedIndexes()
            if selected_indexes:
                contact = selected_indexes[0].data(Qt.UserRole)
                contact_uri = contact.uri
            else:
                contact, contact_uri = URIUtils.find_contact(self.search_box.text())
            session_manager = SessionManager()
            session_manager.create_session(contact, contact_uri, [StreamDescription('screen-sharing', mode='server'), StreamDescription('audio')])

603
    def _SH_BreakConference(self):
604
        active_session = self.session_list.selectionModel().selectedIndexes()[0].data(Qt.UserRole)
605
        self.session_model.breakConference(active_session.client_conference)
606 607 608 609

    def _SH_ContactListSelectionChanged(self, selected, deselected):
        account_manager = AccountManager()
        selected_items = self.contact_list.selectionModel().selectedIndexes()
610
        self.enable_call_buttons(account_manager.default_account is not None and len(selected_items) == 1 and isinstance(selected_items[0].data(Qt.UserRole), Contact))
Dan Pascu's avatar
Dan Pascu committed
611

612
    def _SH_ContactModelAddedItems(self, items):
613
        if not self.search_box.text():
Dan Pascu's avatar
Dan Pascu committed
614 615 616 617
            return
        active_widget = self.search_list_panel if self.contact_search_model.rowCount() else self.not_found_panel
        self.search_view.setCurrentWidget(active_widget)

618
    def _SH_ContactModelRemovedItems(self, items):
619
        if not self.search_box.text():
Dan Pascu's avatar
Dan Pascu committed
620 621
            return
        if any(type(item) is Contact for item in items) and self.contact_search_model.rowCount() == 0:
622
            self.search_box.clear()  # check this. it is no longer be the correct behaviour as now contacts can be deleted from remote -Dan
Dan Pascu's avatar
Dan Pascu committed
623 624 625 626
        else:
            active_widget = self.search_list_panel if self.contact_search_model.rowCount() else self.not_found_panel
            self.search_view.setCurrentWidget(active_widget)

627 628 629 630
    def _SH_DisplayNameEditingFinished(self):
        self.display_name.clearFocus()
        index = self.identity.currentIndex()
        if index != -1:
631
            name = self.display_name.text()
632
            account = self.identity.itemData(index).account
633 634 635
            account.display_name = name if name else None
            account.save()

636 637 638 639 640 641
    def _SH_HangupAllButtonClicked(self):
        for session in self.session_model.sessions:
            session.end()

    def _SH_IdentityChanged(self, index):
        account_manager = AccountManager()
642
        account_manager.default_account = self.identity.itemData(index).account
643

644 645
    def _SH_IdentityCurrentIndexChanged(self, index):
        if index != -1:
646
            account = self.identity.itemData(index).account
647
            self.display_name.setText(account.display_name or u'')
648
            self.display_name.setEnabled(True)
649
            self.activity_note.setEnabled(True)
650
            self.account_state.setEnabled(True)
651 652 653
        else:
            self.display_name.clear()
            self.display_name.setEnabled(False)
654
            self.activity_note.setEnabled(False)
655 656 657
            self.account_state.setEnabled(False)
            self.account_state.setState(AccountState.Invisible)
            self.saved_account_state = None
658

659
    def _SH_MakeConference(self):
660
        self.session_model.conferenceSessions([session for session in self.session_model.active_sessions if session.client_conference is None])
661

662
    def _SH_MuteButtonClicked(self, muted):
663 664 665
        settings = SIPSimpleSettings()
        settings.audio.muted = muted
        settings.save()
666

667
    def _SH_SearchBoxReturnPressed(self):
668
        address = self.search_box.text()
669
        if address:
670
            contact, contact_uri = URIUtils.find_contact(address)
671
            session_manager = SessionManager()
672
            session_manager.create_session(contact, contact_uri, [StreamDescription('audio')])
673 674

    def _SH_SearchBoxTextChanged(self, text):
675
        self.contact_search_model.setFilterFixedString(text)
676 677 678
        account_manager = AccountManager()
        if text:
            self.switch_view_button.view = SwitchViewButton.ContactView
679 680 681 682
            if self.contacts_view.currentWidget() is not self.search_panel:
                self.search_list.selectionModel().clearSelection()
            self.contacts_view.setCurrentWidget(self.search_panel)
            self.search_view.setCurrentWidget(self.search_list_panel if self.contact_search_model.rowCount() else self.not_found_panel)
683
            selected_items = self.search_list.selectionModel().selectedIndexes()
684
            self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)<=1)
685
        else:
686
            self.contacts_view.setCurrentWidget(self.contact_list_panel)
687
            selected_items = self.contact_list.selectionModel().selectedIndexes()
688
            self.enable_call_buttons(account_manager.default_account is not None and len(selected_items) == 1 and type(selected_items[0].data(Qt.UserRole)) is Contact)
689 690
        self.search_list.detail_model.contact = None
        self.search_list.detail_view.hide()
691 692 693 694

    def _SH_SearchListSelectionChanged(self, selected, deselected):
        account_manager = AccountManager()
        selected_items = self.search_list.selectionModel().selectedIndexes()
695
        self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)<=1)
696

697 698 699 700 701 702
    def _SH_ServerToolsAccountModelChanged(self, parent_index, start, end):
        server_tools_enabled = self.server_tools_account_model.rowCount() > 0
        self.sip_server_settings_action.setEnabled(server_tools_enabled)
        self.search_for_people_action.setEnabled(server_tools_enabled)
        self.history_on_server_action.setEnabled(server_tools_enabled)

703
    def _SH_SessionListSelectionChanged(self, selected, deselected):
704
        selected_indexes = selected.indexes()
705
        active_session = selected_indexes[0].data(Qt.UserRole) if selected_indexes else Null
706
        if active_session.client_conference:
707 708 709
            self.conference_button.setEnabled(True)
            self.conference_button.setChecked(True)
        else:
710
            self.conference_button.setEnabled(len([session for session in self.session_model.active_sessions if session.client_conference is None]) > 1)
711
            self.conference_button.setChecked(False)
712

713 714 715
    def _SH_AudioSessionModelAddedSession(self, session_item):
        if len(session_item.blink_session.streams) == 1:
            self.switch_view_button.view = SwitchViewButton.SessionView
716

717 718
    def _SH_AudioSessionModelRemovedSession(self, session_item):
        if self.session_model.rowCount() == 0:
Dan Pascu's avatar
Dan Pascu committed
719 720
            self.switch_view_button.view = SwitchViewButton.ContactView

721
    def _SH_AudioSessionModelChangedStructure(self):
722
        active_sessions = self.session_model.active_sessions
723
        self.active_sessions_label.setText(u'There is 1 active call' if len(active_sessions) == 1 else u'There are %d active calls' % len(active_sessions))
724
        self.active_sessions_label.setVisible(any(active_sessions))
725
        self.hangup_all_button.setEnabled(any(active_sessions))
726
        selected_indexes = self.session_list.selectionModel().selectedIndexes()
727
        active_session = selected_indexes[0].data(Qt.UserRole) if selected_indexes else Null
728
        if active_session.client_conference:
729 730
            self.conference_button.setEnabled(True)
            self.conference_button.setChecked(True)
731
        else:
732
            self.conference_button.setEnabled(len([session for session in active_sessions if session.client_conference is None]) > 1)
733
            self.conference_button.setChecked(False)
734 735 736 737
        if active_sessions:
            if self.account_state.state is not AccountState.Invisible:
                if self.saved_account_state is None:
                    self.saved_account_state = self.account_state.state, self.activity_note.text()
738
                self.account_state.setState(AccountState.Busy.Internal, note=u'On the phone')
739 740 741 742
        elif self.saved_account_state is not None:
            state, note = self.saved_account_state
            self.saved_account_state = None
            self.account_state.setState(state, note)
743

744 745 746 747 748
    def _SH_SilentButtonClicked(self, silent):
        settings = SIPSimpleSettings()
        settings.audio.silent = silent
        settings.save()

749
    def _SH_SwitchViewButtonChangedView(self, view):
750
        self.main_view.setCurrentWidget(self.contacts_panel if view is SwitchViewButton.ContactView else self.sessions_panel)
Dan Pascu's avatar
Dan Pascu committed
751

Dan Pascu's avatar
Dan Pascu committed
752 753
    def _SH_PendingWatcherDialogFinished(self, result):
        self.pending_watcher_dialogs.remove(self.sender())
754

755 756 757 758 759 760
    def _SH_SystemTrayIconActivated(self, reason):
        if reason == QSystemTrayIcon.Trigger:
            self.show()
            self.raise_()
            self.activateWindow()

761 762 763 764 765 766
    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPApplicationWillStart(self, notification):
767
        account_manager = AccountManager()
768
        settings = SIPSimpleSettings()
769
        self.silent_action.setChecked(settings.audio.silent)
770
        self.silent_button.setChecked(settings.audio.silent)
Dan Pascu's avatar
Dan Pascu committed
771 772
        self.answering_machine_action.setChecked(settings.answering_machine.enabled)
        self.auto_accept_chat_action.setChecked(settings.chat.auto_accept)
773
        self.received_messages_sound_action.setChecked(settings.sounds.play_message_alerts)
Dan Pascu's avatar
Dan Pascu committed
774
        if settings.google_contacts.enabled:
775
            self.google_contacts_action.setText(u'Disable &Google Contacts')
Dan Pascu's avatar
Dan Pascu committed
776 777
        else:
            self.google_contacts_action.setText(u'Enable &Google Contacts...')
778
        self.google_contacts_action.triggered.connect(self._AH_GoogleContactsActionTriggered)
779
        if not any(account.enabled for account in account_manager.iter_accounts()):
780
            self.display_name.setEnabled(False)
781
            self.activity_note.setEnabled(False)
782
            self.account_state.setEnabled(False)
783

784 785
    def _NH_SIPApplicationDidStart(self, notification):
        self.load_audio_devices()
Dan Pascu's avatar
Dan Pascu committed
786
        self.load_video_devices()
787 788
        notification.center.add_observer(self, name='CFGSettingsObjectDidChange')
        notification.center.add_observer(self, name='AudioDevicesDidChange')
789
        notification.center.add_observer(self, name='VideoDevicesDidChange')
Dan Pascu's avatar
Dan Pascu committed
790 791 792 793
        blink_settings = BlinkSettings()
        self.account_state.history = [(item.state, item.note) for item in blink_settings.presence.state_history]
        state = getattr(AccountState, blink_settings.presence.current_state.state, AccountState.Available)
        self.account_state.setState(state, blink_settings.presence.current_state.note)
794 795

    def _NH_AudioDevicesDidChange(self, notification):
796 797 798
        self.output_device_menu.clear()  # because actions are owned by the menu and only referenced by their corresponding action groups,
        self.input_device_menu.clear()   # clearing the menus will result in the actions automatically disappearing from the corresponding
        self.alert_device_menu.clear()   # action groups as well
799
        if self.session_model.active_sessions:
800
            added_devices = set(notification.data.new_devices).difference(notification.data.old_devices)
801 802 803 804 805 806
            if added_devices:
                new_device = added_devices.pop()
                settings = SIPSimpleSettings()
                settings.audio.input_device = new_device
                settings.audio.output_device = new_device
                settings.save()
807 808
        self.load_audio_devices()

809 810 811 812 813 814 815 816 817 818
    def _NH_VideoDevicesDidChange(self, notification):
        self.video_camera_menu.clear()  # actions will be removed automatically from the action group because they are owned by the menu and only referenced in the action group
        if self.session_model.active_sessions:
            added_devices = set(notification.data.new_devices).difference(notification.data.old_devices)
            if added_devices:
                settings = SIPSimpleSettings()
                settings.video.device = added_devices.pop()
                settings.save()
        self.load_video_devices()

819
    def _NH_CFGSettingsObjectDidChange(self, notification):
820
        settings = SIPSimpleSettings()
821
        blink_settings = BlinkSettings()
Dan Pascu's avatar
Dan Pascu committed
822
        icon_manager = IconManager()
823
        if notification.sender is settings:
824 825 826
            if 'audio.muted' in notification.data.modified:
                self.mute_action.setChecked(settings.audio.muted)
                self.mute_button.setChecked(settings.audio.muted)
827
            if 'audio.silent' in notification.data.modified:
828
                self.silent_action.setChecked(settings.audio.silent)
829 830
                self.silent_button.setChecked(settings.audio.silent)
            if 'audio.output_device' in notification.data.modified:
831
                action = next(action for action in self.output_devices_group.actions() if action.data() == settings.audio.output_device)
832 833
                action.setChecked(True)
            if 'audio.input_device' in notification.data.modified:
834
                action = next(action for action in self.input_devices_group.actions() if action.data() == settings.audio.input_device)
835 836
                action.setChecked(True)
            if 'audio.alert_device' in notification.data.modified:
837
                action = next(action for action in self.alert_devices_group.actions() if action.data() == settings.audio.alert_device)
838
                action.setChecked(True)
Dan Pascu's avatar
Dan Pascu committed
839
            if 'video.device' in notification.data.modified:
840
                action = next(action for action in self.video_devices_group.actions() if action.data() == settings.video.device)
Dan Pascu's avatar
Dan Pascu committed
841
                action.setChecked(True)
Dan Pascu's avatar
Dan Pascu committed
842 843 844 845
            if 'answering_machine.enabled' in notification.data.modified:
                self.answering_machine_action.setChecked(settings.answering_machine.enabled)
            if 'chat.auto_accept' in notification.data.modified:
                self.auto_accept_chat_action.setChecked(settings.chat.auto_accept)
846 847
            if 'sounds.play_message_alerts' in notification.data.modified:
                self.received_messages_sound_action.setChecked(settings.sounds.play_message_alerts)
Dan Pascu's avatar
Dan Pascu committed
848 849
            if 'google_contacts.enabled' in notification.data.modified:
                if notification.sender.google_contacts.enabled:
850
                    self.google_contacts_action.setText(u'Disable &Google Contacts')
Dan Pascu's avatar
Dan Pascu committed
851 852
                else:
                    self.google_contacts_action.setText(u'Enable &Google Contacts...')
853 854 855 856 857
        elif notification.sender is blink_settings:
            if 'presence.current_state' in notification.data.modified:
                state = getattr(AccountState, blink_settings.presence.current_state.state, AccountState.Available)
                self.account_state.setState(state, blink_settings.presence.current_state.note)
            if 'presence.icon' in notification.data.modified:
Dan Pascu's avatar
Dan Pascu committed
858
                self.set_user_icon(icon_manager.get('avatar'))
859 860 861
            if 'presence.offline_note' in notification.data.modified:
                # TODO: set offline note -Saul
                pass
Luci Stanescu's avatar
Luci Stanescu committed
862
        elif isinstance(notification.sender, (Account, BonjourAccount)):
Dan Pascu's avatar
Dan Pascu committed
863
            account_manager = AccountManager()
864
            account = notification.sender
865
            if 'enabled' in notification.data.modified:
866
                action = next(action for action in self.accounts_menu.actions() if action.data() is account)
867
                action.setChecked(account.enabled)
Dan Pascu's avatar
Dan Pascu committed
868 869
            if 'display_name' in notification.data.modified and account is account_manager.default_account:
                self.display_name.setText(account.display_name or u'')
870 871
            if {'enabled', 'message_summary.enabled', 'message_summary.voicemail_uri'}.intersection(notification.data.modified):
                action = next(action for action in self.voicemail_menu.actions() if action.data() is account)
872
                action.setVisible(False if account is BonjourAccount() else account.enabled and account.message_summary.enabled)
873
                action.setEnabled(False if account is BonjourAccount() else account.voicemail_uri is not None)
874 875 876

    def _NH_SIPAccountManagerDidAddAccount(self, notification):
        account = notification.data.account
Dan Pascu's avatar
Dan Pascu committed
877 878

        action = self.accounts_menu.addAction(account.id if account is not BonjourAccount() else u'Bonjour')
879
        action.setEnabled(True if account is not BonjourAccount() else BonjourAccount.mdns_available)
880
        action.setCheckable(True)
881
        action.setChecked(account.enabled)
882
        action.setData(account)
883
        action.triggered.connect(self._AH_AccountActionTriggered)
Dan Pascu's avatar
Dan Pascu committed
884 885

        action = self.voicemail_menu.addAction(self.mwi_icons[0], account.id)
886
        action.setVisible(False if account is BonjourAccount() else account.enabled and account.message_summary.enabled)
887
        action.setEnabled(False if account is BonjourAccount() else account.voicemail_uri is not None)
888
        action.setData(account)
889
        action.triggered.connect(self._AH_VoicemailActionTriggered)
890 891 892

    def _NH_SIPAccountManagerDidRemoveAccount(self, notification):
        account = notification.data.account
893
        action = next(action for action in self.accounts_menu.actions() if action.data() is account)
Saul Ibarra's avatar
Saul Ibarra committed
894
        self.accounts_menu.removeAction(action)
895
        action = next(action for action in self.voicemail_menu.actions() if action.data() is account)
896
        self.voicemail_menu.removeAction(action)
897 898 899 900 901 902

    def _NH_SIPAccountManagerDidChangeDefaultAccount(self, notification):
        if notification.data.account is None:
            self.enable_call_buttons(False)
        else:
            selected_items = self.contact_list.selectionModel().selectedIndexes()
903
            self.enable_call_buttons(len(selected_items) == 1 and isinstance(selected_items[0].data(Qt.UserRole), Contact))
904

905
    def _NH_SIPAccountGotMessageSummary(self, notification):
906 907
        account = notification.sender
        summary = notification.data.message_summary
908
        action = next(action for action in self.voicemail_menu.actions() if action.data() is account)
909
        action.setEnabled(account.voicemail_uri is not None)
910 911 912 913 914
        if summary.messages_waiting:
            try:
                new_messages = limit(int(summary.summaries['voice-message']['new_messages']), min=0, max=11)
            except (KeyError, ValueError):
                new_messages = 0
915
        else:
916 917
            new_messages = 0
        action.setIcon(self.mwi_icons[new_messages])
918

919 920
    def _NH_SIPAccountGotPendingWatcher(self, notification):
        dialog = PendingWatcherDialog(notification.sender, notification.data.uri, notification.data.display_name)
Dan Pascu's avatar
Dan Pascu committed
921
        dialog.finished.connect(self._SH_PendingWatcherDialogFinished)
922 923 924
        self.pending_watcher_dialogs.append(dialog)
        dialog.show()

925 926 927 928 929 930
    def _NH_BlinkSessionNewOutgoing(self, notification):
        self.search_box.clear()

    def _NH_BlinkSessionDidReinitializeForOutgoing(self, notification):
        self.search_box.clear()

Dan Pascu's avatar
Dan Pascu committed
931 932 933 934
    def _NH_BlinkSessionTransferNewOutgoing(self, notification):
        self.search_box.clear()
        self.switch_view_button.view = SwitchViewButton.SessionView

935
    def _NH_BlinkFileTransferNewIncoming(self, notification):
Saul Ibarra's avatar
Saul Ibarra committed
936 937
        self.filetransfer_window.show(activate=QApplication.activeWindow() is not None)

938
    def _NH_BlinkFileTransferNewOutgoing(self, notification):
Saul Ibarra's avatar
Saul Ibarra committed
939
        self.filetransfer_window.show(activate=QApplication.activeWindow() is not None)
940

Dan Pascu's avatar
Dan Pascu committed
941 942
del ui_class, base_class