import os
import urllib.parse

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

from application import log
from application.notification import IObserver, NotificationCenter
from application.python import Null, limit
from gnutls.crypto import X509Certificate, X509PrivateKey
from gnutls.errors import GNUTLSError
from zope.interface import implementer

from sipsimple.account import AccountManager, BonjourAccount
from sipsimple.application import SIPApplication
from sipsimple.configuration import DefaultValue
from sipsimple.configuration.datatypes import H264Profile, MSRPRelayAddress, Path, PortRange, SIPProxyAddress
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.threading import run_in_thread

from blink.accounts import AddAccountDialog
from blink.chatwindow import ChatMessageStyle, ChatStyleError, ChatMessage, ChatEvent, ChatSender
from blink.configuration.datatypes import FileURL
from blink.configuration.settings import BlinkSettings
from blink.resources import ApplicationData, Resources
from blink.logging import LogManager
from blink.util import QSingleton, call_in_gui_thread, run_in_gui_thread


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


# LineEdit and ComboBox validators
#
class IDDPrefixValidator(QRegExpValidator):
    def __init__(self, parent=None):
        super(IDDPrefixValidator, self).__init__(QRegExp('[0-9+*#]+'), parent)

    def fixup(self, input):
        return super(IDDPrefixValidator, self).fixup(input or '+')


class PrefixValidator(QRegExpValidator):
    def __init__(self, parent=None):
        super(PrefixValidator, self).__init__(QRegExp('(None|[0-9+*#]+)'), parent)

    def fixup(self, input):
        return super(PrefixValidator, self).fixup(input or 'None')


class HostnameValidator(QRegExpValidator):
    def __init__(self, parent=None):
        super(HostnameValidator, self).__init__(QRegExp('^([\w\-_]+(\.[\w\-_]+)*)?$', Qt.CaseInsensitive), parent)


class SIPAddressValidator(QRegExpValidator):
    def __init__(self, parent=None):
        super(SIPAddressValidator, self).__init__(QRegExp('^([\w\-_+%]+@[\w\-_]+(\.[\w\-_]+)*)?$', Qt.CaseInsensitive), parent)

    def fixup(self, input):
        if input and '@' not in input:
            preferences_window = self.parent()
            input += '@%s' % preferences_window.selected_account.id.domain
        return super(SIPAddressValidator, self).fixup(input)


class WebURLValidator(QRegExpValidator):
    def __init__(self, parent=None):
        super(WebURLValidator, self).__init__(QRegExp('^(https?://[\w\-_]+(\.[\w\-_]+)*(:\d+)?(/.*)?)?$', Qt.CaseInsensitive), parent)


class XCAPRootValidator(WebURLValidator):
    def fixup(self, input):
        url = urllib.parse.urlparse(input)
        if not (url.scheme and url.netloc):
            input = ''
        return super(XCAPRootValidator, self).fixup(input)

    def validate(self, input, pos):
        state, input, pos = super(XCAPRootValidator, self).validate(input, pos)
        if state == QValidator.Acceptable:
            if input.endswith(('?', ';', '&')):
                state = QValidator.Invalid
            else:
                url = urllib.parse.urlparse(input)
                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
        return state, input, pos


# Custom widgets used in preferences.ui
#
class SIPPortEditor(QSpinBox):
    def __init__(self, parent=None):
        super(SIPPortEditor, self).__init__(parent)
        self.setRange(0, 65535)
        self.sibling = Null  # if there is a sibling port, its value is invalid for this port

    def stepBy(self, steps):
        value = self.value()
        sibling_value = self.sibling.value()
        if value+steps == sibling_value != 0:
            steps += steps/abs(steps)  # add one more unit in the right direction
        if 0 < value+steps < 1024:
            if steps < 0:
                steps = -value
            else:
                steps = 1024 - value
        if value+steps == sibling_value != 0:
            steps += steps/abs(steps)  # add one more unit in the right direction
        return super(SIPPortEditor, self).stepBy(steps)

    def validate(self, input, pos):
        state, input, pos = super(SIPPortEditor, self).validate(input, pos)
        if state == QValidator.Acceptable:
            value = int(input)
            if 0 < value < 1024:
                state = QValidator.Intermediate
            elif value == self.sibling.value() != 0:
                state = QValidator.Intermediate
        return state, input, pos


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)


