Commit 2aa132a3 authored by Dan Pascu's avatar Dan Pascu

Added dialog for adding and creating SIP accounts

parent e3110d79
...@@ -8,19 +8,26 @@ __version__ = '0.1.0' ...@@ -8,19 +8,26 @@ __version__ = '0.1.0'
__date__ = 'July 5, 2010' __date__ = 'July 5, 2010'
import os
import sys import sys
from collections import defaultdict
import cjson
from PyQt4.QtCore import QThread from PyQt4.QtCore import QThread
from PyQt4.QtGui import QApplication from PyQt4.QtGui import QApplication
from application import log from application import log
from application.notification import IObserver, NotificationCenter from application.notification import IObserver, NotificationCenter
from application.python.util import Null from application.python.util import Null
from application.system import unlink
from gnutls.crypto import X509Certificate, X509PrivateKey
from gnutls.errors import GNUTLSError
from zope.interface import implements from zope.interface import implements
from sipsimple.account import Account, BonjourAccount from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.application import SIPApplication from sipsimple.application import SIPApplication
from sipsimple.configuration.backend.file import FileBackend from sipsimple.configuration.backend.file import FileBackend
from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.util import makedirs
from blink.configuration.account import AccountExtension, BonjourAccountExtension from blink.configuration.account import AccountExtension, BonjourAccountExtension
from blink.configuration.settings import SIPSimpleSettingsExtension from blink.configuration.settings import SIPSimpleSettingsExtension
...@@ -84,6 +91,81 @@ class Blink(QApplication): ...@@ -84,6 +91,81 @@ class Blink(QApplication):
log_manager = LogManager() log_manager = LogManager()
log_manager.stop() log_manager.stop()
def fetch_account(self):
filename = os.path.expanduser('~/.blink_account')
if not os.path.exists(filename):
return
try:
data = open(filename).read()
data = cjson.decode(data.replace(r'\/', '/'))
except (OSError, IOError), e:
print "Failed to read json data from ~/.blink_account: %s" % e
return
except cjson.DecodeError, e:
print "Failed to decode json data from ~/.blink_account: %s" % e
return
finally:
unlink(filename)
data = defaultdict(lambda: None, data)
account_id = data['sip_address']
if account_id is None:
return
account_manager = AccountManager()
try:
account = account_manager.get_account(account_id)
except KeyError:
account = Account(account_id)
account.display_name = data['display_name']
default_account = account
else:
default_account = account_manager.default_account
account.auth.username = data['auth_username']
account.auth.password = data['password'] or ''
account.sip.outbound_proxy = data['outbound_proxy']
account.xcap.xcap_root = data['xcap_root']
account.nat_traversal.msrp_relay = data['msrp_relay']
account.server.settings_url = data['settings_url']
if data['passport'] is not None:
try:
passport = data['passport']
certificate_path = self.save_certificates(account_id, passport['crt'], passport['key'], passport['ca'])
account.tls.certificate = certificate_path
except (GNUTLSError, IOError, OSError):
pass
account.enabled = True
account.save()
account_manager.default_account = default_account
def save_certificates(self, sip_address, crt, key, ca):
crt = crt.strip() + os.linesep
key = key.strip() + os.linesep
ca = ca.strip() + os.linesep
X509Certificate(crt)
X509PrivateKey(key)
X509Certificate(ca)
makedirs(ApplicationData.get('tls'))
certificate_path = ApplicationData.get(os.path.join('tls', sip_address+'.crt'))
file = open(certificate_path, 'w')
os.chmod(certificate_path, 0600)
file.write(crt+key)
file.close()
ca_path = ApplicationData.get(os.path.join('tls', 'ca.crt'))
try:
existing_cas = open(ca_path).read().strip() + os.linesep
except:
file = open(ca_path, 'w')
file.write(ca)
file.close()
else:
if ca not in existing_cas:
file = open(ca_path, 'w')
file.write(existing_cas+ca)
file.close()
settings = SIPSimpleSettings()
settings.tls.ca_list = ca_path
settings.save()
return certificate_path
def customEvent(self, event): def customEvent(self, event):
handler = getattr(self, '_EH_%s' % event.name, Null) handler = getattr(self, '_EH_%s' % event.name, Null)
handler(event) handler(event)
...@@ -105,6 +187,10 @@ class Blink(QApplication): ...@@ -105,6 +187,10 @@ class Blink(QApplication):
@run_in_gui_thread @run_in_gui_thread
def _NH_SIPApplicationDidStart(self, notification): def _NH_SIPApplicationDidStart(self, notification):
account_manager = AccountManager()
self.fetch_account()
if account_manager.get_accounts() == [BonjourAccount()]:
self.main_window.add_account_dialog.open_for_create()
self.main_window.show() self.main_window.show()
self.update_manager.initialize() self.update_manager.initialize()
......
# Copyright (C) 2010 AG Projects. See LICENSE for details. # Copyright (C) 2010 AG Projects. See LICENSE for details.
# #
__all__ = ['AccountModel', 'ActiveAccountModel', 'AccountSelector'] from __future__ import with_statement
__all__ = ['AccountModel', 'ActiveAccountModel', 'AccountSelector', 'AddAccountDialog']
import os
import re
import urllib
import urllib2
from collections import defaultdict
from PyQt4 import uic
from PyQt4.QtCore import Qt, QAbstractListModel, QModelIndex from PyQt4.QtCore import Qt, QAbstractListModel, QModelIndex
from PyQt4.QtGui import QComboBox, QIcon, QPalette, QPixmap, QSortFilterProxyModel, QStyledItemDelegate from PyQt4.QtGui import QButtonGroup, QComboBox, QIcon, QPalette, QPixmap, QSortFilterProxyModel, QStyledItemDelegate
import cjson
from application.notification import IObserver, NotificationCenter from application.notification import IObserver, NotificationCenter
from application.python.util import Null from application.python.util import Null
from gnutls.errors import GNUTLSError
from zope.interface import implements from zope.interface import implements
from sipsimple.account import Account, AccountManager, BonjourAccount from sipsimple.account import Account, AccountManager, BonjourAccount
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.util import user_info
from blink.resources import Resources from blink.resources import Resources
from blink.util import run_in_gui_thread from blink.widgets.labels import Status
from blink.util import QSingleton, call_in_auxiliary_thread, call_in_gui_thread, run_in_auxiliary_thread, run_in_gui_thread
class AccountInfo(object): class AccountInfo(object):
...@@ -210,3 +224,240 @@ class AccountSelector(QComboBox): ...@@ -210,3 +224,240 @@ class AccountSelector(QComboBox):
source_model = model.sourceModel() source_model = model.sourceModel()
account_index = source_model.accounts.index(account) account_index = source_model.accounts.index(account)
self.setCurrentIndex(model.mapFromSource(source_model.index(account_index)).row()) self.setCurrentIndex(model.mapFromSource(source_model.index(account_index)).row())
ui_class, base_class = uic.loadUiType(Resources.get('add_account.ui'))
class AddAccountDialog(base_class, ui_class):
__metaclass__ = QSingleton
def __init__(self, parent=None):
super(AddAccountDialog, self).__init__(parent)
with Resources.directory:
self.setupUi(self)
self.background_frame.setStyleSheet("")
self.button_group = QButtonGroup(self)
self.button_group.setObjectName("button_group")
self.button_group.addButton(self.add_account_button, self.panel_view.indexOf(self.add_account_panel))
self.button_group.addButton(self.create_account_button, self.panel_view.indexOf(self.create_account_panel))
font = self.title_label.font()
font.setPointSizeF(self.info_label.fontInfo().pointSizeF() + 3)
font.setFamily("Sans Serif")
self.title_label.setFont(font)
font_metrics = self.create_status_label.fontMetrics()
self.create_status_label.setMinimumHeight(font_metrics.height() + 2*(font_metrics.height() + font_metrics.leading())) # reserve space for 3 lines
font_metrics = self.email_note_label.fontMetrics()
self.email_note_label.setMinimumWidth(font_metrics.width(u'The E-mail address is used when sending voicemail')) # hack to make text justification look nice everywhere
self.add_account_button.setChecked(True)
self.panel_view.setCurrentWidget(self.add_account_panel)
self.new_password_editor.textChanged.connect(self._SH_PasswordTextChanged)
self.button_group.buttonClicked[int].connect(self._SH_PanelChangeRequest)
self.accept_button.clicked.connect(self._SH_AcceptButtonClicked)
self.display_name_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
self.name_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
self.username_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
self.sip_address_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
self.password_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
self.new_password_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
self.verify_password_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
self.email_address_editor.statusChanged.connect(self._SH_ValidityStatusChanged)
self.display_name_editor.regexp = re.compile('^.*$')
self.name_editor.regexp = re.compile('^.+$')
self.username_editor.regexp = re.compile('^[a-z1-9][a-z0-9_.-]{1,65}[a-z0-9]$')
self.sip_address_editor.regexp = re.compile('^[^@]+@.+$')
self.password_editor.regexp = re.compile('^.*$')
self.new_password_editor.regexp = re.compile('^.{8,}$')
self.verify_password_editor.regexp = re.compile('^$')
self.email_address_editor.regexp = re.compile('^[^@]+@.+$')
def _get_display_name(self):
if self.panel_view.currentWidget() is self.add_account_panel:
return unicode(self.display_name_editor.text())
else:
return unicode(self.name_editor.text())
def _set_display_name(self, value):
self.display_name_editor.setText(value)
self.name_editor.setText(value)
def _get_username(self):
return unicode(self.username_editor.text())
def _set_username(self, value):
self.username_editor.setText(value)
def _get_sip_address(self):
return unicode(self.sip_address_editor.text())
def _set_sip_address(self, value):
self.sip_address_editor.setText(value)
def _get_password(self):
if self.panel_view.currentWidget() is self.add_account_panel:
return unicode(self.password_editor.text())
else:
return unicode(self.new_password_editor.text())
def _set_password(self, value):
self.password_editor.setText(value)
self.new_password_editor.setText(value)
def _get_verify_password(self):
return unicode(self.verify_password_editor.text())
def _set_verify_password(self, value):
self.verify_password_editor.setText(value)
def _get_email_address(self):
return unicode(self.email_address_editor.text())
def _set_email_address(self, value):
self.email_address_editor.setText(value)
display_name = property(_get_display_name, _set_display_name)
username = property(_get_username, _set_username)
sip_address = property(_get_sip_address, _set_sip_address)
password = property(_get_password, _set_password)
verify_password = property(_get_verify_password, _set_verify_password)
email_address = property(_get_email_address, _set_email_address)
del _get_display_name, _set_display_name, _get_username, _set_username
del _get_sip_address, _set_sip_address, _get_email_address, _set_email_address
del _get_password, _set_password, _get_verify_password, _set_verify_password
def _SH_AcceptButtonClicked(self):
if self.panel_view.currentWidget() is self.add_account_panel:
account = Account(self.sip_address)
account.enabled = True
account.display_name = self.display_name
account.auth.password = self.password
call_in_auxiliary_thread(account.save)
account_manager = AccountManager()
account_manager.default_account = account
self.accept()
else:
self.setEnabled(False)
self.create_status_label.value = Status('Creating account on server...')
self._create_sip_account(self.username, self.password, self.email_address, self.display_name)
def _SH_PanelChangeRequest(self, index):
self.panel_view.setCurrentIndex(index)
if self.panel_view.currentWidget() is self.add_account_panel:
inputs = [self.display_name_editor, self.sip_address_editor, self.password_editor]
else:
inputs = [self.name_editor, self.username_editor, self.new_password_editor, self.verify_password_editor, self.email_address_editor]
self.accept_button.setEnabled(all(input.valid for input in inputs))
def _SH_PasswordTextChanged(self, text):
self.verify_password_editor.regexp = re.compile(u'^%s$' % re.escape(unicode(text)))
def _SH_ValidityStatusChanged(self):
red = '#cc0000'
# validate the add panel
if not self.display_name_editor.valid:
self.add_status_label.value = Status("Display name cannot be empty", color=red)
elif not self.sip_address_editor.valid:
self.add_status_label.value = Status("SIP address should be specified as user@domain", color=red)
elif not self.password_editor.valid:
self.add_status_label.value = Status("Password cannot be empty", color=red)
else:
self.add_status_label.value = None
# validate the create panel
if not self.name_editor.valid:
self.create_status_label.value = Status("Display name cannot be empty", color=red)
elif not self.username_editor.valid:
self.create_status_label.value = Status("Username should have at least 3 characters, start with a letter or a non-zero digit and contain only letters, digits and .-_", color=red)
elif not self.new_password_editor.valid:
self.create_status_label.value = Status("Password should contain at least 8 characters", color=red)
elif not self.verify_password_editor.valid:
self.create_status_label.value = Status("Passwords do not match", color=red)
elif not self.email_address_editor.valid:
self.create_status_label.value = Status("E-mail address should be specified as user@domain", color=red)
else:
self.create_status_label.value = None
# enable the accept button if everything is valid in the current panel
if self.panel_view.currentWidget() is self.add_account_panel:
inputs = [self.display_name_editor, self.sip_address_editor, self.password_editor]
else:
inputs = [self.name_editor, self.username_editor, self.new_password_editor, self.verify_password_editor, self.email_address_editor]
self.accept_button.setEnabled(all(input.valid for input in inputs))
def _initialize(self):
self.display_name = user_info.fullname
self.username = user_info.username
self.sip_address = u''
self.password = u''
self.verify_password = u''
self.email_address = u''
@run_in_auxiliary_thread
def _create_sip_account(self, username, password, email_address, display_name, timezone=None):
red = '#cc0000'
if timezone is None:
try:
timezone = open('/etc/timezone').read().strip()
except (OSError, IOError):
try:
timezone = '/'.join(os.readlink('/etc/localtime').split('/')[-2:])
except (OSError, IOError):
pass
enrollment_data = dict(username=username.encode('utf-8'),
password=password.encode('utf-8'),
email=email_address.encode('utf-8'),
display_name=display_name.encode('utf-8'),
tzinfo=timezone)
try:
settings = SIPSimpleSettings()
response = urllib2.urlopen(settings.server.enrollment_url, urllib.urlencode(dict(enrollment_data)))
response_data = cjson.decode(response.read().replace(r'\/', '/'))
response_data = defaultdict(lambda: None, response_data)
if response_data['success']:
from blink import Blink
try:
certificate_path = None
passport = response_data['passport']
if passport is not None:
certificate_path = Blink().save_certificates(response_data['sip_address'], passport['crt'], passport['key'], passport['ca'])
except (GNUTLSError, IOError, OSError):
pass
account = Account(response_data['sip_address'])
account.enabled = True
account.display_name = display_name
account.auth.password = password
account.sip.outbound_proxy = response_data['outbound_proxy']
account.nat_traversal.msrp_relay = response_data['msrp_relay']
account.xcap.xcap_root = response_data['xcap_root']
account.tls.certificate = certificate_path
account.server.settings_url = response_data['settings_url']
account.save()
account_manager = AccountManager()
account_manager.default_account = account
call_in_gui_thread(self.accept)
elif response_data['error'] == 'user_exists':
call_in_gui_thread(setattr, self.create_status_label, 'value', Status('The username you requested is already taken. Please choose another one and try again.', color=red))
else:
call_in_gui_thread(setattr, self.create_status_label, 'value', Status(response_data['error_message'], color=red))
except (cjson.DecodeError, KeyError):
call_in_gui_thread(setattr, self.create_status_label, 'value', Status('Illegal server response', color=red))
except urllib2.URLError, e:
call_in_gui_thread(setattr, self.create_status_label, 'value', Status('Failed to contact server: %s' % e.reason, color=red))
finally:
call_in_gui_thread(self.setEnabled, True)
def open_for_add(self):
self.add_account_button.click()
self.add_account_button.setFocus()
self.accept_button.setEnabled(False)
self._initialize()
self.show()
def open_for_create(self):
self.create_account_button.click()
self.create_account_button.setFocus()
self.accept_button.setEnabled(False)
self._initialize()
self.show()
del ui_class, base_class
...@@ -5,24 +5,34 @@ ...@@ -5,24 +5,34 @@
__all__ = ['AccountExtension', 'BonjourAccountExtension'] __all__ = ['AccountExtension', 'BonjourAccountExtension']
from sipsimple.account import PSTNSettings from sipsimple.account import PSTNSettings, TLSSettings
from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension
from sipsimple.util import user_info from sipsimple.util import user_info
from blink.configuration.datatypes import CustomSoundFile, DefaultPath from blink.configuration.datatypes import ApplicationDataPath, CustomSoundFile, DefaultPath, HTTPURL
class PSTNSettingsExtension(PSTNSettings): class PSTNSettingsExtension(PSTNSettings):
idd_prefix = Setting(type=unicode, default=None, nillable=True) idd_prefix = Setting(type=unicode, default=None, nillable=True)
class ServerSettings(SettingsGroup):
settings_url = Setting(type=HTTPURL, default=None, nillable=True)
class SoundSettings(SettingsGroup): class SoundSettings(SettingsGroup):
inbound_ringtone = Setting(type=CustomSoundFile, default=CustomSoundFile(DefaultPath), nillable=True) inbound_ringtone = Setting(type=CustomSoundFile, default=CustomSoundFile(DefaultPath), nillable=True)
class TLSSettingsExtension(TLSSettings):
certificate = Setting(type=ApplicationDataPath, default=None, nillable=True)
class AccountExtension(SettingsObjectExtension): class AccountExtension(SettingsObjectExtension):
pstn = PSTNSettingsExtension pstn = PSTNSettingsExtension
server = ServerSettings
sounds = SoundSettings sounds = SoundSettings
tls = TLSSettingsExtension
display_name = Setting(type=str, default=user_info.fullname, nillable=True) display_name = Setting(type=str, default=user_info.fullname, nillable=True)
......
...@@ -3,10 +3,13 @@ ...@@ -3,10 +3,13 @@
"""Definitions of datatypes for use in settings extensions.""" """Definitions of datatypes for use in settings extensions."""
__all__ = ['ApplicationDataPath', 'SoundFile', 'DefaultPath', 'CustomSoundFile'] __all__ = ['ApplicationDataPath', 'SoundFile', 'DefaultPath', 'CustomSoundFile', 'HTTPURL']
import os import os
import re import re
from urlparse import urlparse
from sipsimple.configuration.datatypes import Hostname
from blink.resources import ApplicationData from blink.resources import ApplicationData
...@@ -98,3 +101,15 @@ class CustomSoundFile(object): ...@@ -98,3 +101,15 @@ class CustomSoundFile(object):
del _get_path, _set_path del _get_path, _set_path
class HTTPURL(unicode):
def __new__(cls, value):
value = unicode(value)
url = urlparse(value)
if url.scheme not in (u'http', u'https'):
raise ValueError("illegal HTTP URL scheme (http and https only): %s" % url.scheme)
Hostname(url.hostname)
if url.port is not None and not (0 < url.port < 65536):
raise ValueError("illegal port value: %d" % url.port)
return value
...@@ -9,10 +9,10 @@ import platform ...@@ -9,10 +9,10 @@ import platform
import sys import sys
from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension
from sipsimple.configuration.settings import AudioSettings, LogsSettings from sipsimple.configuration.settings import AudioSettings, LogsSettings, TLSSettings
from blink import __version__ from blink import __version__
from blink.configuration.datatypes import ApplicationDataPath, SoundFile from blink.configuration.datatypes import ApplicationDataPath, HTTPURL, SoundFile
from blink.resources import Resources from blink.resources import Resources
...@@ -27,15 +27,25 @@ class LogsSettingsExtension(LogsSettings): ...@@ -27,15 +27,25 @@ class LogsSettingsExtension(LogsSettings):
trace_notifications = Setting(type=bool, default=False) trace_notifications = Setting(type=bool, default=False)
class ServerSettings(SettingsGroup):
enrollment_url = Setting(type=HTTPURL, default="https://blink.sipthor.net/enrollment.phtml")
class SoundSettings(SettingsGroup): class SoundSettings(SettingsGroup):
inbound_ringtone = Setting(type=SoundFile, default=SoundFile(Resources.get('sounds/inbound_ringtone.wav')), nillable=True) inbound_ringtone = Setting(type=SoundFile, default=SoundFile(Resources.get('sounds/inbound_ringtone.wav')), nillable=True)
outbound_ringtone = Setting(type=SoundFile, default=SoundFile(Resources.get('sounds/outbound_ringtone.wav')), nillable=True) outbound_ringtone = Setting(type=SoundFile, default=SoundFile(Resources.get('sounds/outbound_ringtone.wav')), nillable=True)
class TLSSettingsExtension(TLSSettings):
ca_list = Setting(type=ApplicationDataPath, default=None, nillable=True)
class SIPSimpleSettingsExtension(SettingsObjectExtension): class SIPSimpleSettingsExtension(SettingsObjectExtension):
audio = AudioSettingsExtension audio = AudioSettingsExtension
logs = LogsSettingsExtension logs = LogsSettingsExtension
server = ServerSettings
sounds = SoundSettings sounds = SoundSettings
tls = TLSSettingsExtension
user_agent = Setting(type=str, default='Blink %s (%s)' % (__version__, platform.system() if sys.platform!='darwin' else 'MacOSX Qt')) user_agent = Setting(type=str, default='Blink %s (%s)' % (__version__, platform.system() if sys.platform!='darwin' else 'MacOSX Qt'))
......
...@@ -20,7 +20,7 @@ from sipsimple.application import SIPApplication ...@@ -20,7 +20,7 @@ from sipsimple.application import SIPApplication
from sipsimple.configuration.settings import SIPSimpleSettings from sipsimple.configuration.settings import SIPSimpleSettings
from blink.aboutpanel import AboutPanel from blink.aboutpanel import AboutPanel
from blink.accounts import AccountModel, ActiveAccountModel from blink.accounts import AccountModel, ActiveAccountModel, AddAccountDialog
from blink.contacts import BonjourNeighbour, Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel from blink.contacts import BonjourNeighbour, Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel
from blink.sessions import SessionManager, SessionModel from blink.sessions import SessionManager, SessionModel
from blink.resources import Resources from blink.resources import Resources
...@@ -62,6 +62,7 @@ class MainWindow(base_class, ui_class): ...@@ -62,6 +62,7 @@ class MainWindow(base_class, ui_class):
self.contact_model.load() self.contact_model.load()
self.about_panel = AboutPanel(self) self.about_panel = AboutPanel(self)
self.add_account_dialog = AddAccountDialog(self)
self.contact_editor_dialog = ContactEditorDialog(self.contact_model, self) self.contact_editor_dialog = ContactEditorDialog(self.contact_model, self)
self.session_model = SessionModel(self) self.session_model = SessionModel(self)
...@@ -112,6 +113,7 @@ class MainWindow(base_class, ui_class): ...@@ -112,6 +113,7 @@ class MainWindow(base_class, ui_class):
self.search_box.shortcut.activated.connect(self.search_box.setFocus) self.search_box.shortcut.activated.connect(self.search_box.setFocus)
self.about_action.triggered.connect(self.about_panel.show) self.about_action.triggered.connect(self.about_panel.show)
self.add_account_action.triggered.connect(self.add_account_dialog.open_for_add)
self.mute_action.triggered.connect(self._SH_MuteButtonClicked) self.mute_action.triggered.connect(self._SH_MuteButtonClicked)
self.redial_action.triggered.connect(self._SH_RedialActionTriggered) self.redial_action.triggered.connect(self._SH_RedialActionTriggered)
self.silent_action.triggered.connect(self._SH_SilentButtonClicked) self.silent_action.triggered.connect(self._SH_SilentButtonClicked)
...@@ -168,6 +170,7 @@ class MainWindow(base_class, ui_class): ...@@ -168,6 +170,7 @@ class MainWindow(base_class, ui_class):
def closeEvent(self, event): def closeEvent(self, event):
super(MainWindow, self).closeEvent(event) super(MainWindow, self).closeEvent(event)
self.about_panel.close() self.about_panel.close()
self.add_account_dialog.close()
self.contact_editor_dialog.close() self.contact_editor_dialog.close()
def set_user_icon(self, image_file_name): def set_user_icon(self, image_file_name):
......
# Copyright (c) 2010 AG Projects. See LICENSE for details.
#
from __future__ import with_statement
__all__ = ['BackgroundFrame']
from PyQt4.QtCore import Qt, QEvent, QPoint, QRect, QSize
from PyQt4.QtGui import QColor, QFrame, QPainter, QPixmap
from blink.resources import Resources
from blink.widgets.util import QtDynamicProperty
class BackgroundFrame(QFrame):
backgroundColor = QtDynamicProperty('backgroundColor', unicode)
backgroundImage = QtDynamicProperty('backgroundImage', unicode)
imageGeometry = QtDynamicProperty('imageGeometry', QRect)
def __init__(self, parent=None):
super(BackgroundFrame, self).__init__(parent)
self.backgroundColor = None
self.backgroundImage = None
self.imageGeometry = None
self.pixmap = None
self.scaled_pixmap = None
@property
def image_position(self):
return QPoint(0, 0) if self.imageGeometry is None else self.imageGeometry.topLeft()
@property
def image_size(self):
if self.imageGeometry is not None:
size = self.imageGeometry.size().expandedTo(QSize(0, 0)) # requested size with negative values turned to 0
if size.isNull():
return size if self.pixmap is None else self.pixmap.size()
elif size.width() == 0:
return size.expandedTo(QSize(16777215, 0))
elif size.height() == 0:
return size.expandedTo(QSize(0, 16777215))
else:
return size
elif self.pixmap:
return self.pixmap.size()
else:
return QSize(0, 0)
def event(self, event):
if event.type() == QEvent.DynamicPropertyChange:
if event.propertyName() == 'backgroundImage':
self.pixmap = QPixmap()
if self.backgroundImage and self.pixmap.load(Resources.get(self.backgroundImage)):
self.scaled_pixmap = self.pixmap.scaled(self.image_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
else:
self.pixmap = self.scaled_pixmap = None
self.update()
elif event.propertyName() == 'imageGeometry' and self.pixmap:
self.scaled_pixmap = self.pixmap.scaled(self.image_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.update()
elif event.propertyName() == 'backgroundColor':
self.update()
return super(BackgroundFrame, self).event(event)
def resizeEvent(self, event):
self.scaled_pixmap = self.pixmap and self.pixmap.scaled(self.image_size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
def paintEvent(self, event):
super(BackgroundFrame, self).paintEvent(event)
painter = QPainter(self)
if self.backgroundColor:
painter.fillRect(self.rect(), QColor(self.backgroundColor))
if self.scaled_pixmap is not None:
painter.drawPixmap(self.image_position, self.scaled_pixmap)
painter.end()
# Copyright (c) 2010 AG Projects. See LICENSE for details. # Copyright (c) 2010 AG Projects. See LICENSE for details.
# #
__all__ = ['LineEdit', 'ValidatingLineEdit']
import re
from PyQt4.QtCore import Qt, QEvent, pyqtSignal from PyQt4.QtCore import Qt, QEvent, pyqtSignal
from PyQt4.QtGui import QLineEdit, QBoxLayout, QHBoxLayout, QLayout, QPainter, QPalette, QSpacerItem, QSizePolicy, QStyle, QWidget, QStyleOptionFrameV2 from PyQt4.QtGui import QLineEdit, QBoxLayout, QHBoxLayout, QLabel, QLayout, QPainter, QPalette, QPixmap, QSpacerItem, QSizePolicy, QStyle, QWidget, QStyleOptionFrameV2
from blink.resources import Resources
from blink.widgets.util import QtDynamicProperty from blink.widgets.util import QtDynamicProperty
...@@ -117,3 +122,46 @@ class LineEdit(QLineEdit): ...@@ -117,3 +122,46 @@ class LineEdit(QLineEdit):
widget.hide() widget.hide()
class ValidatingLineEdit(LineEdit):
statusChanged = pyqtSignal()
def __init__(self, parent=None):
super(ValidatingLineEdit, self).__init__(parent)
self.invalid_entry_label = QLabel(self)
self.invalid_entry_label.setFixedSize(18, 16)
self.invalid_entry_label.setPixmap(QPixmap(Resources.get('icons/invalid16.png')))
self.invalid_entry_label.setScaledContents(False)
self.invalid_entry_label.setAlignment(Qt.AlignCenter)
self.invalid_entry_label.setObjectName('invalid_entry_label')
self.invalid_entry_label.hide()
self.addTailWidget(self.invalid_entry_label)
option = QStyleOptionFrameV2()
self.initStyleOption(option)
frame_width = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth, option, self)
self.setMinimumHeight(self.invalid_entry_label.minimumHeight() + 2 + 2*frame_width)
self.textChanged.connect(self.text_changed)
self.valid = True
self.regexp = re.compile(r'.*')
def _get_regexp(self):
return self.__dict__['regexp']
def _set_regexp(self, regexp):
self.__dict__['regexp'] = regexp
valid = regexp.search(unicode(self.text())) is not None
if self.valid != valid:
self.invalid_entry_label.setVisible(not valid)
self.valid = valid
self.statusChanged.emit()
regexp = property(_get_regexp, _set_regexp)
del _get_regexp, _set_regexp
def text_changed(self, text):
valid = self.regexp.search(unicode(text)) is not None
if self.valid != valid:
self.invalid_entry_label.setVisible(not valid)
self.valid = valid
self.statusChanged.emit()
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>531</width>
<height>459</height>
</rect>
</property>
<property name="windowTitle">
<string>Add account</string>
</property>
<layout class="QVBoxLayout" name="dialog_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>15</number>
</property>
<item>
<widget class="BackgroundFrame" name="background_frame">
<property name="minimumSize">
<size>
<width>522</width>
<height>400</height>
</size>
</property>
<property name="styleSheet">
<string>QFrame#background_frame {
background: url(resources/icons/blink.png);
background-repeat: no-repeat;
background-position: center left;
}
</string>
</property>
<property name="backgroundColor" stdset="0">
<string/>
</property>
<property name="backgroundImage" stdset="0">
<string>icons/blink.png</string>
</property>
<property name="imageGeometry" stdset="0">
<rect>
<x>-18</x>
<y>10</y>
<width>476</width>
<height>354</height>
</rect>
</property>
<layout class="QHBoxLayout" name="background_frame_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<spacer name="background_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>100</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QFrame" name="input_frame">
<property name="styleSheet">
<string>QFrame#input_frame {
border: 2px;
border-radius: 4px;
border-style: solid;
border-color: #545454;
background-color: rgba(244, 244, 244, 228); /* 244, 244, 244, 228 or 248, 248, 248, 224 */
}
</string>
</property>
<layout class="QGridLayout" name="input_frame_layout">
<property name="leftMargin">
<number>7</number>
</property>
<property name="rightMargin">
<number>22</number>
</property>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="title_label">
<property name="font">
<font>
<family>Sans Serif</family>
<pointsize>12</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Add account</string>
</property>
</widget>
</item>
<item row="1" column="0" rowspan="7">
<spacer name="indent_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>15</width>
<height>48</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="1">
<spacer name="title_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item row="2" column="1">
<widget class="QLabel" name="info_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Select whether you want to add a SIP account you already have or create a new one and then fill in the requested information.</string>
</property>
<property name="alignment">
<set>Qt::AlignJustify|Qt::AlignVCenter</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<spacer name="info_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="1">
<widget class="QRadioButton" name="add_account_button">
<property name="text">
<string>Add an existing SIP account</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QRadioButton" name="create_account_button">
<property name="text">
<string>Create a free SIP account</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QStackedWidget" name="panel_view">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="add_account_panel">
<layout class="QGridLayout" name="add_panel_layout">
<property name="margin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="display_name_label">
<property name="text">
<string>Display name:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ValidatingLineEdit" name="display_name_editor">
<property name="minimumSize">
<size>
<width>0</width>
<height>22</height>
</size>
</property>
<property name="widgetSpacing" stdset="0">
<number>0</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="sip_address_label">
<property name="text">
<string>SIP address:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="password_label">
<property name="text">
<string>Password:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="ValidatingLineEdit" name="sip_address_editor">
<property name="minimumSize">
<size>
<width>0</width>
<height>22</height>
</size>
</property>
<property name="inactiveText" stdset="0">
<string/>
</property>
<property name="widgetSpacing" stdset="0">
<number>0</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="ValidatingLineEdit" name="password_editor">
<property name="minimumSize">
<size>
<width>0</width>
<height>22</height>
</size>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="widgetSpacing" stdset="0">
<number>0</number>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<spacer name="panel_vertical_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="4" column="0" colspan="2">
<widget class="StatusLabel" name="add_status_label">
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="create_account_panel">
<layout class="QGridLayout" name="create_panel_layout">
<property name="margin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="name_label">
<property name="text">
<string>Your name:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="ValidatingLineEdit" name="name_editor">
<property name="minimumSize">
<size>
<width>0</width>
<height>22</height>
</size>
</property>
<property name="widgetSpacing" stdset="0">
<number>0</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="username_label">
<property name="text">
<string>Choose a username:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="new_password_label">
<property name="text">
<string>Choose a password:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="verify_password_label">
<property name="text">
<string>Verify password:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="email_address_label">
<property name="text">
<string>E-mail address:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="ValidatingLineEdit" name="username_editor">
<property name="minimumSize">
<size>
<width>0</width>
<height>22</height>
</size>
</property>
<property name="widgetSpacing" stdset="0">
<number>0</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="ValidatingLineEdit" name="new_password_editor">
<property name="minimumSize">
<size>
<width>0</width>
<height>22</height>
</size>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="widgetSpacing" stdset="0">
<number>0</number>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="ValidatingLineEdit" name="verify_password_editor">
<property name="minimumSize">
<size>
<width>0</width>
<height>22</height>
</size>
</property>
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="widgetSpacing" stdset="0">
<number>0</number>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="ValidatingLineEdit" name="email_address_editor">
<property name="minimumSize">
<size>
<width>0</width>
<height>22</height>
</size>
</property>
<property name="widgetSpacing" stdset="0">
<number>0</number>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="email_note_label">
<property name="text">
<string>The E-mail address is used when sending voicemail messages, missed call notifications and to recover a lost password.</string>
</property>
<property name="alignment">
<set>Qt::AlignJustify|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="StatusLabel" name="create_status_label">
<property name="text">
<string/>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="6" column="1">
<spacer name="panel_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>10</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="grid_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>10</width>
<height>15</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="button_box_layout">
<property name="spacing">
<number>6</number>
</property>
<item>
<spacer name="button_box_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="reject_button">
<property name="minimumSize">
<size>
<width>85</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="accept_button">
<property name="minimumSize">
<size>
<width>85</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Add</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BackgroundFrame</class>
<extends>QFrame</extends>
<header>blink.widgets.frames</header>
<container>1</container>
</customwidget>
<customwidget>
<class>ValidatingLineEdit</class>
<extends>QLineEdit</extends>
<header>blink.widgets.lineedit</header>
</customwidget>
<customwidget>
<class>StatusLabel</class>
<extends>QLabel</extends>
<header>blink.widgets.labels</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>add_account_button</tabstop>
<tabstop>create_account_button</tabstop>
<tabstop>display_name_editor</tabstop>
<tabstop>sip_address_editor</tabstop>
<tabstop>password_editor</tabstop>
<tabstop>name_editor</tabstop>
<tabstop>username_editor</tabstop>
<tabstop>new_password_editor</tabstop>
<tabstop>verify_password_editor</tabstop>
<tabstop>email_address_editor</tabstop>
<tabstop>accept_button</tabstop>
<tabstop>reject_button</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>reject_button</sender>
<signal>clicked()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>413</x>
<y>429</y>
</hint>
<hint type="destinationlabel">
<x>73</x>
<y>439</y>
</hint>
</hints>
</connection>
</connections>
</ui>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment