preferences.py 95.8 KB
Newer Older
Dan Pascu's avatar
Dan Pascu committed
1 2

import os
Adrian Georgescu's avatar
Adrian Georgescu committed
3
import urllib.parse
Dan Pascu's avatar
Dan Pascu committed
4

Dan Pascu's avatar
Dan Pascu committed
5 6 7 8
from PyQt5 import uic
from PyQt5.QtCore import Qt, QEvent, QRegExp
from PyQt5.QtGui import QFont, QRegExpValidator, QValidator
from PyQt5.QtWidgets import QActionGroup, QButtonGroup, QFileDialog, QListView, QListWidgetItem, QMessageBox, QSpinBox, QStyle, QStyleOptionComboBox, QStyledItemDelegate
Dan Pascu's avatar
Dan Pascu committed
9 10 11

from application import log
from application.notification import IObserver, NotificationCenter
12
from application.python import Null, limit
13
from gnutls.crypto import X509Certificate, X509PrivateKey
Dan Pascu's avatar
Dan Pascu committed
14
from gnutls.errors import GNUTLSError
15
from zope.interface import implementer
Dan Pascu's avatar
Dan Pascu committed
16

Dan Pascu's avatar
Dan Pascu committed
17
from sipsimple.account import AccountManager, BonjourAccount
Dan Pascu's avatar
Dan Pascu committed
18 19
from sipsimple.application import SIPApplication
from sipsimple.configuration import DefaultValue
20
from sipsimple.configuration.datatypes import H264Profile, MSRPRelayAddress, Path, PortRange, SIPProxyAddress, STUNServerAddress, STUNServerAddressList
Dan Pascu's avatar
Dan Pascu committed
21
from sipsimple.configuration.settings import SIPSimpleSettings
22
from sipsimple.threading import run_in_thread
Dan Pascu's avatar
Dan Pascu committed
23 24

from blink.accounts import AddAccountDialog
Dan Pascu's avatar
Dan Pascu committed
25 26 27
from blink.chatwindow import ChatMessageStyle, ChatStyleError, ChatMessage, ChatEvent, ChatSender
from blink.configuration.datatypes import FileURL
from blink.configuration.settings import BlinkSettings
Dan Pascu's avatar
Dan Pascu committed
28 29
from blink.resources import ApplicationData, Resources
from blink.logging import LogManager
30
from blink.util import QSingleton, call_in_gui_thread, run_in_gui_thread
Dan Pascu's avatar
Dan Pascu committed
31 32


33 34
__all__ = ['PreferencesWindow', 'AccountListView', 'SIPPortEditor']

Dan Pascu's avatar
Dan Pascu committed
35 36 37 38 39

# LineEdit and ComboBox validators
#
class IDDPrefixValidator(QRegExpValidator):
    def __init__(self, parent=None):
Adrian Georgescu's avatar
Adrian Georgescu committed
40
        super(IDDPrefixValidator, self).__init__(QRegExp('[0-9+*#]+'), parent)
Dan Pascu's avatar
Dan Pascu committed
41 42

    def fixup(self, input):
Adrian Georgescu's avatar
Adrian Georgescu committed
43
        return super(IDDPrefixValidator, self).fixup(input or '+')
Dan Pascu's avatar
Dan Pascu committed
44 45 46 47


class PrefixValidator(QRegExpValidator):
    def __init__(self, parent=None):
Adrian Georgescu's avatar
Adrian Georgescu committed
48
        super(PrefixValidator, self).__init__(QRegExp('(None|[0-9+*#]+)'), parent)
Dan Pascu's avatar
Dan Pascu committed
49 50

    def fixup(self, input):
Adrian Georgescu's avatar
Adrian Georgescu committed
51
        return super(PrefixValidator, self).fixup(input or 'None')
Dan Pascu's avatar
Dan Pascu committed
52 53


54 55
class HostnameValidator(QRegExpValidator):
    def __init__(self, parent=None):
Adrian Georgescu's avatar
Adrian Georgescu committed
56
        super(HostnameValidator, self).__init__(QRegExp('^([\w\-_]+(\.[\w\-_]+)*)?$', Qt.CaseInsensitive), parent)
57 58


Dan Pascu's avatar
Dan Pascu committed
59 60
class SIPAddressValidator(QRegExpValidator):
    def __init__(self, parent=None):
Adrian Georgescu's avatar
Adrian Georgescu committed
61
        super(SIPAddressValidator, self).__init__(QRegExp('^([\w\-_+%]+@[\w\-_]+(\.[\w\-_]+)*)?$', Qt.CaseInsensitive), parent)
Dan Pascu's avatar
Dan Pascu committed
62 63 64 65

    def fixup(self, input):
        if input and '@' not in input:
            preferences_window = self.parent()
Adrian Georgescu's avatar
Adrian Georgescu committed
66
            input += '@%s' % preferences_window.selected_account.id.domain
Dan Pascu's avatar
Dan Pascu committed
67 68 69 70 71
        return super(SIPAddressValidator, self).fixup(input)


class WebURLValidator(QRegExpValidator):
    def __init__(self, parent=None):
Adrian Georgescu's avatar
Adrian Georgescu committed
72
        super(WebURLValidator, self).__init__(QRegExp('^(https?://[\w\-_]+(\.[\w\-_]+)*(:\d+)?(/.*)?)?$', Qt.CaseInsensitive), parent)
Dan Pascu's avatar
Dan Pascu committed
73 74 75 76


class XCAPRootValidator(WebURLValidator):
    def fixup(self, input):
Adrian Georgescu's avatar
Adrian Georgescu committed
77
        url = urllib.parse.urlparse(input)
Dan Pascu's avatar
Dan Pascu committed
78
        if not (url.scheme and url.netloc):
Adrian Georgescu's avatar
Adrian Georgescu committed
79
            input = ''
Dan Pascu's avatar
Dan Pascu committed
80 81 82
        return super(XCAPRootValidator, self).fixup(input)

    def validate(self, input, pos):
83
        state, input, pos = super(XCAPRootValidator, self).validate(input, pos)
Dan Pascu's avatar
Dan Pascu committed
84
        if state == QValidator.Acceptable:
85
            if input.endswith(('?', ';', '&')):
Dan Pascu's avatar
Dan Pascu committed
86 87
                state = QValidator.Invalid
            else:
Adrian Georgescu's avatar
Adrian Georgescu committed
88
                url = urllib.parse.urlparse(input)
Dan Pascu's avatar
Dan Pascu committed
89 90 91 92 93 94
                if url.params or url.query or url.fragment:
                    state = QValidator.Invalid
                elif url.port is not None:
                    port = int(url.port)
                    if not (0 < port <= 65535):
                        state = QValidator.Invalid
95
        return state, input, pos
Dan Pascu's avatar
Dan Pascu committed
96 97 98 99 100 101 102 103


# Custom widgets used in preferences.ui
#
class SIPPortEditor(QSpinBox):
    def __init__(self, parent=None):
        super(SIPPortEditor, self).__init__(parent)
        self.setRange(0, 65535)
104
        self.sibling = Null  # if there is a sibling port, its value is invalid for this port
Dan Pascu's avatar
Dan Pascu committed
105 106 107 108 109

    def stepBy(self, steps):
        value = self.value()
        sibling_value = self.sibling.value()
        if value+steps == sibling_value != 0:
110
            steps += steps/abs(steps)  # add one more unit in the right direction
Dan Pascu's avatar
Dan Pascu committed
111 112 113 114 115 116
        if 0 < value+steps < 1024:
            if steps < 0:
                steps = -value
            else:
                steps = 1024 - value
        if value+steps == sibling_value != 0:
117
            steps += steps/abs(steps)  # add one more unit in the right direction
Dan Pascu's avatar
Dan Pascu committed
118 119 120
        return super(SIPPortEditor, self).stepBy(steps)

    def validate(self, input, pos):
121
        state, input, pos = super(SIPPortEditor, self).validate(input, pos)
Dan Pascu's avatar
Dan Pascu committed
122 123 124 125 126 127
        if state == QValidator.Acceptable:
            value = int(input)
            if 0 < value < 1024:
                state = QValidator.Intermediate
            elif value == self.sibling.value() != 0:
                state = QValidator.Intermediate
128
        return state, input, pos
Dan Pascu's avatar
Dan Pascu committed
129 130


131 132 133 134 135 136 137 138
class AccountDelegate(QStyledItemDelegate):
    def paint(self, painter, option, index):
        account_info = index.data(Qt.UserRole)
        if not account_info.account.enabled:
            option.state &= ~QStyle.State_Enabled
        super(AccountDelegate, self).paint(painter, option, index)


Dan Pascu's avatar
Dan Pascu committed
139 140 141
class AccountListView(QListView):
    def __init__(self, parent=None):
        super(AccountListView, self).__init__(parent)
142
        self.setItemDelegate(AccountDelegate(self))
143
        # self.setDropIndicatorShown(False)
Dan Pascu's avatar
Dan Pascu committed
144

145 146
    def selectionChanged(self, selected, deselected):
        super(AccountListView, self).selectionChanged(selected, deselected)
Dan Pascu's avatar
Dan Pascu committed
147 148 149 150 151 152 153
        selection_model = self.selectionModel()
        selection = selection_model.selection()
        if selection_model.currentIndex() not in selection:
            index = selection.indexes()[0] if not selection.isEmpty() else self.model().index(-1)
            selection_model.setCurrentIndex(index, selection_model.Select)


154 155 156
class blocked_qt_signals(object):
    def __init__(self, qobject):
        self.qobject = qobject
157

158 159 160
    def __enter__(self):
        self.qobject.blockSignals(True)
        return self.qobject
161

162 163 164 165 166
    def __exit__(self, type, value, traceback):
        self.qobject.blockSignals(False)


class UnspecifiedOutboundProxy(object):
Adrian Georgescu's avatar
Adrian Georgescu committed
167
    host = ''
168
    port = 5060
Adrian Georgescu's avatar
Adrian Georgescu committed
169
    transport = 'UDP'
170 171 172


class UnspecifiedMSRPRelay(object):
Adrian Georgescu's avatar
Adrian Georgescu committed
173
    host = ''
174
    port = 0
Adrian Georgescu's avatar
Adrian Georgescu committed
175
    transport = 'TLS'
176 177


Dan Pascu's avatar
Dan Pascu committed
178 179
ui_class, base_class = uic.loadUiType(Resources.get('preferences.ui'))

180

181
@implementer(IObserver)
Adrian Georgescu's avatar
Adrian Georgescu committed
182
class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):
Dan Pascu's avatar
Dan Pascu committed
183 184 185 186 187 188 189 190 191 192 193 194

    def __init__(self, account_model, parent=None):
        super(PreferencesWindow, self).__init__(parent)

        with Resources.directory:
            self.setupUi()

        self.setWindowTitle('Blink Preferences')

        self.account_list.setModel(account_model)
        self.delete_account_button.setEnabled(False)

Dan Pascu's avatar
Dan Pascu committed
195 196
        self.camera_preview.installEventFilter(self)

Dan Pascu's avatar
Dan Pascu committed
197 198 199 200 201 202 203 204 205 206 207
        notification_center = NotificationCenter()
        notification_center.add_observer(self, name='SIPApplicationDidStart')

        # Dialogs
        self.add_account_dialog = AddAccountDialog(self)

        # Signals
        self.toolbar.actionTriggered.connect(self._SH_ToolbarActionTriggered)

        # Account
        self.account_list.selectionModel().selectionChanged.connect(self._SH_AccountListSelectionChanged)
208
        self.account_list.model().dataChanged.connect(self._SH_AccountListDataChanged)
Dan Pascu's avatar
Dan Pascu committed
209 210 211 212 213
        self.add_account_button.clicked.connect(self.show_add_account_dialog)
        self.delete_account_button.clicked.connect(self._SH_DeleteAccountButtonClicked)

        # Account information
        self.account_enabled_button.clicked.connect(self._SH_AccountEnabledButtonClicked)
