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