class AccountListView(QListView):
    def __init__(self, parent=None):
        super(AccountListView, self).__init__(parent)
        self.setItemDelegate(AccountDelegate(self))
        # self.setDropIndicatorShown(False)

    def selectionChanged(self, selected, deselected):
        super(AccountListView, self).selectionChanged(selected, deselected)
        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)


class blocked_qt_signals(object):
    def __init__(self, qobject):
        self.qobject = qobject

    def __enter__(self):
        self.qobject.blockSignals(True)
        return self.qobject

    def __exit__(self, type, value, traceback):
        self.qobject.blockSignals(False)


class UnspecifiedOutboundProxy(object):
    host = ''
    port = 5060
    transport = 'UDP'


class UnspecifiedMSRPRelay(object):
    host = ''
    port = 0
    transport = 'TLS'


ui_class, base_class = uic.loadUiType(Resources.get('preferences.ui'))


@implementer(IObserver)
class PreferencesWindow(base_class, ui_class, metaclass=QSingleton):

    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)

        self.camera_preview.installEventFilter(self)

        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)
        self.account_list.model().dataChanged.connect(self._SH_AccountListDataChanged)
        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)
        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)
        self.account_audio_codecs_list.model().rowsMoved.connect(self._SH_AccountAudioCodecsListModelRowsMoved)
        self.reset_account_audio_codecs_button.clicked.connect(self._SH_ResetAudioCodecsButtonClicked)
        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)
        self.inband_dtmf_button.clicked.connect(self._SH_InbandDTMFButtonClicked)
        self.rtp_encryption_button.clicked.connect(self._SH_RTPEncryptionButtonClicked)
        self.key_negotiation_button.activated[int].connect(self._SH_KeyNegotiationButtonActivated)

        # 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)
        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)
        self.conference_server_editor.editingFinished.connect(self._SH_ConferenceServerEditorEditingFinished)

        # Account network settings
        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)
        self.account_tls_cert_file_editor.locationCleared.connect(self._SH_AccountTLSCertFileEditorLocationCleared)
        self.account_tls_cert_file_browse_button.clicked.connect(self._SH_AccountTLSCertFileBrowseButtonClicked)
        self.account_tls_verify_server_button.clicked.connect(self._SH_AccountTLSVerifyServerButtonClicked)

        # 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)
        self.tail_length_slider.valueChanged.connect(self._SH_TailLengthSliderValueChanged)

        # Audio codecs
        self.audio_codecs_list.itemChanged.connect(self._SH_AudioCodecsListItemChanged)
        self.audio_codecs_list.model().rowsMoved.connect(self._SH_AudioCodecsListModelRowsMoved)

        # 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)

        # 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)

        # Chat and SMS
        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)

        self.auto_accept_chat_button.clicked.connect(self._SH_AutoAcceptChatButtonClicked)
        self.chat_message_alert_button.clicked.connect(self._SH_ChatMessageAlertButtonClicked)
        self.sms_replication_button.clicked.connect(self._SH_SMSReplicationButtonClicked)

        self.session_info_style_button.clicked.connect(self._SH_SessionInfoStyleButtonClicked)
        self.traffic_units_button.clicked.connect(self._SH_TrafficUnitsButtonClicked)

        # 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)

        # File logging
        self.trace_sip_button.clicked.connect(self._SH_TraceSIPButtonClicked)
        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)

        # 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
        self.tls_ca_file_editor.locationCleared.connect(self._SH_TLSCAFileEditorLocationCleared)
        self.tls_ca_file_browse_button.clicked.connect(self._SH_TLSCAFileBrowseButtonClicked)

        # Setup initial state (show the accounts page right after start)
        self.accounts_action.trigger()
        self.account_tab_widget.setCurrentIndex(0)

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

        # Accounts
        self.key_negotiation_button.clear()
        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')

        # Audio

        # 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()

        # Hide the controls for the features that are not yet implemented -Dan
        self.answering_machine_group_box.hide()
        self.sms_replication_button.hide()

        # 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
        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)

        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)

        for index in range(self.idd_prefix_button.count()):
            text = self.idd_prefix_button.itemText(index)
            self.idd_prefix_button.setItemData(index, None if text == "+" else text)
        for index in range(self.prefix_button.count()):
            text = self.prefix_button.itemText(index)
            self.prefix_button.setItemData(index, None if text == "None" else text)

        self.voicemail_uri_editor.setValidator(SIPAddressValidator(self))
        self.xcap_root_editor.setValidator(XCAPRootValidator(self))
        self.server_tools_url_editor.setValidator(WebURLValidator(self))
        self.conference_server_editor.setValidator(HostnameValidator(self))
        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
        font_metrics = self.outbound_proxy_label.fontMetrics()  # we assume all labels have the same font
        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,
                  self.conference_server_label, self.msrp_transport_label)
        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
        font_metrics = self.register_interval_label.fontMetrics()  # we assume all labels have the same font
        labels = (self.register_interval_label, self.publish_interval_label, self.subscribe_interval_label,
                  self.idd_prefix_label, self.prefix_label, self.account_tls_cert_file_label)
        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)
        self.account_tls_cert_file_label.setMinimumWidth(text_width)

        # audio settings
        font_metrics = self.answer_delay_label.fontMetrics()  # we assume all labels have the same font
        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; }""")

    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

    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()
        transport = self.msrp_relay_transport_button.currentText().lower()
        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()
        transport = self.outbound_proxy_transport_button.currentText().lower()
        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:
            return selected_index.data(Qt.UserRole).account

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

        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:
            # user has a non-default codec order, we need to sync with the new settings
            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)
            if added_codecs:
                settings.rtp.audio_codec_order = DefaultValue  # reset codec order
                settings.rtp.audio_codec_list  = DefaultValue  # reset codec list
                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]
                if codec_order == SIPSimpleSettings.rtp.audio_codec_order.default:
                    codec_order = DefaultValue
                if codec_list == SIPSimpleSettings.rtp.audio_codec_list.default:
                    codec_list = DefaultValue
                settings.rtp.audio_codec_order = codec_order
                settings.rtp.audio_codec_list  = codec_list
                settings.save()

        for account in (account for account in account_manager.iter_accounts() if account.rtp.audio_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.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)
            if added_codecs:
                account.rtp.audio_codec_order = DefaultValue  # reset codec order
                account.rtp.audio_codec_list  = DefaultValue  # reset codec list
                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]
                if codec_order == SIPSimpleSettings.rtp.audio_codec_order.default and codec_list == SIPSimpleSettings.rtp.audio_codec_list.default:
                    codec_order = DefaultValue
                    codec_list  = DefaultValue
                account.rtp.audio_codec_order = codec_order
                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:
                settings.rtp.video_codec_order = DefaultValue  # reset codec order
                settings.rtp.video_codec_list  = DefaultValue  # reset codec list
                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:
                account.rtp.video_codec_order = DefaultValue  # reset codec order
                account.rtp.video_codec_list  = DefaultValue  # reset codec list
                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
                account.save()

    def load_audio_devices(self):
        settings = SIPSimpleSettings()

        class Separator: pass

        self.audio_input_device_button.clear()
        self.audio_input_device_button.addItem('System Default', 'system_default')
        self.audio_input_device_button.insertSeparator(1)
        self.audio_input_device_button.setItemData(1, Separator)  # prevent the separator from being selected (must have different itemData than the None device)
        for device in SIPApplication.engine.input_devices:
            self.audio_input_device_button.addItem(device, device)
        self.audio_input_device_button.addItem('None', None)
        self.audio_input_device_button.setCurrentIndex(self.audio_input_device_button.findData(settings.audio.input_device))

        self.audio_output_device_button.clear()
        self.audio_output_device_button.addItem('System Default', 'system_default')
        self.audio_output_device_button.insertSeparator(1)
        self.audio_output_device_button.setItemData(1, Separator)  # prevent the separator from being selected (must have different itemData than the None device)
        for device in SIPApplication.engine.output_devices:
            self.audio_output_device_button.addItem(device, device)
        self.audio_output_device_button.addItem('None', None)
        self.audio_output_device_button.setCurrentIndex(self.audio_output_device_button.findData(settings.audio.output_device))

        self.audio_alert_device_button.clear()
        self.audio_alert_device_button.addItem('System Default', 'system_default')
        self.audio_alert_device_button.insertSeparator(1)
        self.audio_alert_device_button.setItemData(1, Separator)  # prevent the separator from being selected (must have different itemData than the None device)
        for device in SIPApplication.engine.output_devices:
            self.audio_alert_device_button.addItem(device, device)
        self.audio_alert_device_button.addItem('None', None)
        self.audio_alert_device_button.setCurrentIndex(self.audio_alert_device_button.findData(settings.audio.alert_device))

    def load_video_devices(self):
        settings = SIPSimpleSettings()

        class Separator: pass

        self.video_camera_button.clear()
        self.video_camera_button.addItem('System Default', 'system_default')
        self.video_camera_button.insertSeparator(1)
        self.video_camera_button.setItemData(1, Separator)  # prevent the separator from being selected (must have different itemData than the None device)
        for device in SIPApplication.engine.video_devices:
            self.video_camera_button.addItem(device, device)
        self.video_camera_button.addItem('None', None)
        self.video_camera_button.setCurrentIndex(self.video_camera_button.findData(settings.video.device))

    def load_settings(self):
        """Load settings from configuration into the UI controls"""
        settings = SIPSimpleSettings()
        blink_settings = BlinkSettings()

        # Audio devices
        self.load_audio_devices()
        self.enable_echo_cancelling_button.setChecked(settings.audio.echo_canceller.enabled)
        with blocked_qt_signals(self.tail_length_slider):
            self.tail_length_slider.setValue(settings.audio.echo_canceller.tail_length)
        self.audio_sample_rate_button.clear()
        for rate in SIPSimpleSettings.audio.sample_rate.type.valid_values:
            self.audio_sample_rate_button.addItem(str(rate), rate)
        self.audio_sample_rate_button.setCurrentIndex(self.audio_sample_rate_button.findText(str(settings.audio.sample_rate)))

        # Audio codecs
        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)

        # Answering Machine settings
        self.enable_answering_machine_button.setChecked(settings.answering_machine.enabled)
        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)
        # TODO: load unavailable message -Dan

        # Video devices
        self.load_video_devices()

        self.video_resolution_button.setCurrentIndex(self.video_resolution_button.findData(str(settings.video.resolution)))
        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)

        self.h264_profile_button.setCurrentIndex(self.h264_profile_button.findData(str(settings.video.h264.profile)))
        self.video_codec_bitrate_button.setCurrentIndex(self.video_codec_bitrate_button.findData(settings.video.max_bitrate))

        # Chat and SMS settings
        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)

        self.auto_accept_chat_button.setChecked(settings.chat.auto_accept)
        self.chat_message_alert_button.setChecked(settings.sounds.play_message_alerts)
        self.sms_replication_button.setChecked(settings.chat.sms_replication)

        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)

        # 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)

        # File logging settings
        self.trace_sip_button.setChecked(settings.logs.trace_sip)
        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)
        with blocked_qt_signals(self.pjsip_trace_level):
            self.pjsip_trace_level.setValue(limit(settings.logs.pjsip_level, min=0, max=5))

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

        if settings.sip.tcp_port and settings.sip.tcp_port == settings.sip.tls_port:
            log.warning("the SIP TLS and TCP ports cannot be the same")
            settings.sip.tls_port = settings.sip.tcp_port+1 if settings.sip.tcp_port < 65535 else 65534
            settings.save()

        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)

        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 '')

    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)
        self.display_name_editor.setText(account.display_name or '')
        if account is not bonjour_account:
            self.password_editor.setText(account.auth.password)
            selected_index = self.account_list.selectionModel().selectedIndexes()[0]
            selected_account_info = self.account_list.model().data(selected_index, Qt.UserRole)
            if not account.enabled:
                selected_account_info.registration_state = None
                selected_account_info.registrar = None
                
            if selected_account_info.registration_state:
                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())
            else:
                self.account_registration_label.setText('Not Registered')
        else:
            self.account_registration_label.setText('')

        # Media tab
        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)

        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)

        self.reset_account_audio_codecs_button.setEnabled(account.rtp.audio_codec_order is not None)
        self.reset_account_video_codecs_button.setEnabled(account.rtp.video_codec_order is not None)

        self.inband_dtmf_button.setChecked(account.rtp.inband_dtmf)
        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))

        if account is not bonjour_account:
            # Server settings tab
            self.always_use_my_proxy_button.setChecked(account.sip.always_use_my_proxy)
            outbound_proxy = account.sip.outbound_proxy or UnspecifiedOutboundProxy
            self.outbound_proxy_host_editor.setText(outbound_proxy.host)
            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()))
            self.auth_username_editor.setText(account.auth.username or '')

            self.always_use_my_msrp_relay_button.setChecked(account.nat_traversal.use_msrp_relay_for_outbound)
            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()))

            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 '')

            # 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
            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)
            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()

            self.account_tls_cert_file_editor.setText(account.tls.certificate or '')
            self.account_tls_verify_server_button.setChecked(account.tls.verify_server)

    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())
        font_family = blink_settings.chat_window.font or style.font_family
        font_size = blink_settings.chat_window.font_size or style.font_size
        user_icons = 'show-icons' if blink_settings.chat_window.show_user_icons else 'hide-icons'

        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))
        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

    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:
                index = next(index for index, account in enumerate(model.accounts) if account is default_account)
            except StopIteration:
                index = 0
            selection_model.select(model.index(index), selection_model.ClearAndSelect)
        self._update_logs_size_label()
        super(PreferencesWindow, self).show()
        self.raise_()
        self.activateWindow()

    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
        self.log_files_size_label.setText("There are currently %s of log files" % self._normalize_binary_size(logs_size))

    def _update_pstn_example_label(self):
        prefix = self.prefix_button.currentText()
        idd_prefix = self.idd_prefix_button.currentText()
        self.pstn_example_transformed_label.setText("%s%s442079460000" % ('' if prefix == 'None' else prefix, idd_prefix))

    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')
            chat_element.setStyleProperty('top', '%dpx' % (widget_height-content_height))
        else:
            chat_element.setStyleProperty('position', 'static')
            chat_element.setStyleProperty('top', None)
        frame = self.style_view.page().mainFrame()
        if scroll or frame.scrollBarMaximum(Qt.Vertical) - frame.scrollBarValue(Qt.Vertical) <= widget_height*0.2:
            frame = self.style_view.page().mainFrame()
            frame.setScrollBarValue(Qt.Vertical, frame.scrollBarMaximum(Qt.Vertical))

    # Signal handlers
    #
    def _SH_ToolbarActionTriggered(self, action):
        if action == self.logging_action:
            self._update_logs_size_label()
        self.pages.setCurrentIndex(action.index)

    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
    
        self.load_account_settings(selected_account)
    
    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:
            selected_account = selected_index.data(Qt.UserRole).account
            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:
                    tab_widget.addTab(self.server_settings_tab, "Server Settings")
                if tab_widget.indexOf(self.network_tab) == -1:
                    tab_widget.addTab(self.network_tab, "Network")
                if tab_widget.indexOf(self.advanced_tab) == -1:
                    tab_widget.addTab(self.advanced_tab, "Advanced")
                self.password_label.show()
                self.password_editor.show()
                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
            self.load_account_settings(selected_account)

    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:
                    self.account_registration_label.setText('Registration %s' % account_info.registration_state.title())
                else:
                    self.account_registration_label.setText('Not Registered')

    def _SH_DeleteAccountButtonClicked(self):
        model = self.account_list.model()

        selected_index = self.account_list.selectionModel().selectedIndexes()[0]
        selected_account = selected_index.data(Qt.UserRole).account

        title, message = "Remove Account", "Permanently remove account %s?" % selected_account.id
        if QMessageBox.question(self, title, message, QMessageBox.Ok|QMessageBox.Cancel) == QMessageBox.Cancel:
            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)
            if position < len(active_accounts)-1:
                account_manager.default_account = active_accounts[position+1]
            elif position > 0:
                account_manager.default_account = active_accounts[position-1]
            else:
                account_manager.default_account = None

        if selected_account.tls.certificate is not None and selected_account.tls.certificate.normalized.startswith(ApplicationData.directory):
            try:
                os.unlink(selected_account.tls.certificate.normalized)
            except (AttributeError, OSError, IOError):
                pass

        selected_account.delete()

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

    def _SH_DisplayNameEditorEditingFinished(self):
        account = self.selected_account
        display_name = self.display_name_editor.text() or None
        if account.display_name != display_name:
            account.display_name = display_name
            account.save()

    def _SH_PasswordEditorEditingFinished(self):
        account = self.selected_account
        password = self.password_editor.text()
        if account.auth.password != password:
            account.auth.password = password
            account.save()

    # Account media settings
    def _SH_AccountAudioCodecsListItemChanged(self, item):
        account = self.selected_account
        items = [self.account_audio_codecs_list.item(row) for row in range(self.account_audio_codecs_list.count())]
        account.rtp.audio_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
        account.rtp.audio_codec_order = [item.text() for item in items]
        account.save()

    def _SH_AccountAudioCodecsListModelRowsMoved(self, source_parent, source_start, source_end, dest_parent, dest_row):
        account = self.selected_account
        items = [self.account_audio_codecs_list.item(row) for row in range(self.account_audio_codecs_list.count())]
        account.rtp.audio_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
        account.rtp.audio_codec_order = [item.text() for item in items]
        account.save()

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

        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)

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

    def _SH_AccountVideoCodecsListItemChanged(self, item):
        account = self.selected_account
        items = [self.account_video_codecs_list.item(row) for row in range(self.account_video_codecs_list.count())]
        account.rtp.video_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
        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
        items = [self.account_video_codecs_list.item(row) for row in range(self.account_video_codecs_list.count())]
        account.rtp.video_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
        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()

    def _SH_InbandDTMFButtonClicked(self, checked):
        account = self.selected_account
        account.rtp.inband_dtmf = checked
        account.save()

    def _SH_RTPEncryptionButtonClicked(self, checked):
        self.key_negotiation_button.setEnabled(checked)
        account = self.selected_account
        account.rtp.encryption.enabled = checked
        account.save()

    def _SH_KeyNegotiationButtonActivated(self, index):
        account = self.selected_account
        account.rtp.encryption.key_negotiation = self.key_negotiation_button.itemData(index)
        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):
        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_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
        auth_username = self.auth_username_editor.text() or None
        if account.auth.username != auth_username:
            account.auth.username = auth_username
            account.save()

    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()

    def _SH_MSRPRelayPortValueChanged(self, value):
        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_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
        voicemail_uri = self.voicemail_uri_editor.text() or None
        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
        xcap_root = self.xcap_root_editor.text() or None
        if account.xcap.xcap_root != xcap_root:
            account.xcap.xcap_root = xcap_root
            account.save()

    def _SH_ServerToolsURLEditorEditingFinished(self):
        account = self.selected_account
        url = self.server_tools_url_editor.text() or None
        if account.server.settings_url != url:
            account.server.settings_url = url
            account.save()

    def _SH_ConferenceServerEditorEditingFinished(self):
        account = self.selected_account
        server = self.conference_server_editor.text() or None
        if account.server.conference_server != server:
            account.server.conference_server = server
            account.save()

    # 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
        account.msrp.transport = text.lower()
        account.save()

    # Account advanced settings
    def _SH_RegisterIntervalValueChanged(self, value):
        account = self.selected_account
        account.sip.register_interval = value
        account.save()

    def _SH_PublishIntervalValueChanged(self, value):
        account = self.selected_account
        account.sip.publish_interval = value
        account.save()

    def _SH_SubscribeIntervalValueChanged(self, value):
        account = self.selected_account
        account.sip.subscribe_interval = value
        account.save()

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

    def _SH_IDDPrefixButtonActivated(self, text):
        self._update_pstn_example_label()
        account = self.selected_account
        idd_prefix = None if text == '+' else text
        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
        prefix = None if text == 'None' else text
        if account.pstn.prefix != prefix:
            account.pstn.prefix = prefix
            account.save()

    def _SH_AccountTLSCertFileEditorLocationCleared(self):
        account = self.selected_account
        account.tls.certificate = None
        account.save()

    def _SH_AccountTLSCertFileBrowseButtonClicked(self, checked):
        # TODO: open the file selection dialog in non-modal mode (and the error messages boxes as well). -Dan
        account = self.selected_account
        directory = os.path.dirname(account.tls.certificate.normalized) if account.tls.certificate else Path('~').normalized
        cert_path = QFileDialog.getOpenFileName(self, 'Select Certificate File', directory, "TLS certificates (*.crt *.pem)")[0] or None
        if cert_path is not None:
            cert_path = os.path.normpath(cert_path)
            if cert_path != account.tls.certificate:
                try:
                    contents = open(cert_path).read()
                    X509Certificate(contents)
                    X509PrivateKey(contents)
                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)
                else:
                    self.account_tls_cert_file_editor.setText(cert_path)
                    account.tls.certificate = cert_path
                    account.save()

    def _SH_AccountTLSVerifyServerButtonClicked(self, checked):
        account = self.selected_account
        account.tls.verify_server = checked
        account.save()

    # Audio devices signal handlers
    def _SH_AudioAlertDeviceButtonActivated(self, index):
        device = self.audio_alert_device_button.itemData(index)
        settings = SIPSimpleSettings()
        settings.audio.alert_device = device
        settings.save()

    def _SH_AudioInputDeviceButtonActivated(self, index):
        device = self.audio_input_device_button.itemData(index)
        settings = SIPSimpleSettings()
        settings.audio.input_device = device
        settings.save()

    def _SH_AudioOutputDeviceButtonActivated(self, index):
        device = self.audio_output_device_button.itemData(index)
        settings = SIPSimpleSettings()
        settings.audio.output_device = device
        settings.save()

    def _SH_AudioSampleRateButtonActivated(self, text):
        settings = SIPSimpleSettings()
        settings.audio.sample_rate = text
        settings.save()

    def _SH_EnableEchoCancellingButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.audio.echo_canceller.enabled = checked
        settings.save()

    def _SH_TailLengthSliderValueChanged(self, value):
        settings = SIPSimpleSettings()
        settings.audio.echo_canceller.tail_length = value
        settings.save()

    # Audio codecs signal handlers
    def _SH_AudioCodecsListItemChanged(self, item):
        settings = SIPSimpleSettings()
        item_iterator = (self.audio_codecs_list.item(row) for row in range(self.audio_codecs_list.count()))
        settings.rtp.audio_codec_list = [item.text() for item in item_iterator if item.checkState() == Qt.Checked]
        settings.save()

    def _SH_AudioCodecsListModelRowsMoved(self, source_parent, source_start, source_end, dest_parent, dest_row):
        settings = SIPSimpleSettings()
        items = [self.audio_codecs_list.item(row) for row in range(self.audio_codecs_list.count())]
        settings.rtp.audio_codec_order = [item.text() for item in items]
        settings.rtp.audio_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
        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:
            self.answer_delay_seconds_label.setText('')
        elif value == 1:
            self.answer_delay_seconds_label.setText('second')
        else:
            self.answer_delay_seconds_label.setText('seconds')
        settings = SIPSimpleSettings()
        if settings.answering_machine.answer_delay != value:
            settings.answering_machine.answer_delay = value
            settings.save()

    def _SH_MaxRecordingValueChanged(self, value):
        self.max_recording_minutes_label.setText('minute' if value == 1 else 'minutes')
        settings = SIPSimpleSettings()
        if settings.answering_machine.max_recording != value:
            settings.answering_machine.max_recording = value
            settings.save()

    # 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()
        item_iterator = (self.video_codecs_list.item(row) for row in range(self.video_codecs_list.count()))
        settings.rtp.video_codec_list = [item.text() for item in item_iterator if item.checkState() == Qt.Checked]
        settings.save()

    def _SH_VideoCodecsListModelRowsMoved(self, source_parent, source_start, source_end, dest_parent, dest_row):
        settings = SIPSimpleSettings()
        items = [self.video_codecs_list.item(row) for row in range(self.video_codecs_list.count())]
        settings.rtp.video_codec_order = [item.text() for item in items]
        settings.rtp.video_codec_list = [item.text() for item in items if item.checkState() == Qt.Checked]
        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()

    # Chat and SMS signal handlers
    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)

    def _SH_AutoAcceptChatButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.chat.auto_accept = checked
        settings.save()

    def _SH_ChatMessageAlertButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.sounds.play_message_alerts = checked
        settings.save()

    def _SH_SMSReplicationButtonClicked(self, checked):
        settings = SIPSimpleSettings()
        settings.chat.sms_replication = checked
        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()

    # 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()
        directory = QFileDialog.getExistingDirectory(self, 'Select Screenshots Directory', settings.screenshots_directory.normalized) or None
        if directory is not None:
            directory = os.path.normpath(directory)
            if directory != settings.screenshots_directory:
                self.screenshots_directory_editor.setText(directory)
                settings.screenshots_directory = directory
                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()

    # File transfer signal handlers
    def _SH_TransfersDirectoryBrowseButtonClicked(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()
        directory = QFileDialog.getExistingDirectory(self, 'Select Transfers Directory', settings.transfers_directory.normalized) or None
        if directory is not None:
            directory = os.path.normpath(directory)
            if directory != settings.transfers_directory:
                self.transfers_directory_editor.setText(directory)
                settings.transfers_directory = directory
                settings.save()

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

    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()
        if settings.logs.pjsip_level != value:
            settings.logs.pjsip_level = value
            settings.save()

    @run_in_thread('file-io')
    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()
        if settings.sip.udp_port != value:
            settings.sip.udp_port = value
            settings.save()

    def _SH_TCPPortValueChanged(self, value):
        settings = SIPSimpleSettings()
        if settings.sip.tcp_port != value:
            settings.sip.tcp_port = value
            settings.save()

    def _SH_TLSPortValueChanged(self, value):
        settings = SIPSimpleSettings()
        if settings.sip.tls_port != value:
            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())
        if settings.rtp.port_range != port_range:
            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)
        if settings.rtp.port_range != port_range:
            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()
        directory = os.path.dirname(settings.tls.ca_list.normalized) if settings.tls.ca_list else Path('~').normalized
        ca_path = QFileDialog.getOpenFileName(self, 'Select Certificate Authority File', directory, "TLS certificates (*.crt *.pem)")[0] or None
        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())
                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)
                else:
                    self.tls_ca_file_editor.setText(ca_path)
                    settings.tls.ca_list = ca_path
                    settings.save()

    @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()
        notification.center.add_observer(self, name='AudioDevicesDidChange')
        notification.center.add_observer(self, name='VideoDevicesDidChange')
        notification.center.add_observer(self, name='VideoDeviceDidChangeCamera')
        notification.center.add_observer(self, name='CFGSettingsObjectDidChange')
        notification.center.add_observer(self, name='SIPRegistrationInfoDidChange')

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

    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

    def _NH_CFGSettingsObjectDidChange(self, notification):
        settings = SIPSimpleSettings()
        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:
            if 'audio.alert_device' in notification.data.modified:
                self.audio_alert_device_button.setCurrentIndex(self.audio_alert_device_button.findData(settings.audio.alert_device))
            if 'audio.input_device' in notification.data.modified:
                self.audio_input_device_button.setCurrentIndex(self.audio_input_device_button.findData(settings.audio.input_device))
            if 'audio.output_device' in notification.data.modified:
                self.audio_output_device_button.setCurrentIndex(self.audio_output_device_button.findData(settings.audio.output_device))
            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)
            if 'sounds.play_message_alerts' in notification.data.modified:
                self.chat_message_alert_button.setChecked(settings.sounds.play_message_alerts)
            if 'video.device' in notification.data.modified:
                self.video_camera_button.setCurrentIndex(self.video_camera_button.findData(settings.video.device))
        elif notification.sender is self.selected_account is not None:
            account = notification.sender
            if 'enabled' in notification.data.modified:
                self.account_enabled_button.setChecked(account.enabled)
                if not account.enabled:
                    self.refresh_account_registration_widgets(account)
                self.reregister_button.setEnabled(account.enabled)
            if 'display_name' in notification.data.modified:
                self.display_name_editor.setText(account.display_name or '')
            if 'rtp.audio_codec_list' in notification.data.modified:
                self.reset_account_audio_codecs_button.setEnabled(account.rtp.audio_codec_list is not None)
            if 'rtp.video_codec_list' in notification.data.modified:
                self.reset_account_video_codecs_button.setEnabled(account.rtp.video_codec_list is not None)

del ui_class, base_class