214 215
        self.account_enabled_presence_button.clicked.connect(self._SH_AccountEnabledPresenceButtonClicked)
        self.account_enabled_mwi_button.clicked.connect(self._SH_AccountEnabledMWIButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
216 217 218 219 220
        self.display_name_editor.editingFinished.connect(self._SH_DisplayNameEditorEditingFinished)
        self.password_editor.editingFinished.connect(self._SH_PasswordEditorEditingFinished)

        # Account media settings
        self.account_audio_codecs_list.itemChanged.connect(self._SH_AccountAudioCodecsListItemChanged)
221
        self.account_audio_codecs_list.model().rowsMoved.connect(self._SH_AccountAudioCodecsListModelRowsMoved)
Dan Pascu's avatar
Dan Pascu committed
222
        self.reset_account_audio_codecs_button.clicked.connect(self._SH_ResetAudioCodecsButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
223 224 225
        self.account_video_codecs_list.itemChanged.connect(self._SH_AccountVideoCodecsListItemChanged)
        self.account_video_codecs_list.model().rowsMoved.connect(self._SH_AccountVideoCodecsListModelRowsMoved)
        self.reset_account_video_codecs_button.clicked.connect(self._SH_ResetVideoCodecsButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
226
        self.inband_dtmf_button.clicked.connect(self._SH_InbandDTMFButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
227 228
        self.rtp_encryption_button.clicked.connect(self._SH_RTPEncryptionButtonClicked)
        self.key_negotiation_button.activated[int].connect(self._SH_KeyNegotiationButtonActivated)
Dan Pascu's avatar
Dan Pascu committed
229 230 231 232 233 234 235 236 237

        # Account server settings
        self.always_use_my_proxy_button.clicked.connect(self._SH_AlwaysUseMyProxyButtonClicked)
        self.outbound_proxy_host_editor.editingFinished.connect(self._SH_OutboundProxyHostEditorEditingFinished)
        self.outbound_proxy_port.valueChanged[int].connect(self._SH_OutboundProxyPortValueChanged)
        self.outbound_proxy_transport_button.activated[str].connect(self._SH_OutboundProxyTransportButtonActivated)
        self.auth_username_editor.editingFinished.connect(self._SH_AuthUsernameEditorEditingFinished)
        self.always_use_my_msrp_relay_button.clicked.connect(self._SH_AlwaysUseMyMSRPRelayButtonClicked)
        self.msrp_relay_host_editor.editingFinished.connect(self._SH_MSRPRelayHostEditorEditingFinished)
238
        self.stun_server_list_editor.editingFinished.connect(self._SH_StunServerListEditorEditingFinished)
Dan Pascu's avatar
Dan Pascu committed
239 240 241 242 243
        self.msrp_relay_port.valueChanged[int].connect(self._SH_MSRPRelayPortValueChanged)
        self.msrp_relay_transport_button.activated[str].connect(self._SH_MSRPRelayTransportButtonActivated)
        self.voicemail_uri_editor.editingFinished.connect(self._SH_VoicemailURIEditorEditingFinished)
        self.xcap_root_editor.editingFinished.connect(self._SH_XCAPRootEditorEditingFinished)
        self.server_tools_url_editor.editingFinished.connect(self._SH_ServerToolsURLEditorEditingFinished)
244
        self.conference_server_editor.editingFinished.connect(self._SH_ConferenceServerEditorEditingFinished)
Dan Pascu's avatar
Dan Pascu committed
245

246
        # Account NAT traversal settings
Dan Pascu's avatar
Dan Pascu committed
247 248 249 250 251 252 253 254 255 256
        self.use_ice_button.clicked.connect(self._SH_UseICEButtonClicked)
        self.msrp_transport_button.activated[str].connect(self._SH_MSRPTransportButtonActivated)

        # Account advanced settings
        self.register_interval.valueChanged[int].connect(self._SH_RegisterIntervalValueChanged)
        self.publish_interval.valueChanged[int].connect(self._SH_PublishIntervalValueChanged)
        self.subscribe_interval.valueChanged[int].connect(self._SH_SubscribeIntervalValueChanged)
        self.reregister_button.clicked.connect(self._SH_ReregisterButtonClicked)
        self.idd_prefix_button.activated[str].connect(self._SH_IDDPrefixButtonActivated)
        self.prefix_button.activated[str].connect(self._SH_PrefixButtonActivated)
257
        self.account_tls_name_editor.editingFinished.connect(self._SH_TLSPeerNameEditorEditingFinished)
Dan Pascu's avatar
Dan Pascu committed
258

259
        # Account sms settings
260 261
        self.message_cpim_enabled_button.clicked.connect(self._SH_EnableMessageCPIMButtonClicked)
        self.message_iscomposing_enabled_button.clicked.connect(self._SH_EnableMessageIsComposingButtonClicked)
262
        self.message_imdn_enabled_button.clicked.connect(self._SH_EnableMessageIMDNButtonClicked)
263
        self.message_add_unknown_contacts_button.clicked.connect(self._SH_AddUnknownContactsButtonClicked)
264
        self.message_pgp_enabled_button.clicked.connect(self._SH_EnablePGPButtonClicked)
265
        self.message_replication_button.clicked.connect(self._SH_MessageReplicationButtonClicked)
266 267 268
        self.message_synchronization_button.clicked.connect(self._SH_MessageSynchronizationButtonClicked)
        self.history_url_editor.editingFinished.connect(self._SH_HistoryUrlEditorEditingFinshed)
        self.last_id_editor.editingFinished.connect(self._SH_LastIdEditorEditingFinished)
269

Dan Pascu's avatar
Dan Pascu committed
270 271 272 273 274 275
        # Audio devices
        self.audio_alert_device_button.activated[int].connect(self._SH_AudioAlertDeviceButtonActivated)
        self.audio_input_device_button.activated[int].connect(self._SH_AudioInputDeviceButtonActivated)
        self.audio_output_device_button.activated[int].connect(self._SH_AudioOutputDeviceButtonActivated)
        self.audio_sample_rate_button.activated[str].connect(self._SH_AudioSampleRateButtonActivated)
        self.enable_echo_cancelling_button.clicked.connect(self._SH_EnableEchoCancellingButtonClicked)
276
        self.tail_length_slider.valueChanged.connect(self._SH_TailLengthSliderValueChanged)
Dan Pascu's avatar
Dan Pascu committed
277 278 279

        # Audio codecs
        self.audio_codecs_list.itemChanged.connect(self._SH_AudioCodecsListItemChanged)
280
        self.audio_codecs_list.model().rowsMoved.connect(self._SH_AudioCodecsListModelRowsMoved)
Dan Pascu's avatar
Dan Pascu committed
281 282 283 284 285 286

        # Answering machine
        self.enable_answering_machine_button.clicked.connect(self._SH_EnableAnsweringMachineButtonClicked)
        self.answer_delay.valueChanged[int].connect(self._SH_AnswerDelayValueChanged)
        self.max_recording.valueChanged[int].connect(self._SH_MaxRecordingValueChanged)

Dan Pascu's avatar
Dan Pascu committed
287 288 289 290 291 292 293 294 295 296 297
        # Video devices
        self.video_camera_button.activated[int].connect(self._SH_VideoCameraButtonActivated)
        self.video_resolution_button.activated[int].connect(self._SH_VideoResolutionButtonActivated)
        self.video_framerate_button.activated[int].connect(self._SH_VideoFramerateButtonActivated)

        # Video codecs
        self.video_codecs_list.itemChanged.connect(self._SH_VideoCodecsListItemChanged)
        self.video_codecs_list.model().rowsMoved.connect(self._SH_VideoCodecsListModelRowsMoved)
        self.video_codec_bitrate_button.activated[int].connect(self._SH_VideoCodecBitrateButtonActivated)
        self.h264_profile_button.activated[int].connect(self._SH_H264ProfileButtonActivated)

298
        # Chat
Dan Pascu's avatar
Dan Pascu committed
299 300 301 302 303 304 305 306 307 308 309
        self.style_view.sizeChanged.connect(self._SH_StyleViewSizeChanged)
        self.style_view.page().mainFrame().contentsSizeChanged.connect(self._SH_StyleViewFrameContentsSizeChanged)

        self.style_button.activated[int].connect(self._SH_StyleButtonActivated)
        self.style_variant_button.activated[str].connect(self._SH_StyleVariantButtonActivated)
        self.style_show_icons_button.clicked.connect(self._SH_StyleShowIconsButtonClicked)

        self.style_font_button.currentIndexChanged[str].connect(self._SH_StyleFontButtonCurrentIndexChanged)
        self.style_font_size.valueChanged[int].connect(self._SH_StyleFontSizeValueChanged)
        self.style_default_font_button.clicked.connect(self._SH_StyleDefaultFontButtonClicked)

Dan Pascu's avatar
Dan Pascu committed
310
        self.auto_accept_chat_button.clicked.connect(self._SH_AutoAcceptChatButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
311
        self.chat_message_alert_button.clicked.connect(self._SH_ChatMessageAlertButtonClicked)
312
        self.sms_replication_button.clicked.connect(self._SH_SMSReplicationButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
313 314 315

        self.session_info_style_button.clicked.connect(self._SH_SessionInfoStyleButtonClicked)
        self.traffic_units_button.clicked.connect(self._SH_TrafficUnitsButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
316

Dan Pascu's avatar
Dan Pascu committed
317 318 319 320 321
        # Screen sharing
        self.screen_sharing_scale_button.clicked.connect(self._SH_ScreenSharingScaleButtonClicked)
        self.screen_sharing_fullscreen_button.clicked.connect(self._SH_ScreenSharingFullscreenButtonClicked)
        self.screen_sharing_viewonly_button.clicked.connect(self._SH_ScreenSharingViewonlyButtonClicked)

Dan Pascu's avatar
Dan Pascu committed
322 323
        # File logging
        self.trace_sip_button.clicked.connect(self._SH_TraceSIPButtonClicked)
324
        self.trace_messaging_button.clicked.connect(self._SH_TraceMessagingButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
        self.trace_msrp_button.clicked.connect(self._SH_TraceMSRPButtonClicked)
        self.trace_xcap_button.clicked.connect(self._SH_TraceXCAPButtonClicked)
        self.trace_notifications_button.clicked.connect(self._SH_TraceNotificationsButtonClicked)
        self.trace_pjsip_button.clicked.connect(self._SH_TracePJSIPButtonClicked)
        self.pjsip_trace_level.valueChanged[int].connect(self._SH_PJSIPTraceLevelValueChanged)
        self.clear_log_files_button.clicked.connect(self._SH_ClearLogFilesButtonClicked)

        # SIP and RTP
        self.sip_transports_button_group.buttonClicked.connect(self._SH_SIPTransportsButtonClicked)
        self.udp_port.valueChanged[int].connect(self._SH_UDPPortValueChanged)
        self.tcp_port.valueChanged[int].connect(self._SH_TCPPortValueChanged)
        self.tls_port.valueChanged[int].connect(self._SH_TLSPortValueChanged)
        self.media_ports_start.valueChanged[int].connect(self._SH_MediaPortsStartValueChanged)
        self.media_ports.valueChanged[int].connect(self._SH_MediaPortsValueChanged)

Dan Pascu's avatar
Dan Pascu committed
340 341 342 343 344
        # Files and directories
        self.screenshots_directory_browse_button.clicked.connect(self._SH_ScreenshotsDirectoryBrowseButtonClicked)
        self.transfers_directory_browse_button.clicked.connect(self._SH_TransfersDirectoryBrowseButtonClicked)

        # TLS settings
Dan Pascu's avatar
Dan Pascu committed
345 346
        self.tls_ca_file_editor.locationCleared.connect(self._SH_TLSCAFileEditorLocationCleared)
        self.tls_ca_file_browse_button.clicked.connect(self._SH_TLSCAFileBrowseButtonClicked)
347 348 349
        self.tls_cert_file_editor.locationCleared.connect(self._SH_TLSCertFileEditorLocationCleared)
        self.tls_cert_file_browse_button.clicked.connect(self._SH_TLSCertFileBrowseButtonClicked)
        self.tls_verify_server_button.clicked.connect(self._SH_TLSVerifyServerButtonClicked)
Dan Pascu's avatar
Dan Pascu committed
350

351 352 353 354
        # Auto answer
        self.auto_answer_interval.valueChanged[int].connect(self._SH_AutoAnswerIntervalChanged)
        self.account_auto_answer.clicked.connect(self._SH_AccountAutoAnswerChanged)

355 356 357
        # Interface
        self.history_name_and_uri_button.clicked.connect(self._SH_HistoryNameAndUriButtonClicked)

358
        # Setup initial state (show the accounts page right after start)
Dan Pascu's avatar
Dan Pascu committed
359 360 361 362 363 364
        self.accounts_action.trigger()
        self.account_tab_widget.setCurrentIndex(0)

    def setupUi(self):
        super(PreferencesWindow, self).setupUi(self)

Dan Pascu's avatar
Dan Pascu committed
365 366
        # Accounts
        self.key_negotiation_button.clear()
Adrian Georgescu's avatar
Adrian Georgescu committed
367 368 369 370
        self.key_negotiation_button.addItem('Opportunistic', 'opportunistic')
        self.key_negotiation_button.addItem('ZRTP', 'zrtp')
        self.key_negotiation_button.addItem('SDES optional', 'sdes_optional')
        self.key_negotiation_button.addItem('SDES mandatory', 'sdes_mandatory')
Dan Pascu's avatar
Dan Pascu committed
371

Dan Pascu's avatar
Dan Pascu committed
372 373
        # Audio

374 375 376 377 378
        # Hide the tail_length slider as it is only useful for debugging -Dan
        self.tail_length_label.hide()
        self.tail_length_slider.hide()
        self.tail_length_value_label.hide()

Dan Pascu's avatar
Dan Pascu committed
379
        # Hide the controls for the features that are not yet implemented -Dan
380
        self.answering_machine_group_box.hide()
Dan Pascu's avatar
Dan Pascu committed
381 382
        self.sms_replication_button.hide()

Dan Pascu's avatar
Dan Pascu committed
383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
        # Video
        size_policy = self.camera_preview.sizePolicy()
        size_policy.setHeightForWidth(True)
        self.camera_preview.setSizePolicy(size_policy)
        self.camera_preview.mirror = True

        self.video_resolution_button.clear()
        self.video_resolution_button.addItem('HD 720p', '1280x720')
        self.video_resolution_button.addItem('VGA', '640x480')
        self.h264_level_map = {'1280x720': '3.1', '640x480': '3.0'}

        self.video_framerate_button.clear()
        for rate in range(10, 31, 5):
            self.video_framerate_button.addItem('%d fps' % rate, rate)

        self.video_codec_bitrate_button.clear()
        self.video_codec_bitrate_button.addItem('automatic', None)
        for bitrate in (1.0, 2.0, 4.0):
            self.video_codec_bitrate_button.addItem('%g Mbps' % bitrate, bitrate)

        self.h264_profile_button.clear()
        for profile in H264Profile.valid_values:
            self.h264_profile_button.addItem(profile, profile)

        # Chat
Dan Pascu's avatar
Dan Pascu committed
408 409 410 411 412 413 414 415 416 417 418 419 420
        self.style_view.template = open(Resources.get('chat/template.html')).read()

        self.style_button.clear()
        self.style_variant_button.clear()

        styles_path = Resources.get('chat/styles')
        for style_name in os.listdir(styles_path):
            try:
                style = ChatMessageStyle(style_name)
            except ChatStyleError:
                pass
            else:
                self.style_button.addItem(style_name, style)
421

Dan Pascu's avatar
Dan Pascu committed
422 423 424 425 426 427
        self.section_group = QActionGroup(self)
        self.section_group.setExclusive(True)
        for index, action in enumerate(action for action in self.toolbar.actions() if not action.isSeparator()):
            action.index = index
            self.section_group.addAction(action)

Adrian Georgescu's avatar
Adrian Georgescu committed
428
        for index in range(self.idd_prefix_button.count()):
429
            text = self.idd_prefix_button.itemText(index)
430
            self.idd_prefix_button.setItemData(index, None if text == "+" else text)
Adrian Georgescu's avatar
Adrian Georgescu committed
431
        for index in range(self.prefix_button.count()):
432
            text = self.prefix_button.itemText(index)
433
            self.prefix_button.setItemData(index, None if text == "None" else text)
Dan Pascu's avatar
Dan Pascu committed
434 435 436 437

        self.voicemail_uri_editor.setValidator(SIPAddressValidator(self))
        self.xcap_root_editor.setValidator(XCAPRootValidator(self))
        self.server_tools_url_editor.setValidator(WebURLValidator(self))
438
        self.conference_server_editor.setValidator(HostnameValidator(self))
Dan Pascu's avatar
Dan Pascu committed
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
        self.idd_prefix_button.setValidator(IDDPrefixValidator(self))
        self.prefix_button.setValidator(PrefixValidator(self))

        # Adding the button group in designer has issues on Ubuntu 10.04
        self.sip_transports_button_group = QButtonGroup(self)
        self.sip_transports_button_group.setObjectName("sip_transports_button_group")
        self.sip_transports_button_group.setExclusive(False)
        self.sip_transports_button_group.addButton(self.enable_udp_button)
        self.sip_transports_button_group.addButton(self.enable_tcp_button)
        self.sip_transports_button_group.addButton(self.enable_tls_button)

        self.enable_udp_button.name = 'udp'
        self.enable_tcp_button.name = 'tcp'
        self.enable_tls_button.name = 'tls'

        self.tcp_port.sibling = self.tls_port
        self.tls_port.sibling = self.tcp_port

        # Adjust some minimum label widths in order to better align settings in different group boxes, widgets or tabs

        # account server and network tab
460
        font_metrics = self.outbound_proxy_label.fontMetrics()  # we assume all labels have the same font
Dan Pascu's avatar
Dan Pascu committed
461 462
        labels = (self.outbound_proxy_label, self.auth_username_label, self.msrp_relay_label,
                  self.voicemail_uri_label, self.xcap_root_label, self.server_tools_url_label,
463
                  self.conference_server_label, self.msrp_transport_label)
Dan Pascu's avatar
Dan Pascu committed
464 465 466 467 468
        text_width = max(font_metrics.width(label.text()) for label in labels) + 15
        self.outbound_proxy_label.setMinimumWidth(text_width)
        self.msrp_transport_label.setMinimumWidth(text_width)

        # account advanced tab
469
        font_metrics = self.register_interval_label.fontMetrics()  # we assume all labels have the same font
Dan Pascu's avatar
Dan Pascu committed
470
        labels = (self.register_interval_label, self.publish_interval_label, self.subscribe_interval_label,
471
                  self.idd_prefix_label, self.prefix_label)
Dan Pascu's avatar
Dan Pascu committed
472 473 474
        text_width = max(font_metrics.width(label.text()) for label in labels) + 15
        self.register_interval_label.setMinimumWidth(text_width)
        self.idd_prefix_label.setMinimumWidth(text_width)
475
        self.tls_cert_file_label.setMinimumWidth(text_width)
Dan Pascu's avatar
Dan Pascu committed
476 477

        # audio settings
478
        font_metrics = self.answer_delay_label.fontMetrics()  # we assume all labels have the same font
Dan Pascu's avatar
Dan Pascu committed
479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
        labels = (self.audio_input_device_label, self.audio_output_device_label, self.audio_alert_device_label, self.audio_sample_rate_label,
                  self.answer_delay_label, self.max_recording_label, self.unavailable_message_label)
        text_width = max(font_metrics.width(label.text()) for label in labels)
        self.audio_input_device_label.setMinimumWidth(text_width)
        self.answer_delay_label.setMinimumWidth(text_width)

        # Adjust the combo boxes for themes with too much padding (like the default theme on Ubuntu 10.04)
        combo_box = self.audio_input_device_button
        option = QStyleOptionComboBox()
        combo_box.initStyleOption(option)
        wide_padding = (combo_box.height() - combo_box.style().subControlRect(QStyle.CC_ComboBox, option, QStyle.SC_ComboBoxEditField, combo_box).height() >= 10)
        if False and wide_padding: # TODO: review later and decide if its worth or not -Dan
            self.audio_alert_device_button.setStyleSheet("""QComboBox { padding: 4px 4px 4px 4px; }""")
            self.audio_input_device_button.setStyleSheet("""QComboBox { padding: 4px 4px 4px 4px; }""")
            self.audio_output_device_button.setStyleSheet("""QComboBox { padding: 4px 4px 4px 4px; }""")
            self.audio_sample_rate_button.setStyleSheet("""QComboBox { padding: 4px 4px 4px 4px; }""")
            self.unavailable_message_button.setStyleSheet("""QComboBox { padding: 4px 4px 4px 4px; }""")

497 498
        self.history_url_editor.setValidator(WebURLValidator(self))

Dan Pascu's avatar
Dan Pascu committed
499 500 501 502 503 504 505 506 507
    def eventFilter(self, watched, event):
        if watched is self.camera_preview:
            event_type = event.type()
            if event_type == QEvent.Show:
                self.camera_preview.producer = SIPApplication.video_device.producer
            elif event_type == QEvent.Hide:
                self.camera_preview.producer = None
        return False

Dan Pascu's avatar
Dan Pascu committed
508 509 510 511 512 513 514 515
    def closeEvent(self, event):
        super(PreferencesWindow, self).closeEvent(event)
        self.add_account_dialog.close()

    @property
    def account_msrp_relay(self):
        host = self.msrp_relay_host_editor.text()
        port = self.msrp_relay_port.value()
516
        transport = self.msrp_relay_transport_button.currentText().lower()
Dan Pascu's avatar
Dan Pascu committed
517 518 519 520 521 522
        return MSRPRelayAddress(host, port, transport) if host else None

    @property
    def account_outbound_proxy(self):
        host = self.outbound_proxy_host_editor.text()
        port = self.outbound_proxy_port.value()
523
        transport = self.outbound_proxy_transport_button.currentText().lower()
Dan Pascu's avatar
Dan Pascu committed
524 525 526 527 528 529 530 531 532
        return SIPProxyAddress(host, port, transport) if host else None

    @property
    def selected_account(self):
        try:
            selected_index = self.account_list.selectionModel().selectedIndexes()[0]
        except IndexError:
            return None
        else:
533
            return selected_index.data(Qt.UserRole).account
Dan Pascu's avatar
Dan Pascu committed
534 535 536 537 538

    def _sync_defaults(self):
        settings = SIPSimpleSettings()
        account_manager = AccountManager()

Dan Pascu's avatar
Dan Pascu committed
539
        if settings.rtp.audio_codec_order is not SIPSimpleSettings.rtp.audio_codec_order.default or settings.rtp.audio_codec_list is not SIPSimpleSettings.rtp.audio_codec_list.default:
540
            # user has a non-default codec order, we need to sync with the new settings
Dan Pascu's avatar
Dan Pascu committed
541 542
            added_codecs = set(SIPSimpleSettings.rtp.audio_codec_order.default).difference(settings.rtp.audio_codec_order)
            removed_codecs = set(settings.rtp.audio_codec_order).difference(SIPSimpleSettings.rtp.audio_codec_order.default)
Dan Pascu's avatar
Dan Pascu committed
543
            if added_codecs:
544 545
                settings.rtp.audio_codec_order = DefaultValue  # reset codec order
                settings.rtp.audio_codec_list  = DefaultValue  # reset codec list
Dan Pascu's avatar
Dan Pascu committed
546 547 548 549
                settings.save()
            elif removed_codecs:
                codec_order = [codec for codec in settings.rtp.audio_codec_order if codec not in removed_codecs]
                codec_list  = [codec for codec in settings.rtp.audio_codec_list if codec not in removed_codecs]
Dan Pascu's avatar
Dan Pascu committed
550
                if codec_order == SIPSimpleSettings.rtp.audio_codec_order.default:
Dan Pascu's avatar
Dan Pascu committed
551
                    codec_order = DefaultValue
Dan Pascu's avatar
Dan Pascu committed
552
                if codec_list == SIPSimpleSettings.rtp.audio_codec_list.default:
Dan Pascu's avatar
Dan Pascu committed
553 554
                    codec_list = DefaultValue
                settings.rtp.audio_codec_order = codec_order
Dan Pascu's avatar
Dan Pascu committed
555
                settings.rtp.audio_codec_list  = codec_list
Dan Pascu's avatar
Dan Pascu committed
556 557 558
                settings.save()

        for account in (account for account in account_manager.iter_accounts() if account.rtp.audio_codec_order is not None):
559
            # user has a non-default codec order, we need to sync with the new settings
Dan Pascu's avatar
Dan Pascu committed
560 561
            added_codecs = set(SIPSimpleSettings.rtp.audio_codec_order.default).difference(account.rtp.audio_codec_order)
            removed_codecs = set(account.rtp.audio_codec_order).difference(SIPSimpleSettings.rtp.audio_codec_order.default)
Dan Pascu's avatar
Dan Pascu committed
562
            if added_codecs:
563 564
                account.rtp.audio_codec_order = DefaultValue  # reset codec order
                account.rtp.audio_codec_list  = DefaultValue  # reset codec list
Dan Pascu's avatar
Dan Pascu committed
565 566 567 568
                account.save()
            elif removed_codecs:
                codec_order = [codec for codec in account.rtp.audio_codec_order if codec not in removed_codecs]
                codec_list  = [codec for codec in account.rtp.audio_codec_list if codec not in removed_codecs]
Dan Pascu's avatar
Dan Pascu committed
569
                if codec_order == SIPSimpleSettings.rtp.audio_codec_order.default and codec_list == SIPSimpleSettings.rtp.audio_codec_list.default:
Dan Pascu's avatar
Dan Pascu committed
570 571 572
                    codec_order = DefaultValue
                    codec_list  = DefaultValue
                account.rtp.audio_codec_order = codec_order
Dan Pascu's avatar
Dan Pascu committed
573 574 575 576 577 578 579 580
                account.rtp.audio_codec_list  = codec_list
                account.save()

        if settings.rtp.video_codec_order is not SIPSimpleSettings.rtp.video_codec_order.default or settings.rtp.video_codec_list is not SIPSimpleSettings.rtp.video_codec_list.default:
            # user has a non-default codec order, we need to sync with the new settings
            added_codecs = set(SIPSimpleSettings.rtp.video_codec_order.default).difference(settings.rtp.video_codec_order)
            removed_codecs = set(settings.rtp.video_codec_order).difference(SIPSimpleSettings.rtp.video_codec_order.default)
            if added_codecs:
581 582
                settings.rtp.video_codec_order = DefaultValue  # reset codec order
                settings.rtp.video_codec_list  = DefaultValue  # reset codec list
Dan Pascu's avatar
Dan Pascu committed
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
                settings.save()
            elif removed_codecs:
                codec_order = [codec for codec in settings.rtp.video_codec_order if codec not in removed_codecs]
                codec_list  = [codec for codec in settings.rtp.video_codec_list if codec not in removed_codecs]
                if codec_order == SIPSimpleSettings.rtp.video_codec_order.default:
                    codec_order = DefaultValue
                if codec_list == SIPSimpleSettings.rtp.video_codec_list.default:
                    codec_list = DefaultValue
                settings.rtp.video_codec_order = codec_order
                settings.rtp.video_codec_list  = codec_list
                settings.save()

        for account in (account for account in account_manager.iter_accounts() if account.rtp.video_codec_order is not None):
            # user has a non-default codec order, we need to sync with the new settings
            added_codecs = set(SIPSimpleSettings.rtp.video_codec_order.default).difference(account.rtp.video_codec_order)
            removed_codecs = set(account.rtp.video_codec_order).difference(SIPSimpleSettings.rtp.video_codec_order.default)
            if added_codecs:
600 601
                account.rtp.video_codec_order = DefaultValue  # reset codec order
                account.rtp.video_codec_list  = DefaultValue  # reset codec list
Dan Pascu's avatar
Dan Pascu committed
602 603 604 605 606 607 608 609 610
                account.save()
            elif removed_codecs:
                codec_order = [codec for codec in account.rtp.video_codec_order if codec not in removed_codecs]
                codec_list  = [codec for codec in account.rtp.video_codec_list if codec not in removed_codecs]
                if codec_order == SIPSimpleSettings.rtp.video_codec_order.default and codec_list == SIPSimpleSettings.rtp.video_codec_list.default:
                    codec_order = DefaultValue
                    codec_list  = DefaultValue
                account.rtp.video_codec_order = codec_order
                account.rtp.video_codec_list  = codec_list
Dan Pascu's avatar
Dan Pascu committed
611 612 613 614 615 616 617 618
                account.save()

    def load_audio_devices(self):
        settings = SIPSimpleSettings()

        class Separator: pass

        self.audio_input_device_button.clear()
Adrian Georgescu's avatar
Adrian Georgescu committed
619
        self.audio_input_device_button.addItem('System Default', 'system_default')
Dan Pascu's avatar
Dan Pascu committed
620
        self.audio_input_device_button.insertSeparator(1)
621
        self.audio_input_device_button.setItemData(1, Separator)  # prevent the separator from being selected (must have different itemData than the None device)
Dan Pascu's avatar
Dan Pascu committed
622
        for device in SIPApplication.engine.input_devices:
623
            self.audio_input_device_button.addItem(device, device)
Adrian Georgescu's avatar
Adrian Georgescu committed
624
        self.audio_input_device_button.addItem('None', None)
625
        self.audio_input_device_button.setCurrentIndex(self.audio_input_device_button.findData(settings.audio.input_device))
Dan Pascu's avatar
Dan Pascu committed
626 627

        self.audio_output_device_button.clear()
Adrian Georgescu's avatar
Adrian Georgescu committed
628
        self.audio_output_device_button.addItem('System Default', 'system_default')
Dan Pascu's avatar
Dan Pascu committed
629
        self.audio_output_device_button.insertSeparator(1)
630
        self.audio_output_device_button.setItemData(1, Separator)  # prevent the separator from being selected (must have different itemData than the None device)
Dan Pascu's avatar
Dan Pascu committed
631
        for device in SIPApplication.engine.output_devices:
632
            self.audio_output_device_button.addItem(device, device)
Adrian Georgescu's avatar
Adrian Georgescu committed
633
        self.audio_output_device_button.addItem('None', None)
634
        self.audio_output_device_button.setCurrentIndex(self.audio_output_device_button.findData(settings.audio.output_device))
Dan Pascu's avatar
Dan Pascu committed
635 636

        self.audio_alert_device_button.clear()
Adrian Georgescu's avatar
Adrian Georgescu committed
637
        self.audio_alert_device_button.addItem('System Default', 'system_default')
Dan Pascu's avatar
Dan Pascu committed
638
        self.audio_alert_device_button.insertSeparator(1)
639
        self.audio_alert_device_button.setItemData(1, Separator)  # prevent the separator from being selected (must have different itemData than the None device)
Dan Pascu's avatar
Dan Pascu committed
640
        for device in SIPApplication.engine.output_devices:
641
            self.audio_alert_device_button.addItem(device, device)
Adrian Georgescu's avatar
Adrian Georgescu committed
642
        self.audio_alert_device_button.addItem('None', None)
643
        self.audio_alert_device_button.setCurrentIndex(self.audio_alert_device_button.findData(settings.audio.alert_device))
Dan Pascu's avatar
Dan Pascu committed
644

Dan Pascu's avatar
Dan Pascu committed
645 646 647 648 649 650
    def load_video_devices(self):
        settings = SIPSimpleSettings()

        class Separator: pass

        self.video_camera_button.clear()
Adrian Georgescu's avatar
Adrian Georgescu committed
651
        self.video_camera_button.addItem('System Default', 'system_default')
Dan Pascu's avatar
Dan Pascu committed
652
        self.video_camera_button.insertSeparator(1)
653
        self.video_camera_button.setItemData(1, Separator)  # prevent the separator from being selected (must have different itemData than the None device)
Dan Pascu's avatar
Dan Pascu committed
654 655
        for device in SIPApplication.engine.video_devices:
            self.video_camera_button.addItem(device, device)
Adrian Georgescu's avatar
Adrian Georgescu committed
656
        self.video_camera_button.addItem('None', None)
Dan Pascu's avatar
Dan Pascu committed
657 658
        self.video_camera_button.setCurrentIndex(self.video_camera_button.findData(settings.video.device))

Dan Pascu's avatar
Dan Pascu committed
659 660 661
    def load_settings(self):
        """Load settings from configuration into the UI controls"""
        settings = SIPSimpleSettings()
Dan Pascu's avatar
Dan Pascu committed
662
        blink_settings = BlinkSettings()
Dan Pascu's avatar
Dan Pascu committed
663 664 665

        # Audio devices
        self.load_audio_devices()
666
        self.enable_echo_cancelling_button.setChecked(settings.audio.echo_canceller.enabled)
667 668
        with blocked_qt_signals(self.tail_length_slider):
            self.tail_length_slider.setValue(settings.audio.echo_canceller.tail_length)
Dan Pascu's avatar
Dan Pascu committed
669 670
        self.audio_sample_rate_button.clear()
        for rate in SIPSimpleSettings.audio.sample_rate.type.valid_values:
671
            self.audio_sample_rate_button.addItem(str(rate), rate)
Dan Pascu's avatar
Dan Pascu committed
672 673 674
        self.audio_sample_rate_button.setCurrentIndex(self.audio_sample_rate_button.findText(str(settings.audio.sample_rate)))

        # Audio codecs
675 676 677 678 679
        with blocked_qt_signals(self.audio_codecs_list):
            self.audio_codecs_list.clear()
            for codec in settings.rtp.audio_codec_order:
                item = QListWidgetItem(codec, self.audio_codecs_list)
                item.setCheckState(Qt.Checked if codec in settings.rtp.audio_codec_list else Qt.Unchecked)
Dan Pascu's avatar
Dan Pascu committed
680

681
        # Answering Machine settings
Dan Pascu's avatar
Dan Pascu committed
682
        self.enable_answering_machine_button.setChecked(settings.answering_machine.enabled)
683 684 685 686
        with blocked_qt_signals(self.answer_delay):
            self.answer_delay.setValue(settings.answering_machine.answer_delay)
        with blocked_qt_signals(self.max_recording):
            self.max_recording.setValue(settings.answering_machine.max_recording)
Dan Pascu's avatar
Dan Pascu committed
687 688
        # TODO: load unavailable message -Dan

Dan Pascu's avatar
Dan Pascu committed
689 690 691
        # Video devices
        self.load_video_devices()

Adrian Georgescu's avatar
Adrian Georgescu committed
692
        self.video_resolution_button.setCurrentIndex(self.video_resolution_button.findData(str(settings.video.resolution)))
Dan Pascu's avatar
Dan Pascu committed
693 694 695 696 697 698 699 700 701
        self.video_framerate_button.setCurrentIndex(self.video_framerate_button.findData(settings.video.framerate))

        # Video codecs
        with blocked_qt_signals(self.video_codecs_list):
            self.video_codecs_list.clear()
            for codec in settings.rtp.video_codec_order:
                item = QListWidgetItem(codec, self.video_codecs_list)
                item.setCheckState(Qt.Checked if codec in settings.rtp.video_codec_list else Qt.Unchecked)

Adrian Georgescu's avatar
Adrian Georgescu committed
702
        self.h264_profile_button.setCurrentIndex(self.h264_profile_button.findData(str(settings.video.h264.profile)))
Dan Pascu's avatar
Dan Pascu committed
703 704
        self.video_codec_bitrate_button.setCurrentIndex(self.video_codec_bitrate_button.findData(settings.video.max_bitrate))

705
        # Chat
Dan Pascu's avatar
Dan Pascu committed
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731
        style_index = self.style_button.findText(blink_settings.chat_window.style)
        if style_index == -1:
            style_index = 0
            blink_settings.chat_window.style = self.style_button.itemText(style_index)
            blink_settings.chat_window.style_variant = None
            blink_settings.save()
        style = self.style_button.itemData(style_index)
        self.style_button.setCurrentIndex(style_index)
        self.style_variant_button.clear()
        for variant in style.variants:
            self.style_variant_button.addItem(variant)
        variant_index = self.style_variant_button.findText(blink_settings.chat_window.style_variant or style.default_variant)
        if variant_index == -1:
            variant_index = self.style_variant_button.findText(style.default_variant)
            blink_settings.chat_window.style_variant = None
            blink_settings.save()
        self.style_variant_button.setCurrentIndex(variant_index)
        self.style_show_icons_button.setChecked(blink_settings.chat_window.show_user_icons)
        self.update_chat_preview()

        with blocked_qt_signals(self.style_font_button):
            self.style_font_button.setCurrentFont(QFont(blink_settings.chat_window.font or style.font_family))
        with blocked_qt_signals(self.style_font_size):
            self.style_font_size.setValue(blink_settings.chat_window.font_size or style.font_size)
        self.style_default_font_button.setEnabled(blink_settings.chat_window.font is not None or blink_settings.chat_window.font_size is not None)

Dan Pascu's avatar
Dan Pascu committed
732
        self.auto_accept_chat_button.setChecked(settings.chat.auto_accept)
Dan Pascu's avatar
Dan Pascu committed
733
        self.chat_message_alert_button.setChecked(settings.sounds.play_message_alerts)
734
        self.sms_replication_button.setChecked(settings.chat.sms_replication)
Dan Pascu's avatar
Dan Pascu committed
735 736 737

        self.session_info_style_button.setChecked(blink_settings.chat_window.session_info.alternate_style)
        self.traffic_units_button.setChecked(blink_settings.chat_window.session_info.bytes_per_second)
Dan Pascu's avatar
Dan Pascu committed
738

Dan Pascu's avatar
Dan Pascu committed
739 740 741 742 743
        # Screen sharing settings
        self.screen_sharing_scale_button.setChecked(blink_settings.screen_sharing.scale)
        self.screen_sharing_fullscreen_button.setChecked(blink_settings.screen_sharing.open_fullscreen)
        self.screen_sharing_viewonly_button.setChecked(blink_settings.screen_sharing.open_viewonly)

Dan Pascu's avatar
Dan Pascu committed
744 745
        # File logging settings
        self.trace_sip_button.setChecked(settings.logs.trace_sip)
746
        self.trace_messaging_button.setChecked(settings.logs.trace_messaging)
Dan Pascu's avatar
Dan Pascu committed
747 748 749 750
        self.trace_msrp_button.setChecked(settings.logs.trace_msrp)
        self.trace_xcap_button.setChecked(settings.logs.trace_xcap)
        self.trace_notifications_button.setChecked(settings.logs.trace_notifications)
        self.trace_pjsip_button.setChecked(settings.logs.trace_pjsip)
751 752
        with blocked_qt_signals(self.pjsip_trace_level):
            self.pjsip_trace_level.setValue(limit(settings.logs.pjsip_level, min=0, max=5))
Dan Pascu's avatar
Dan Pascu committed
753 754 755 756 757

        # Advanced settings
        for button in self.sip_transports_button_group.buttons():
            button.setChecked(button.name in settings.sip.transport_list)

758
        if settings.sip.tcp_port and settings.sip.tcp_port == settings.sip.tls_port:
Dan Pascu's avatar
Dan Pascu committed
759
            log.warning("the SIP TLS and TCP ports cannot be the same")
Tijmen de Mes's avatar
Tijmen de Mes committed
760
            settings.sip.tls_port = settings.sip.tcp_port + 1 if settings.sip.tcp_port < 65535 else 65534
Dan Pascu's avatar
Dan Pascu committed
761 762
            settings.save()

763 764 765 766 767 768 769 770 771 772
        with blocked_qt_signals(self.udp_port):
            self.udp_port.setValue(settings.sip.udp_port)
        with blocked_qt_signals(self.tcp_port):
            self.tcp_port.setValue(settings.sip.tcp_port)
        with blocked_qt_signals(self.tls_port):
            self.tls_port.setValue(settings.sip.tls_port)
        with blocked_qt_signals(self.media_ports_start):
            self.media_ports_start.setValue(settings.rtp.port_range.start)
        with blocked_qt_signals(self.media_ports):
            self.media_ports.setValue(settings.rtp.port_range.end - settings.rtp.port_range.start)
773 774
        with blocked_qt_signals(self.auto_answer_interval):
            self.auto_answer_interval.setValue(settings.sip.auto_answer_interval)
775

Adrian Georgescu's avatar
Adrian Georgescu committed
776 777 778
        self.screenshots_directory_editor.setText(blink_settings.screenshots_directory or '')
        self.transfers_directory_editor.setText(blink_settings.transfers_directory or '')
        self.tls_ca_file_editor.setText(settings.tls.ca_list or '')
779 780
        self.tls_cert_file_editor.setText(settings.tls.certificate or '')
        self.tls_verify_server_button.setChecked(settings.tls.verify_server)
Dan Pascu's avatar
Dan Pascu committed
781

782 783
        self.history_name_and_uri_button.setChecked(blink_settings.interface.show_history_name_and_uri)

Dan Pascu's avatar
Dan Pascu committed
784 785 786 787 788 789 790 791
    def load_account_settings(self, account):
        """Load the account settings from configuration into the UI controls"""
        settings = SIPSimpleSettings()
        bonjour_account = BonjourAccount()

        # Account information tab
        self.account_enabled_button.setChecked(account.enabled)
        self.account_enabled_button.setEnabled(True if account is not bonjour_account else BonjourAccount.mdns_available)
792 793 794 795 796 797

        self.account_enabled_presence_button.setEnabled(account is not bonjour_account)
        self.account_enabled_presence_button.setChecked(account.presence.enabled if account is not bonjour_account else False)

        self.account_enabled_mwi_button.setEnabled(account is not bonjour_account)
        self.account_enabled_mwi_button.setChecked(account.message_summary.enabled if account is not bonjour_account else False)
Tijmen de Mes's avatar
Tijmen de Mes committed
798

Adrian Georgescu's avatar
Adrian Georgescu committed
799
        self.display_name_editor.setText(account.display_name or '')
800

Dan Pascu's avatar
Dan Pascu committed
801 802
        if account is not bonjour_account:
            self.password_editor.setText(account.auth.password)
803 804
            selected_index = self.account_list.selectionModel().selectedIndexes()[0]
            selected_account_info = self.account_list.model().data(selected_index, Qt.UserRole)
805 806 807
            if not account.enabled:
                selected_account_info.registration_state = None
                selected_account_info.registrar = None
Tijmen de Mes's avatar
Tijmen de Mes committed
808

809
            if selected_account_info.registration_state:
810 811 812 813
                if selected_account_info.registration_state == 'succeeded' and selected_account_info.registrar is not None:
                    self.account_registration_label.setText('Registered at %s' % selected_account_info.registrar)
                else:
                    self.account_registration_label.setText('Registration %s' % selected_account_info.registration_state.title())
814
            else:
Adrian Georgescu's avatar
Adrian Georgescu committed
815
                self.account_registration_label.setText('Not Registered')
816
        else:
Adrian Georgescu's avatar
Adrian Georgescu committed
817
            self.account_registration_label.setText('')
Dan Pascu's avatar
Dan Pascu committed
818 819

        # Media tab
820 821 822 823 824 825 826
        with blocked_qt_signals(self.account_audio_codecs_list):
            self.account_audio_codecs_list.clear()
            audio_codec_order = account.rtp.audio_codec_order or settings.rtp.audio_codec_order
            audio_codec_list = account.rtp.audio_codec_list or settings.rtp.audio_codec_list
            for codec in audio_codec_order:
                item = QListWidgetItem(codec, self.account_audio_codecs_list)
                item.setCheckState(Qt.Checked if codec in audio_codec_list else Qt.Unchecked)
Dan Pascu's avatar
Dan Pascu committed
827 828 829 830 831 832 833 834 835

        with blocked_qt_signals(self.account_video_codecs_list):
            self.account_video_codecs_list.clear()
            video_codec_order = account.rtp.video_codec_order or settings.rtp.video_codec_order
            video_codec_list = account.rtp.video_codec_list or settings.rtp.video_codec_list
            for codec in video_codec_order:
                item = QListWidgetItem(codec, self.account_video_codecs_list)
                item.setCheckState(Qt.Checked if codec in video_codec_list else Qt.Unchecked)

Dan Pascu's avatar
Dan Pascu committed
836
        self.reset_account_audio_codecs_button.setEnabled(account.rtp.audio_codec_order is not None)
Dan Pascu's avatar
Dan Pascu committed
837
        self.reset_account_video_codecs_button.setEnabled(account.rtp.video_codec_order is not None)
Dan Pascu's avatar
Dan Pascu committed
838 839

        self.inband_dtmf_button.setChecked(account.rtp.inband_dtmf)
Dan Pascu's avatar
Dan Pascu committed
840 841 842
        self.rtp_encryption_button.setChecked(account.rtp.encryption.enabled)
        self.key_negotiation_button.setEnabled(account.rtp.encryption.enabled)
        self.key_negotiation_button.setCurrentIndex(self.key_negotiation_button.findData(account.rtp.encryption.key_negotiation))
Tijmen de Mes's avatar
Tijmen de Mes committed
843

844
        self.account_auto_answer.setChecked(account.sip.auto_answer)
Dan Pascu's avatar
Dan Pascu committed
845

846 847 848
        # SMS settings tab, also relevant for bonjour
        self.message_cpim_enabled_button.setChecked(account.sms.use_cpim)
        self.message_iscomposing_enabled_button.setChecked(account.sms.enable_iscomposing)
849 850
        self.message_imdn_enabled_button.setEnabled(account.sms.use_cpim)
        self.message_imdn_enabled_button.setChecked(account.sms.enable_imdn)
851
        self.message_add_unknown_contacts_button.setChecked(account.sms.add_unknown_contacts)
852
        self.message_pgp_enabled_button.setChecked(account.sms.enable_pgp)
853

Dan Pascu's avatar
Dan Pascu committed
854
        if account is not bonjour_account:
855
            self.account_auto_answer.setText('Auto answer from allowed contacts')
Dan Pascu's avatar
Dan Pascu committed
856 857
            # Server settings tab
            self.always_use_my_proxy_button.setChecked(account.sip.always_use_my_proxy)
858 859
            outbound_proxy = account.sip.outbound_proxy or UnspecifiedOutboundProxy
            self.outbound_proxy_host_editor.setText(outbound_proxy.host)
860 861 862 863 864
            if account.nat_traversal.stun_server_list:
                stun_server_list = ", ".join('%s:%s' % (s.host, s.port) for s in account.nat_traversal.stun_server_list)
            else:
                stun_server_list = ""
            self.stun_server_list_editor.setText(stun_server_list)
Tijmen de Mes's avatar
Tijmen de Mes committed
865

866 867 868
            with blocked_qt_signals(self.outbound_proxy_port):
                self.outbound_proxy_port.setValue(outbound_proxy.port)
            self.outbound_proxy_transport_button.setCurrentIndex(self.outbound_proxy_transport_button.findText(outbound_proxy.transport.upper()))
Adrian Georgescu's avatar
Adrian Georgescu committed
869
            self.auth_username_editor.setText(account.auth.username or '')
870

Dan Pascu's avatar
Dan Pascu committed
871
            self.always_use_my_msrp_relay_button.setChecked(account.nat_traversal.use_msrp_relay_for_outbound)
872 873 874 875 876 877
            msrp_relay = account.nat_traversal.msrp_relay or UnspecifiedMSRPRelay
            self.msrp_relay_host_editor.setText(msrp_relay.host)
            with blocked_qt_signals(self.msrp_relay_port):
                self.msrp_relay_port.setValue(msrp_relay.port)
            self.msrp_relay_transport_button.setCurrentIndex(self.msrp_relay_transport_button.findText(msrp_relay.transport.upper()))

Adrian Georgescu's avatar
Adrian Georgescu committed
878 879 880 881
            self.voicemail_uri_editor.setText(account.message_summary.voicemail_uri or '')
            self.xcap_root_editor.setText(account.xcap.xcap_root or '')
            self.server_tools_url_editor.setText(account.server.settings_url or '')
            self.conference_server_editor.setText(account.server.conference_server or '')
Dan Pascu's avatar
Dan Pascu committed
882 883 884 885 886 887

            # Network tab
            self.use_ice_button.setChecked(account.nat_traversal.use_ice)
            self.msrp_transport_button.setCurrentIndex(self.msrp_transport_button.findText(account.msrp.transport.upper()))

            # Advanced tab
888 889
            self.account_tls_name_editor.setText(account.sip.tls_name or account.id.domain)

890 891 892 893 894 895
            with blocked_qt_signals(self.register_interval):
                self.register_interval.setValue(account.sip.register_interval)
            with blocked_qt_signals(self.publish_interval):
                self.publish_interval.setValue(account.sip.publish_interval)
            with blocked_qt_signals(self.subscribe_interval):
                self.subscribe_interval.setValue(account.sip.subscribe_interval)
Dan Pascu's avatar
Dan Pascu committed
896 897 898 899 900 901 902 903 904 905 906 907 908 909
            self.reregister_button.setEnabled(account.enabled)

            item_text = account.pstn.idd_prefix or '+'
            index = self.idd_prefix_button.findText(item_text)
            if index == -1:
                self.idd_prefix_button.addItem(item_text)
            self.idd_prefix_button.setCurrentIndex(self.idd_prefix_button.findText(item_text))

            item_text = account.pstn.prefix or 'None'
            index = self.prefix_button.findText(item_text)
            if index == -1:
                self.prefix_button.addItem(item_text)
            self.prefix_button.setCurrentIndex(self.prefix_button.findText(item_text))
            self._update_pstn_example_label()
910 911 912

            # Messages tab
            self.message_replication_button.show()
913 914 915 916 917 918 919
            self.message_synchronization_button.show()
            self.history_url_editor.show()
            self.history_url_label.show()
            self.last_id_editor.show()
            self.last_id_label.show()
            self.history_label.show()
            self.history_line.show()
920
            self.message_replication_button.setChecked(account.sms.enable_message_replication)
921 922 923 924 925
            self.message_synchronization_button.setChecked(account.sms.enable_history_synchronization)
            self.history_url_editor.setEnabled(account.sms.enable_history_synchronization)
            self.history_url_editor.setText(account.sms.history_synchronization_url)
            self.last_id_editor.setEnabled(account.sms.enable_history_synchronization)
            self.last_id_editor.setText(account.sms.history_synchronization_id)
926 927
        else:
            self.account_auto_answer.setText('Auto answer from all neighbours')
928

929
            self.message_replication_button.hide()
930 931 932 933 934 935 936
            self.message_synchronization_button.hide()
            self.history_url_editor.hide()
            self.history_url_label.hide()
            self.last_id_editor.hide()
            self.last_id_label.hide()
            self.history_label.hide()
            self.history_line.hide()
937

Dan Pascu's avatar
Dan Pascu committed
938 939 940 941 942
    def update_chat_preview(self):
        blink_settings = BlinkSettings()

        style = self.style_button.itemData(self.style_button.currentIndex())
        style_variant = self.style_variant_button.itemText(self.style_variant_button.currentIndex())
943 944
        font_family = blink_settings.chat_window.font or style.font_family
        font_size = blink_settings.chat_window.font_size or style.font_size
Dan Pascu's avatar
Dan Pascu committed
945 946
        user_icons = 'show-icons' if blink_settings.chat_window.show_user_icons else 'hide-icons'

Tijmen de Mes's avatar
Tijmen de Mes committed
947
        self.style_view.setHtml(self.style_view.template.format(base_url=FileURL(style.path) + '/', style_url=style_variant+'.style', font_family=font_family, font_size=font_size))
Dan Pascu's avatar
Dan Pascu committed
948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977
        chat_element = self.style_view.page().mainFrame().findFirstElement('#chat')
        chat_element.last_message = None

        def add_message(message):
            insertion_point = chat_element.findFirst('#insert')
            if message.is_related_to(chat_element.last_message):
                message.consecutive = True
                insertion_point.replace(message.to_html(style, user_icons=user_icons))
            else:
                insertion_point.removeFromDocument()
                chat_element.appendInside(message.to_html(style, user_icons=user_icons))
            chat_element.last_message = message

        ruby = ChatSender("Ruby", 'ruby@example.com', Resources.get('icons/avatar-ruby.png'))
        nate = ChatSender("Nate", 'nate@example.net', Resources.get('icons/avatar-nate.png'))

        messages = [ChatMessage("Andrew stepped into the room cautiously. The air was stale as if the place has not been visited in years and he had an acute feeling of being watched. "
                                "Was this the place he was looking for, the place holding the answers he looked for so long? He was hopeful but felt uneasy about it.", ruby, 'incoming'),
                    ChatMessage("Hey Ruby. Is this from the new book you're working on? Looks like it will be another interesting story to read :)", nate, 'outgoing'),
                    ChatMessage("Yeah. But I'm kind of lacking inspiration right now and the book needs to be finished in a month :(", ruby, 'incoming'),
                    ChatMessage("I think you put too much pressure on yourself. What about we get out for a bit? Watch a movie, chat about everyday events for a bit...", nate, 'outgoing'),
                    ChatMessage("It could help you take your mind off of things and relax. We can meet at the usual spot in an hour if you want.", nate, 'outgoing'),
                    ChatMessage("You may be right. Maybe that's what I need indeed. See you there.", ruby, 'incoming'),
                    ChatEvent("Ruby has left the conversation")]

        for message in messages:
            add_message(message)

        del chat_element.last_message

Dan Pascu's avatar
Dan Pascu committed
978 979 980 981 982 983 984
    def show(self):
        selection_model = self.account_list.selectionModel()
        if not selection_model.selectedIndexes():
            model = self.account_list.model()
            account_manager = AccountManager()
            default_account = account_manager.default_account
            try:
985
                index = next(index for index, account in enumerate(model.accounts) if account is default_account)
Dan Pascu's avatar
Dan Pascu committed
986 987 988 989 990
            except StopIteration:
                index = 0
            selection_model.select(model.index(index), selection_model.ClearAndSelect)
        self._update_logs_size_label()
        super(PreferencesWindow, self).show()
991 992
        self.raise_()
        self.activateWindow()
Dan Pascu's avatar
Dan Pascu committed
993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030

    def show_for_accounts(self):
        self.accounts_action.trigger()
        self.show()

    def show_add_account_dialog(self):
        self.add_account_dialog.open_for_add()

    def show_create_account_dialog(self):
        self.add_account_dialog.open_for_create()

    @staticmethod
    def _normalize_binary_size(size):
        """Return a human friendly string representation of size as a power of 2"""
        infinite = float('infinity')
        boundaries = [(             1024, '%d bytes',               1),
                      (          10*1024, '%.2f KB',           1024.0),  (     1024*1024, '%.1f KB',           1024.0),
                      (     10*1024*1024, '%.2f MB',      1024*1024.0),  (1024*1024*1024, '%.1f MB',      1024*1024.0),
                      (10*1024*1024*1024, '%.2f GB', 1024*1024*1024.0),  (      infinite, '%.1f GB', 1024*1024*1024.0)]
        for boundary, format, divisor in boundaries:
            if size < boundary:
                return format % (size/divisor,)
        else:
            return "%d bytes" % size

    def _update_logs_size_label(self):
        logs_size = 0
        for path, dirs, files in os.walk(os.path.join(ApplicationData.directory, 'logs')):
            for name in dirs:
                try:
                    logs_size += os.stat(os.path.join(path, name)).st_size
                except (OSError, IOError):
                    pass
            for name in files:
                try:
                    logs_size += os.stat(os.path.join(path, name)).st_size
                except (OSError, IOError):
                    pass
Adrian Georgescu's avatar
Adrian Georgescu committed
1031
        self.log_files_size_label.setText("There are currently %s of log files" % self._normalize_binary_size(logs_size))
Dan Pascu's avatar
Dan Pascu committed
1032 1033 1034 1035

    def _update_pstn_example_label(self):
        prefix = self.prefix_button.currentText()
        idd_prefix = self.idd_prefix_button.currentText()
Adrian Georgescu's avatar
Adrian Georgescu committed
1036
        self.pstn_example_transformed_label.setText("%s%s442079460000" % ('' if prefix == 'None' else prefix, idd_prefix))
Dan Pascu's avatar
Dan Pascu committed
1037

Dan Pascu's avatar
Dan Pascu committed
1038 1039 1040 1041 1042 1043
    def _align_style_preview(self, scroll=False):
        chat_element = self.style_view.page().mainFrame().findFirstElement('#chat')
        widget_height = self.style_view.size().height()
        content_height = chat_element.geometry().height()
        if widget_height > content_height:
            chat_element.setStyleProperty('position', 'relative')
Tijmen de Mes's avatar
Tijmen de Mes committed
1044
            chat_element.setStyleProperty('top', '%dpx' % (widget_height - content_height))
Dan Pascu's avatar
Dan Pascu committed
1045 1046 1047 1048
        else:
            chat_element.setStyleProperty('position', 'static')
            chat_element.setStyleProperty('top', None)
        frame = self.style_view.page().mainFrame()
Tijmen de Mes's avatar
Tijmen de Mes committed
1049
        if scroll or frame.scrollBarMaximum(Qt.Vertical) - frame.scrollBarValue(Qt.Vertical) <= widget_height * 0.2:
Dan Pascu's avatar
Dan Pascu committed
1050 1051 1052
            frame = self.style_view.page().mainFrame()
            frame.setScrollBarValue(Qt.Vertical, frame.scrollBarMaximum(Qt.Vertical))

Dan Pascu's avatar
Dan Pascu committed
1053 1054 1055 1056 1057 1058 1059
    # Signal handlers
    #
    def _SH_ToolbarActionTriggered(self, action):
        if action == self.logging_action:
            self._update_logs_size_label()
        self.pages.setCurrentIndex(action.index)

1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071
    def _NH_SIPRegistrationInfoDidChange(self, notification):
        self.refresh_account_registration_widgets(notification.sender)

    def refresh_account_registration_widgets(self, account):
        try:
            selected_index = self.account_list.selectionModel().selectedIndexes()[0]
        except IndexError:
            return

        selected_account = selected_index.data(Qt.UserRole).account
        if account.id != selected_account.id:
            return
Tijmen de Mes's avatar
Tijmen de Mes committed
1072

1073
        self.load_account_settings(selected_account)
Tijmen de Mes's avatar
Tijmen de Mes committed
1074

Dan Pascu's avatar
Dan Pascu committed
1075 1076 1077 1078 1079 1080 1081
    def _SH_AccountListSelectionChanged(self, selected, deselected):
        try:
            selected_index = self.account_list.selectionModel().selectedIndexes()[0]
        except IndexError:
            self.delete_account_button.setEnabled(False)
            self.account_tab_widget.setEnabled(False)
        else:
1082
            selected_account = selected_index.data(Qt.UserRole).account
Dan Pascu's avatar
Dan Pascu committed
1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
            self.delete_account_button.setEnabled(selected_account is not BonjourAccount())
            tab_widget = self.account_tab_widget
            tab_widget.setEnabled(True)
            if selected_account is BonjourAccount():
                tab_widget.removeTab(tab_widget.indexOf(self.server_settings_tab))
                tab_widget.removeTab(tab_widget.indexOf(self.network_tab))
                tab_widget.removeTab(tab_widget.indexOf(self.advanced_tab))
                self.password_label.hide()
                self.password_editor.hide()
            else:
                if tab_widget.indexOf(self.server_settings_tab) == -1:
Adrian Georgescu's avatar
Adrian Georgescu committed
1094
                    tab_widget.addTab(self.server_settings_tab, "Server Settings")
Dan Pascu's avatar
Dan Pascu committed
1095
                if tab_widget.indexOf(self.network_tab) == -1:
1096
                    tab_widget.addTab(self.network_tab, "NAT Traversal")
Dan Pascu's avatar
Dan Pascu committed
1097
                if tab_widget.indexOf(self.advanced_tab) == -1:
Adrian Georgescu's avatar
Adrian Georgescu committed
1098
                    tab_widget.addTab(self.advanced_tab, "Advanced")
Dan Pascu's avatar
Dan Pascu committed
1099 1100
                self.password_label.show()
                self.password_editor.show()
Adrian Georgescu's avatar
Adrian Georgescu committed
1101 1102
                self.voicemail_uri_editor.inactiveText = "Discovered by subscribing to %s" % selected_account.id
                self.xcap_root_editor.inactiveText = "Taken from the DNS TXT record for xcap.%s" % selected_account.id.domain
Dan Pascu's avatar
Dan Pascu committed
1103 1104
            self.load_account_settings(selected_account)

1105 1106 1107 1108 1109 1110 1111 1112 1113 1114
    def _SH_AccountListDataChanged(self, topLeft, bottomRight):
        try:
            selected_index = self.account_list.selectionModel().selectedIndexes()[0]
        except IndexError:
            pass
        else:
            account_info = self.account_list.model().data(topLeft, Qt.UserRole)
            selected_account_info = self.account_list.model().data(selected_index, Qt.UserRole)
            if selected_account_info is account_info:
                if account_info.registration_state:
Adrian Georgescu's avatar
Adrian Georgescu committed
1115
                    self.account_registration_label.setText('Registration %s' % account_info.registration_state.title())
1116
                else:
Adrian Georgescu's avatar
Adrian Georgescu committed
1117
                    self.account_registration_label.setText('Not Registered')
1118

Dan Pascu's avatar
Dan Pascu committed
1119 1120 1121 1122
    def _SH_DeleteAccountButtonClicked(self):
        model = self.account_list.model()

        selected_index = self.account_list.selectionModel().selectedIndexes()[0]
1123
        selected_account = selected_index.data(Qt.UserRole).account
Dan Pascu's avatar
Dan Pascu committed
1124

Adrian Georgescu's avatar
Adrian Georgescu committed
1125
        title, message = "Remove Account", "Permanently remove account %s?" % selected_account.id
Tijmen de Mes's avatar
Tijmen de Mes committed
1126
        if QMessageBox.question(self, title, message, QMessageBox.Ok | QMessageBox.Cancel) == QMessageBox.Cancel:
Dan Pascu's avatar
Dan Pascu committed
1127 1128 1129 1130 1131 1132
            return

        account_manager = AccountManager()
        if account_manager.default_account is selected_account:
            active_accounts = [account_info.account for account_info in model.accounts if account_info.account.enabled]
            position = active_accounts.index(selected_account)
Tijmen de Mes's avatar
Tijmen de Mes committed
1133 1134
            if position < len(active_accounts) - 1:
                account_manager.default_account = active_accounts[position + 1]
Dan Pascu's avatar
Dan Pascu committed
1135
            elif position > 0:
Tijmen de Mes's avatar
Tijmen de Mes committed
1136
                account_manager.default_account = active_accounts[position - 1]
Dan Pascu's avatar
Dan Pascu committed
1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147
            else:
                account_manager.default_account = None

        selected_account.delete()

    # Account information
    def _SH_AccountEnabledButtonClicked(self, checked):
        account = self.selected_account
        account.enabled = checked
        account.save()

1148 1149 1150 1151 1152 1153 1154 1155 1156 1157
    def _SH_AccountEnabledPresenceButtonClicked(self, checked):
        account = self.selected_account
        account.presence.enabled = checked
        account.save()

    def _SH_AccountEnabledMWIButtonClicked(self, checked):
        account = self.selected_account
        account.message_summary.enabled = checked
        account.save()

Dan Pascu's avatar
Dan Pascu committed
1158 1159
    def _SH_DisplayNameEditorEditingFinished(self):
        account = self.selected_account
1160
        display_name = self.display_name_editor.text() or None
Dan Pascu's avatar
Dan Pascu committed
1161 1162 1163 1164 1165 1166
        if account.display_name != display_name:
            account.display_name = display_name
            account.save()

    def _SH_PasswordEditorEditingFinished(self):
        account = self.selected_account
1167
        password = self.password_editor.text()
Dan Pascu's avatar
Dan Pascu committed
1168 1169 1170 1171 1172 1173
        if account.auth.password != password:
            account.auth.password = password
            account.save()

    # Account media settings
    def _SH_AccountAudioCodecsListItemChanged(self, item):
1174
        account = self.selected_account
Adrian Georgescu's avatar
Adrian Georgescu committed
1175
        items = [self.account_audio_codecs_list.item(row) for row in range(self.account_audio_codecs_list.count())]
1176
        account.rtp.audio_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
1177 1178
        account.rtp.audio_codec_order = [item.text() for item in items]
        account.save()
Dan Pascu's avatar
Dan Pascu committed
1179 1180 1181

    def _SH_AccountAudioCodecsListModelRowsMoved(self, source_parent, source_start, source_end, dest_parent, dest_row):
        account = self.selected_account
Adrian Georgescu's avatar
Adrian Georgescu committed
1182
        items = [self.account_audio_codecs_list.item(row) for row in range(self.account_audio_codecs_list.count())]
1183
        account.rtp.audio_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
Dan Pascu's avatar
Dan Pascu committed
1184
        account.rtp.audio_codec_order = [item.text() for item in items]
Dan Pascu's avatar
Dan Pascu committed
1185 1186 1187 1188 1189 1190
        account.save()

    def _SH_ResetAudioCodecsButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        account = self.selected_account

1191 1192 1193 1194 1195 1196 1197
        with blocked_qt_signals(self.account_audio_codecs_list):
            self.account_audio_codecs_list.clear()
            audio_codec_order = settings.rtp.audio_codec_order
            audio_codec_list = settings.rtp.audio_codec_list
            for codec in audio_codec_order:
                item = QListWidgetItem(codec, self.account_audio_codecs_list)
                item.setCheckState(Qt.Checked if codec in audio_codec_list else Qt.Unchecked)
Dan Pascu's avatar
Dan Pascu committed
1198 1199 1200 1201 1202

        account.rtp.audio_codec_list  = DefaultValue
        account.rtp.audio_codec_order = DefaultValue
        account.save()

Dan Pascu's avatar
Dan Pascu committed
1203 1204
    def _SH_AccountVideoCodecsListItemChanged(self, item):
        account = self.selected_account
Adrian Georgescu's avatar
Adrian Georgescu committed
1205
        items = [self.account_video_codecs_list.item(row) for row in range(self.account_video_codecs_list.count())]
1206
        account.rtp.video_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
Dan Pascu's avatar
Dan Pascu committed
1207 1208 1209 1210 1211
        account.rtp.video_codec_order = [item.text() for item in items]
        account.save()

    def _SH_AccountVideoCodecsListModelRowsMoved(self, source_parent, source_start, source_end, dest_parent, dest_row):
        account = self.selected_account
Adrian Georgescu's avatar
Adrian Georgescu committed
1212
        items = [self.account_video_codecs_list.item(row) for row in range(self.account_video_codecs_list.count())]
1213
        account.rtp.video_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
Dan Pascu's avatar
Dan Pascu committed
1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232
        account.rtp.video_codec_order = [item.text() for item in items]
        account.save()

    def _SH_ResetVideoCodecsButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        account = self.selected_account

        with blocked_qt_signals(self.account_video_codecs_list):
            self.account_video_codecs_list.clear()
            video_codec_order = settings.rtp.video_codec_order
            video_codec_list = settings.rtp.video_codec_list
            for codec in video_codec_order:
                item = QListWidgetItem(codec, self.account_video_codecs_list)
                item.setCheckState(Qt.Checked if codec in video_codec_list else Qt.Unchecked)

        account.rtp.video_codec_list  = DefaultValue
        account.rtp.video_codec_order = DefaultValue
        account.save()

Dan Pascu's avatar
Dan Pascu committed
1233 1234 1235 1236 1237
    def _SH_InbandDTMFButtonClicked(self, checked):
        account = self.selected_account
        account.rtp.inband_dtmf = checked
        account.save()

Dan Pascu's avatar
Dan Pascu committed
1238 1239 1240 1241 1242
    def _SH_RTPEncryptionButtonClicked(self, checked):
        self.key_negotiation_button.setEnabled(checked)
        account = self.selected_account
        account.rtp.encryption.enabled = checked
        account.save()
Tijmen de Mes's avatar
Tijmen de Mes committed
1243

1244
    def _SH_AutoAnswerIntervalChanged(self, interval):
Tijmen de Mes's avatar
Tijmen de Mes committed
1245 1246 1247
        settings = SIPSimpleSettings()
        settings.sip.auto_answer_interval = interval
        settings.save()
1248 1249 1250 1251 1252

    def _SH_AccountAutoAnswerChanged(self, auto_answer):
        account = self.selected_account
        account.sip.auto_answer = not account.sip.auto_answer
        account.save()
Dan Pascu's avatar
Dan Pascu committed
1253 1254

    def _SH_KeyNegotiationButtonActivated(self, index):
Dan Pascu's avatar
Dan Pascu committed
1255
        account = self.selected_account
Dan Pascu's avatar
Dan Pascu committed
1256
        account.rtp.encryption.key_negotiation = self.key_negotiation_button.itemData(index)
Dan Pascu's avatar
Dan Pascu committed
1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272
        account.save()

    # Account server settings
    def _SH_AlwaysUseMyProxyButtonClicked(self, checked):
        account = self.selected_account
        account.sip.always_use_my_proxy = checked
        account.save()

    def _SH_OutboundProxyHostEditorEditingFinished(self):
        account = self.selected_account
        outbound_proxy = self.account_outbound_proxy
        if account.sip.outbound_proxy != outbound_proxy:
            account.sip.outbound_proxy = outbound_proxy
            account.save()

    def _SH_OutboundProxyPortValueChanged(self, value):
1273 1274 1275 1276 1277
        account = self.selected_account
        outbound_proxy = self.account_outbound_proxy
        if account.sip.outbound_proxy != outbound_proxy:
            account.sip.outbound_proxy = outbound_proxy
            account.save()
Dan Pascu's avatar
Dan Pascu committed
1278 1279 1280 1281 1282 1283 1284 1285 1286 1287

    def _SH_OutboundProxyTransportButtonActivated(self, text):
        account = self.selected_account
        outbound_proxy = self.account_outbound_proxy
        if account.sip.outbound_proxy != outbound_proxy:
            account.sip.outbound_proxy = outbound_proxy
            account.save()

    def _SH_AuthUsernameEditorEditingFinished(self):
        account = self.selected_account
1288
        auth_username = self.auth_username_editor.text() or None
Dan Pascu's avatar
Dan Pascu committed
1289 1290 1291 1292
        if account.auth.username != auth_username:
            account.auth.username = auth_username
            account.save()

1293 1294 1295 1296 1297 1298 1299
    def _SH_TLSPeerNameEditorEditingFinished(self):
        account = self.selected_account
        tls_name = self.account_tls_name_editor.text() or None
        if account.sip.tls_name != tls_name:
            account.sip.tls_name = tls_name
            account.save()

Dan Pascu's avatar
Dan Pascu committed
1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311
    def _SH_AlwaysUseMyMSRPRelayButtonClicked(self, checked):
        account = self.selected_account
        account.nat_traversal.use_msrp_relay_for_outbound = checked
        account.save()

    def _SH_MSRPRelayHostEditorEditingFinished(self):
        account = self.selected_account
        msrp_relay = self.account_msrp_relay
        if account.nat_traversal.msrp_relay != msrp_relay:
            account.nat_traversal.msrp_relay = msrp_relay
            account.save()

1312 1313 1314 1315 1316 1317 1318
    def _SH_StunServerListEditorEditingFinished(self):
        account = self.selected_account
        stun_server_list = self.stun_server_list_editor.text().strip().lower() or ''
        new_stun_server_list = []
        if stun_server_list:
            for server in stun_server_list.split(","):
                try:
Tijmen de Mes's avatar
Tijmen de Mes committed
1319
                    (host, port) = server.strip().split(':')
1320
                except ValueError:
Tijmen de Mes's avatar
Tijmen de Mes committed
1321 1322
                    host = server
                    port = STUNServerAddress.default_port
1323
                else:
Tijmen de Mes's avatar
Tijmen de Mes committed
1324 1325 1326 1327
                    try:
                        int(port)
                    except (TypeError, ValueError) as e:
                        port = STUNServerAddress.default_port
Tijmen de Mes's avatar
Tijmen de Mes committed
1328

1329 1330 1331 1332
                try:
                    new_stun_server_list.append(STUNServerAddress(host, port))
                except ValueError as e:
                    continue
Tijmen de Mes's avatar
Tijmen de Mes committed
1333

1334 1335 1336 1337 1338 1339 1340 1341 1342
        new_stun_server_list = new_stun_server_list or None

        if account.nat_traversal.stun_server_list != new_stun_server_list:
            try:
                account.nat_traversal.stun_server_list = new_stun_server_list
                account.save()
            except ValueError as e:
                pass

Dan Pascu's avatar
Dan Pascu committed
1343
    def _SH_MSRPRelayPortValueChanged(self, value):
1344 1345 1346 1347 1348
        account = self.selected_account
        msrp_relay = self.account_msrp_relay
        if account.nat_traversal.msrp_relay != msrp_relay:
            account.nat_traversal.msrp_relay = msrp_relay
            account.save()
Dan Pascu's avatar
Dan Pascu committed
1349 1350 1351 1352 1353 1354 1355 1356 1357 1358

    def _SH_MSRPRelayTransportButtonActivated(self, text):
        account = self.selected_account
        msrp_relay = self.account_msrp_relay
        if account.nat_traversal.msrp_relay != msrp_relay:
            account.nat_traversal.msrp_relay = msrp_relay
            account.save()

    def _SH_VoicemailURIEditorEditingFinished(self):
        account = self.selected_account
1359
        voicemail_uri = self.voicemail_uri_editor.text() or None
Dan Pascu's avatar
Dan Pascu committed
1360 1361 1362 1363 1364 1365
        if account.message_summary.voicemail_uri != voicemail_uri:
            account.message_summary.voicemail_uri = voicemail_uri
            account.save()

    def _SH_XCAPRootEditorEditingFinished(self):
        account = self.selected_account
1366
        xcap_root = self.xcap_root_editor.text() or None
Dan Pascu's avatar
Dan Pascu committed
1367 1368 1369 1370 1371 1372
        if account.xcap.xcap_root != xcap_root:
            account.xcap.xcap_root = xcap_root
            account.save()

    def _SH_ServerToolsURLEditorEditingFinished(self):
        account = self.selected_account
1373
        url = self.server_tools_url_editor.text() or None
Dan Pascu's avatar
Dan Pascu committed
1374 1375 1376 1377
        if account.server.settings_url != url:
            account.server.settings_url = url
            account.save()

1378 1379
    def _SH_ConferenceServerEditorEditingFinished(self):
        account = self.selected_account
1380
        server = self.conference_server_editor.text() or None
1381 1382 1383 1384
        if account.server.conference_server != server:
            account.server.conference_server = server
            account.save()

Dan Pascu's avatar
Dan Pascu committed
1385 1386 1387 1388 1389 1390 1391 1392
    # Account network settings
    def _SH_UseICEButtonClicked(self, checked):
        account = self.selected_account
        account.nat_traversal.use_ice = checked
        account.save()

    def _SH_MSRPTransportButtonActivated(self, text):
        account = self.selected_account
1393
        account.msrp.transport = text.lower()
Dan Pascu's avatar
Dan Pascu committed
1394 1395 1396 1397
        account.save()

    # Account advanced settings
    def _SH_RegisterIntervalValueChanged(self, value):
1398 1399 1400
        account = self.selected_account
        account.sip.register_interval = value
        account.save()
Dan Pascu's avatar
Dan Pascu committed
1401 1402

    def _SH_PublishIntervalValueChanged(self, value):
1403 1404 1405
        account = self.selected_account
        account.sip.publish_interval = value
        account.save()
Dan Pascu's avatar
Dan Pascu committed
1406 1407

    def _SH_SubscribeIntervalValueChanged(self, value):
1408 1409 1410
        account = self.selected_account
        account.sip.subscribe_interval = value
        account.save()
Dan Pascu's avatar
Dan Pascu committed
1411 1412 1413 1414 1415 1416 1417 1418

    def _SH_ReregisterButtonClicked(self):
        account = self.selected_account
        account.reregister()

    def _SH_IDDPrefixButtonActivated(self, text):
        self._update_pstn_example_label()
        account = self.selected_account
1419
        idd_prefix = None if text == '+' else text
Dan Pascu's avatar
Dan Pascu committed
1420 1421 1422 1423 1424 1425 1426
        if account.pstn.idd_prefix != idd_prefix:
            account.pstn.idd_prefix = idd_prefix
            account.save()

    def _SH_PrefixButtonActivated(self, text):
        self._update_pstn_example_label()
        account = self.selected_account
1427
        prefix = None if text == 'None' else text
Dan Pascu's avatar
Dan Pascu committed
1428 1429 1430 1431
        if account.pstn.prefix != prefix:
            account.pstn.prefix = prefix
            account.save()

1432 1433 1434 1435
    def _SH_TLSCertFileEditorLocationCleared(self):
        settings = SIPSimpleSettings()
        settings.tls.certificate = None
        settings.save()
Dan Pascu's avatar
Dan Pascu committed
1436

1437
    def _SH_TLSCertFileBrowseButtonClicked(self, checked):
Dan Pascu's avatar
Dan Pascu committed
1438
        # TODO: open the file selection dialog in non-modal mode (and the error messages boxes as well). -Dan
1439 1440
        settings = SIPSimpleSettings()
        directory = os.path.dirname(settings.tls.certificate.normalized) if settings.tls.certificate else Path('~').normalized
Adrian Georgescu's avatar
Adrian Georgescu committed
1441
        cert_path = QFileDialog.getOpenFileName(self, 'Select Certificate File', directory, "TLS certificates (*.crt *.pem)")[0] or None
Dan Pascu's avatar
Dan Pascu committed
1442 1443
        if cert_path is not None:
            cert_path = os.path.normpath(cert_path)
1444
            if cert_path != settings.tls.certificate:
Dan Pascu's avatar
Dan Pascu committed
1445
                try:
1446 1447 1448
                    contents = open(cert_path).read()
                    X509Certificate(contents)
                    X509PrivateKey(contents)
Adrian Georgescu's avatar
Adrian Georgescu committed
1449 1450 1451 1452
                except (OSError, IOError) as e:
                    QMessageBox.critical(self, "TLS Certificate Error", "The certificate file could not be opened: %s" % e.strerror)
                except GNUTLSError as e:
                    QMessageBox.critical(self, "TLS Certificate Error", "The certificate file is invalid: %s" % e)
Dan Pascu's avatar
Dan Pascu committed
1453
                else:
1454 1455 1456
                    self.tls_cert_file_editor.setText(cert_path)
                    settings.tls.certificate = cert_path
                    settings.save()
Dan Pascu's avatar
Dan Pascu committed
1457

1458 1459 1460 1461
    def _SH_TLSVerifyServerButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.tls.verify_server = checked
        settings.save()
Dan Pascu's avatar
Dan Pascu committed
1462

1463 1464 1465
    def _SH_EnableMessageCPIMButtonClicked(self, checked):
        account = self.selected_account
        account.sms.use_cpim = checked
1466
        self.message_imdn_enabled_button.setEnabled(account.sms.use_cpim)
1467 1468 1469 1470 1471 1472 1473
        account.save()

    def _SH_EnableMessageIsComposingButtonClicked(self, checked):
        account = self.selected_account
        account.sms.enable_iscomposing = checked
        account.save()

1474 1475 1476 1477 1478
    def _SH_EnableMessageIMDNButtonClicked(self, checked):
        account = self.selected_account
        account.sms.enable_imdn = checked
        account.save()

1479 1480 1481 1482 1483
    def _SH_AddUnknownContactsButtonClicked(self, checked):
        account = self.selected_account
        account.sms.add_unknown_contacts = checked
        account.save()

1484 1485 1486 1487 1488
    def _SH_EnablePGPButtonClicked(self, checked):
        account = self.selected_account
        account.sms.enable_pgp = checked
        account.save()

1489 1490 1491 1492 1493
    def _SH_MessageReplicationButtonClicked(self, checked):
        account = self.selected_account
        account.sms.enable_message_replication = checked
        account.save()

1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512
    def _SH_MessageSynchronizationButtonClicked(self, checked):
        account = self.selected_account
        account.sms.enable_history_synchronization = checked
        account.save()

    def _SH_HistoryUrlEditorEditingFinshed(self):
        account = self.selected_account
        history_url = self.history_url_editor.text() or None
        if account.sms.history_synchronization_url != history_url:
            account.sms.history_synchronization_url = history_url
            account.save()

    def _SH_LastIdEditorEditingFinished(self):
        account = self.selected_account
        last_id = self.last_id_editor.text() or None
        if account.sms.history_synchronization_id != last_id:
            account.sms.history_synchronization_id = last_id
            account.save()

Dan Pascu's avatar
Dan Pascu committed
1513 1514
    # Audio devices signal handlers
    def _SH_AudioAlertDeviceButtonActivated(self, index):
1515
        device = self.audio_alert_device_button.itemData(index)
Dan Pascu's avatar
Dan Pascu committed
1516 1517
        settings = SIPSimpleSettings()
        settings.audio.alert_device = device
1518
        settings.save()
Dan Pascu's avatar
Dan Pascu committed
1519 1520

    def _SH_AudioInputDeviceButtonActivated(self, index):
1521
        device = self.audio_input_device_button.itemData(index)
Dan Pascu's avatar
Dan Pascu committed
1522 1523
        settings = SIPSimpleSettings()
        settings.audio.input_device = device
1524
        settings.save()
Dan Pascu's avatar
Dan Pascu committed
1525 1526

    def _SH_AudioOutputDeviceButtonActivated(self, index):
1527
        device = self.audio_output_device_button.itemData(index)
Dan Pascu's avatar
Dan Pascu committed
1528 1529
        settings = SIPSimpleSettings()
        settings.audio.output_device = device
1530
        settings.save()
Dan Pascu's avatar
Dan Pascu committed
1531 1532 1533

    def _SH_AudioSampleRateButtonActivated(self, text):
        settings = SIPSimpleSettings()
1534
        settings.audio.sample_rate = text
1535
        settings.save()
Dan Pascu's avatar
Dan Pascu committed
1536 1537 1538

    def _SH_EnableEchoCancellingButtonClicked(self, checked):
        settings = SIPSimpleSettings()
1539
        settings.audio.echo_canceller.enabled = checked
Dan Pascu's avatar
Dan Pascu committed
1540 1541
        settings.save()

1542 1543 1544 1545 1546
    def _SH_TailLengthSliderValueChanged(self, value):
        settings = SIPSimpleSettings()
        settings.audio.echo_canceller.tail_length = value
        settings.save()

Dan Pascu's avatar
Dan Pascu committed
1547 1548
    # Audio codecs signal handlers
    def _SH_AudioCodecsListItemChanged(self, item):
1549
        settings = SIPSimpleSettings()
Adrian Georgescu's avatar
Adrian Georgescu committed
1550
        item_iterator = (self.audio_codecs_list.item(row) for row in range(self.audio_codecs_list.count()))
1551
        settings.rtp.audio_codec_list = [item.text() for item in item_iterator if item.checkState() == Qt.Checked]
1552
        settings.save()
Dan Pascu's avatar
Dan Pascu committed
1553 1554 1555

    def _SH_AudioCodecsListModelRowsMoved(self, source_parent, source_start, source_end, dest_parent, dest_row):
        settings = SIPSimpleSettings()
Adrian Georgescu's avatar
Adrian Georgescu committed
1556
        items = [self.audio_codecs_list.item(row) for row in range(self.audio_codecs_list.count())]
1557
        settings.rtp.audio_codec_order = [item.text() for item in items]
1558
        settings.rtp.audio_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
Dan Pascu's avatar
Dan Pascu committed
1559 1560 1561 1562 1563 1564 1565 1566 1567 1568
        settings.save()

    # Answering machine signal handlers
    def _SH_EnableAnsweringMachineButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.answering_machine.enabled = checked
        settings.save()

    def _SH_AnswerDelayValueChanged(self, value):
        if value == 0:
Adrian Georgescu's avatar
Adrian Georgescu committed
1569
            self.answer_delay_seconds_label.setText('')
Dan Pascu's avatar
Dan Pascu committed
1570
        elif value == 1:
Adrian Georgescu's avatar
Adrian Georgescu committed
1571
            self.answer_delay_seconds_label.setText('second')
Dan Pascu's avatar
Dan Pascu committed
1572
        else:
Adrian Georgescu's avatar
Adrian Georgescu committed
1573
            self.answer_delay_seconds_label.setText('seconds')
Dan Pascu's avatar
Dan Pascu committed
1574
        settings = SIPSimpleSettings()
1575
        if settings.answering_machine.answer_delay != value:
Dan Pascu's avatar
Dan Pascu committed
1576 1577 1578 1579
            settings.answering_machine.answer_delay = value
            settings.save()

    def _SH_MaxRecordingValueChanged(self, value):
Adrian Georgescu's avatar
Adrian Georgescu committed
1580
        self.max_recording_minutes_label.setText('minute' if value == 1 else 'minutes')
Dan Pascu's avatar
Dan Pascu committed
1581
        settings = SIPSimpleSettings()
1582
        if settings.answering_machine.max_recording != value:
Dan Pascu's avatar
Dan Pascu committed
1583 1584 1585
            settings.answering_machine.max_recording = value
            settings.save()

Dan Pascu's avatar
Dan Pascu committed
1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608
    # Video devices signal handlers
    def _SH_VideoCameraButtonActivated(self, index):
        device = self.video_camera_button.itemData(index)
        settings = SIPSimpleSettings()
        settings.video.device = device
        settings.save()

    def _SH_VideoResolutionButtonActivated(self, index):
        resolution = self.video_resolution_button.itemData(index)
        settings = SIPSimpleSettings()
        settings.video.resolution = resolution
        settings.video.h264.level = self.h264_level_map[resolution]
        settings.save()

    def _SH_VideoFramerateButtonActivated(self, index):
        framerate = self.video_framerate_button.itemData(index)
        settings = SIPSimpleSettings()
        settings.video.framerate = framerate
        settings.save()

    # Video codecs signal handlers
    def _SH_VideoCodecsListItemChanged(self, item):
        settings = SIPSimpleSettings()
Adrian Georgescu's avatar
Adrian Georgescu committed
1609
        item_iterator = (self.video_codecs_list.item(row) for row in range(self.video_codecs_list.count()))
1610
        settings.rtp.video_codec_list = [item.text() for item in item_iterator if item.checkState() == Qt.Checked]
Dan Pascu's avatar
Dan Pascu committed
1611 1612 1613 1614
        settings.save()

    def _SH_VideoCodecsListModelRowsMoved(self, source_parent, source_start, source_end, dest_parent, dest_row):
        settings = SIPSimpleSettings()
Adrian Georgescu's avatar
Adrian Georgescu committed
1615
        items = [self.video_codecs_list.item(row) for row in range(self.video_codecs_list.count())]
Dan Pascu's avatar
Dan Pascu committed
1616
        settings.rtp.video_codec_order = [item.text() for item in items]
1617
        settings.rtp.video_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
Dan Pascu's avatar
Dan Pascu committed
1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631
        settings.save()

    def _SH_VideoCodecBitrateButtonActivated(self, index):
        bitrate = self.video_codec_bitrate_button.itemData(index)
        settings = SIPSimpleSettings()
        settings.video.max_bitrate = bitrate
        settings.save()

    def _SH_H264ProfileButtonActivated(self, index):
        profile = self.h264_profile_button.itemData(index)
        settings = SIPSimpleSettings()
        settings.video.h264.profile = profile
        settings.save()

Dan Pascu's avatar
Dan Pascu committed
1632
    # Chat and SMS signal handlers
Dan Pascu's avatar
Dan Pascu committed
1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684
    def _SH_StyleViewSizeChanged(self):
        self._align_style_preview(scroll=True)

    def _SH_StyleViewFrameContentsSizeChanged(self, size):
        self._align_style_preview(scroll=True)

    def _SH_StyleButtonActivated(self, index):
        style = self.style_button.itemData(index)
        settings = BlinkSettings()
        if style.name != settings.chat_window.style:
            self.style_variant_button.clear()
            for variant in style.variants:
                self.style_variant_button.addItem(variant)
            self.style_variant_button.setCurrentIndex(self.style_variant_button.findText(style.default_variant))
            settings.chat_window.style = style.name
            settings.chat_window.style_variant = None
            settings.save()

    def _SH_StyleVariantButtonActivated(self, style_variant):
        style = self.style_button.itemData(self.style_button.currentIndex())
        settings = BlinkSettings()
        current_variant = settings.chat_window.style_variant or style.default_variant
        if style_variant != current_variant:
            settings.chat_window.style_variant = style_variant
            settings.save()

    def _SH_StyleShowIconsButtonClicked(self, checked):
        settings = BlinkSettings()
        settings.chat_window.show_user_icons = checked
        settings.save()

    def _SH_StyleFontButtonCurrentIndexChanged(self, font):
        settings = BlinkSettings()
        settings.chat_window.font = font
        settings.save()

    def _SH_StyleFontSizeValueChanged(self, size):
        settings = BlinkSettings()
        settings.chat_window.font_size = size
        settings.save()

    def _SH_StyleDefaultFontButtonClicked(self, checked):
        settings = BlinkSettings()
        settings.chat_window.font = DefaultValue
        settings.chat_window.font_size = DefaultValue
        settings.save()
        style = self.style_button.itemData(self.style_button.currentIndex())
        with blocked_qt_signals(self.style_font_button):
            self.style_font_button.setCurrentFont(QFont(style.font_family))
        with blocked_qt_signals(self.style_font_size):
            self.style_font_size.setValue(style.font_size)

Dan Pascu's avatar
Dan Pascu committed
1685 1686 1687 1688 1689
    def _SH_AutoAcceptChatButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.chat.auto_accept = checked
        settings.save()

1690
    def _SH_ChatMessageAlertButtonClicked(self, checked):
Dan Pascu's avatar
Dan Pascu committed
1691
        settings = SIPSimpleSettings()
1692
        settings.sounds.play_message_alerts = checked
Dan Pascu's avatar
Dan Pascu committed
1693 1694
        settings.save()

1695
    def _SH_SMSReplicationButtonClicked(self, checked):
Dan Pascu's avatar
Dan Pascu committed
1696
        settings = SIPSimpleSettings()
1697
        settings.chat.sms_replication = checked
Dan Pascu's avatar
Dan Pascu committed
1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709
        settings.save()

    def _SH_SessionInfoStyleButtonClicked(self, checked):
        settings = BlinkSettings()
        settings.chat_window.session_info.alternate_style = checked
        settings.save()

    def _SH_TrafficUnitsButtonClicked(self, checked):
        settings = BlinkSettings()
        settings.chat_window.session_info.bytes_per_second = checked
        settings.save()

Dan Pascu's avatar
Dan Pascu committed
1710 1711 1712 1713
    # Screen sharing signal handlers
    def _SH_ScreenshotsDirectoryBrowseButtonClicked(self, checked):
        # TODO: open the file selection dialog in non-modal mode. Same for the one for TLS CA list and the IconSelector from contacts. -Dan
        settings = BlinkSettings()
Adrian Georgescu's avatar
Adrian Georgescu committed
1714
        directory = QFileDialog.getExistingDirectory(self, 'Select Screenshots Directory', settings.screenshots_directory.normalized) or None
Dan Pascu's avatar
Dan Pascu committed
1715 1716
        if directory is not None:
            directory = os.path.normpath(directory)
Dan Pascu's avatar
Dan Pascu committed
1717
            if directory != settings.screenshots_directory:
Dan Pascu's avatar
Dan Pascu committed
1718
                self.screenshots_directory_editor.setText(directory)
Dan Pascu's avatar
Dan Pascu committed
1719
                settings.screenshots_directory = directory
Dan Pascu's avatar
Dan Pascu committed
1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736
                settings.save()

    def _SH_ScreenSharingScaleButtonClicked(self, checked):
        settings = BlinkSettings()
        settings.screen_sharing.scale = checked
        settings.save()

    def _SH_ScreenSharingFullscreenButtonClicked(self, checked):
        settings = BlinkSettings()
        settings.screen_sharing.open_fullscreen = checked
        settings.save()

    def _SH_ScreenSharingViewonlyButtonClicked(self, checked):
        settings = BlinkSettings()
        settings.screen_sharing.open_viewonly = checked
        settings.save()

Dan Pascu's avatar
Dan Pascu committed
1737
    # File transfer signal handlers
Dan Pascu's avatar
Dan Pascu committed
1738
    def _SH_TransfersDirectoryBrowseButtonClicked(self, checked):
Dan Pascu's avatar
Dan Pascu committed
1739
        # TODO: open the file selection dialog in non-modal mode. Same for the one for TLS CA list and the IconSelector from contacts. -Dan
Dan Pascu's avatar
Dan Pascu committed
1740
        settings = BlinkSettings()
Adrian Georgescu's avatar
Adrian Georgescu committed
1741
        directory = QFileDialog.getExistingDirectory(self, 'Select Transfers Directory', settings.transfers_directory.normalized) or None
Dan Pascu's avatar
Dan Pascu committed
1742 1743
        if directory is not None:
            directory = os.path.normpath(directory)
Dan Pascu's avatar
Dan Pascu committed
1744 1745 1746
            if directory != settings.transfers_directory:
                self.transfers_directory_editor.setText(directory)
                settings.transfers_directory = directory
Dan Pascu's avatar
Dan Pascu committed
1747 1748 1749 1750 1751 1752 1753 1754
                settings.save()

    # File logging signal handlers
    def _SH_TraceSIPButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.logs.trace_sip = checked
        settings.save()

1755 1756 1757 1758 1759
    def _SH_TraceMessagingButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.logs.trace_messaging = checked
        settings.save()

Dan Pascu's avatar
Dan Pascu committed
1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781
    def _SH_TraceMSRPButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.logs.trace_msrp = checked
        settings.save()

    def _SH_TraceXCAPButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.logs.trace_xcap = checked
        settings.save()

    def _SH_TraceNotificationsButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.logs.trace_notifications = checked
        settings.save()

    def _SH_TracePJSIPButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.logs.trace_pjsip = checked
        settings.save()

    def _SH_PJSIPTraceLevelValueChanged(self, value):
        settings = SIPSimpleSettings()
1782
        if settings.logs.pjsip_level != value:
Dan Pascu's avatar
Dan Pascu committed
1783 1784 1785
            settings.logs.pjsip_level = value
            settings.save()

1786
    @run_in_thread('file-io')
Dan Pascu's avatar
Dan Pascu committed
1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811
    def _SH_ClearLogFilesButtonClicked(self):
        log_manager = LogManager()
        log_manager.stop()
        for path, dirs, files in os.walk(os.path.join(ApplicationData.directory, 'logs'), topdown=False):
            for name in files:
                try:
                    os.remove(os.path.join(path, name))
                except (OSError, IOError):
                    pass
            for name in dirs:
                try:
                    os.rmdir(os.path.join(path, name))
                except (OSError, IOError):
                    pass
        log_manager.start()
        call_in_gui_thread(self._update_logs_size_label)

    # SIP and RTP signal handlers
    def _SH_SIPTransportsButtonClicked(self, button):
        settings = SIPSimpleSettings()
        settings.sip.transport_list = [button.name for button in self.sip_transports_button_group.buttons() if button.isChecked()]
        settings.save()

    def _SH_UDPPortValueChanged(self, value):
        settings = SIPSimpleSettings()
1812
        if settings.sip.udp_port != value:
Dan Pascu's avatar
Dan Pascu committed
1813 1814 1815 1816 1817
            settings.sip.udp_port = value
            settings.save()

    def _SH_TCPPortValueChanged(self, value):
        settings = SIPSimpleSettings()
1818
        if settings.sip.tcp_port != value:
Dan Pascu's avatar
Dan Pascu committed
1819 1820 1821 1822 1823
            settings.sip.tcp_port = value
            settings.save()

    def _SH_TLSPortValueChanged(self, value):
        settings = SIPSimpleSettings()
1824
        if settings.sip.tls_port != value:
Dan Pascu's avatar
Dan Pascu committed
1825 1826 1827 1828 1829 1830 1831
            settings.sip.tls_port = value
            settings.save()

    def _SH_MediaPortsStartValueChanged(self, value):
        self.media_ports.setMaximum(limit(65535-value, min=10, max=10000))
        settings = SIPSimpleSettings()
        port_range = PortRange(value, value + self.media_ports.value())
1832
        if settings.rtp.port_range != port_range:
Dan Pascu's avatar
Dan Pascu committed
1833 1834 1835 1836 1837 1838 1839
            settings.rtp.port_range = port_range
            settings.save()

    def _SH_MediaPortsValueChanged(self, value):
        self.media_ports_start.setMaximum(limit(65535-value, min=10000, max=65000))
        settings = SIPSimpleSettings()
        port_range = PortRange(self.media_ports_start.value(), self.media_ports_start.value() + value)
1840
        if settings.rtp.port_range != port_range:
Dan Pascu's avatar
Dan Pascu committed
1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852
            settings.rtp.port_range = port_range
            settings.save()

    # TLS signal handlers
    def _SH_TLSCAFileEditorLocationCleared(self):
        settings = SIPSimpleSettings()
        settings.tls.ca_list = None
        settings.save()

    def _SH_TLSCAFileBrowseButtonClicked(self):
        # TODO: open the file selection dialog in non-modal mode (and the error messages boxes as well). -Dan
        settings = SIPSimpleSettings()
1853
        directory = os.path.dirname(settings.tls.ca_list.normalized) if settings.tls.ca_list else Path('~').normalized
Adrian Georgescu's avatar
Adrian Georgescu committed
1854
        ca_path = QFileDialog.getOpenFileName(self, 'Select Certificate Authority File', directory, "TLS certificates (*.crt *.pem)")[0] or None
Dan Pascu's avatar
Dan Pascu committed
1855 1856 1857 1858 1859
        if ca_path is not None:
            ca_path = os.path.normpath(ca_path)
            if ca_path != settings.tls.ca_list:
                try:
                    X509Certificate(open(ca_path).read())
Adrian Georgescu's avatar
Adrian Georgescu committed
1860 1861 1862 1863
                except (OSError, IOError) as e:
                    QMessageBox.critical(self, "TLS Certificate Error", "The certificate authority file could not be opened: %s" % e.strerror)
                except GNUTLSError as e:
                    QMessageBox.critical(self, "TLS Certificate Error", "The certificate authority file is invalid: %s" % e)
Dan Pascu's avatar
Dan Pascu committed
1864 1865 1866 1867 1868
                else:
                    self.tls_ca_file_editor.setText(ca_path)
                    settings.tls.ca_list = ca_path
                    settings.save()

1869 1870 1871 1872 1873
    def _SH_HistoryNameAndUriButtonClicked(self, checked):
        settings = BlinkSettings()
        settings.interface.show_history_name_and_uri = checked
        settings.save()

Dan Pascu's avatar
Dan Pascu committed
1874 1875 1876 1877 1878 1879 1880 1881
    @run_in_gui_thread
    def handle_notification(self, notification):
        handler = getattr(self, '_NH_%s' % notification.name, Null)
        handler(notification)

    def _NH_SIPApplicationDidStart(self, notification):
        self._sync_defaults()
        self.load_settings()
1882
        notification.center.add_observer(self, name='AudioDevicesDidChange')
Dan Pascu's avatar
Dan Pascu committed
1883 1884
        notification.center.add_observer(self, name='VideoDevicesDidChange')
        notification.center.add_observer(self, name='VideoDeviceDidChangeCamera')
1885
        notification.center.add_observer(self, name='CFGSettingsObjectDidChange')
1886
        notification.center.add_observer(self, name='SIPRegistrationInfoDidChange')
Dan Pascu's avatar
Dan Pascu committed
1887 1888 1889 1890

    def _NH_AudioDevicesDidChange(self, notification):
        self.load_audio_devices()

Dan Pascu's avatar
Dan Pascu committed
1891 1892 1893 1894 1895 1896 1897
    def _NH_VideoDevicesDidChange(self, notification):
        self.load_video_devices()

    def _NH_VideoDeviceDidChangeCamera(self, notification):
        if self.camera_preview.isVisible():
            self.camera_preview.producer = SIPApplication.video_device.producer

Dan Pascu's avatar
Dan Pascu committed
1898 1899
    def _NH_CFGSettingsObjectDidChange(self, notification):
        settings = SIPSimpleSettings()
Dan Pascu's avatar
Dan Pascu committed
1900 1901 1902 1903 1904 1905 1906 1907
        blink_settings = BlinkSettings()
        if notification.sender is blink_settings:
            if {'chat_window.style', 'chat_window.style_variant', 'chat_window.show_user_icons'}.intersection(notification.data.modified):
                self.update_chat_preview()
            if {'chat_window.font', 'chat_window.font_size'}.intersection(notification.data.modified):
                self.update_chat_preview()
                self.style_default_font_button.setEnabled(blink_settings.chat_window.font is not None or blink_settings.chat_window.font_size is not None)
        elif notification.sender is settings:
Dan Pascu's avatar
Dan Pascu committed
1908
            if 'audio.alert_device' in notification.data.modified:
1909
                self.audio_alert_device_button.setCurrentIndex(self.audio_alert_device_button.findData(settings.audio.alert_device))
Dan Pascu's avatar
Dan Pascu committed
1910
            if 'audio.input_device' in notification.data.modified:
1911
                self.audio_input_device_button.setCurrentIndex(self.audio_input_device_button.findData(settings.audio.input_device))
Dan Pascu's avatar
Dan Pascu committed
1912
            if 'audio.output_device' in notification.data.modified:
1913
                self.audio_output_device_button.setCurrentIndex(self.audio_output_device_button.findData(settings.audio.output_device))
Dan Pascu's avatar
Dan Pascu committed
1914 1915 1916 1917
            if 'answering_machine.enabled' in notification.data.modified:
                self.enable_answering_machine_button.setChecked(settings.answering_machine.enabled)
            if 'chat.auto_accept' in notification.data.modified:
                self.auto_accept_chat_button.setChecked(settings.chat.auto_accept)
1918 1919
            if 'sounds.play_message_alerts' in notification.data.modified:
                self.chat_message_alert_button.setChecked(settings.sounds.play_message_alerts)
1920 1921
            if 'sip.auto_answer_interval' in notification.data.modified:
                self.auto_answer_interval.setValue(settings.sip.auto_answer_interval)                
Dan Pascu's avatar
Dan Pascu committed
1922 1923
            if 'video.device' in notification.data.modified:
                self.video_camera_button.setCurrentIndex(self.video_camera_button.findData(settings.video.device))
Dan Pascu's avatar
Dan Pascu committed
1924
        elif notification.sender is self.selected_account is not None:
Dan Pascu's avatar
Dan Pascu committed
1925
            account = notification.sender
1926 1927
            if 'sip.auto_answer' in notification.data.modified:
                self.account_auto_answer.setChecked(account.sip.auto_answer)                
Dan Pascu's avatar
Dan Pascu committed
1928 1929
            if 'enabled' in notification.data.modified:
                self.account_enabled_button.setChecked(account.enabled)
1930 1931
                if not account.enabled:
                    self.refresh_account_registration_widgets(account)
Dan Pascu's avatar
Dan Pascu committed
1932
                self.reregister_button.setEnabled(account.enabled)
1933 1934 1935 1936
            if 'message_summary.enabled' in notification.data.modified:
                self.account_enabled_mwi_button.setChecked(account.message_summary.enabled)
            if 'presence.enabled' in notification.data.modified:
                self.account_enabled_presence_button.setChecked(account.presence.enabled)
Dan Pascu's avatar
Dan Pascu committed
1937
            if 'display_name' in notification.data.modified:
Adrian Georgescu's avatar
Adrian Georgescu committed
1938
                self.display_name_editor.setText(account.display_name or '')
Dan Pascu's avatar
Dan Pascu committed
1939 1940
            if 'rtp.audio_codec_list' in notification.data.modified:
                self.reset_account_audio_codecs_button.setEnabled(account.rtp.audio_codec_list is not None)
Dan Pascu's avatar
Dan Pascu committed
1941 1942
            if 'rtp.video_codec_list' in notification.data.modified:
                self.reset_account_video_codecs_button.setEnabled(account.rtp.video_codec_list is not None)
1943 1944 1945 1946 1947
            if 'sms.history_synchronization_url' in notification.data.modified:
                if not account.sms.history_synchronization_url:
                    account.sms.history_synchronization_token = None
                    account.sms.enable_history_synchronization = False
                    account.save()
Dan Pascu's avatar
Dan Pascu committed
1948 1949


Tijmen de Mes's avatar
Tijmen de Mes committed
1950
del ui_class, base_class