Commit 9c840d9b authored by Saul Ibarra's avatar Saul Ibarra

Added initial presence support

parent 8272c5a2
...@@ -42,4 +42,12 @@ Traceback (most recent call last): ...@@ -42,4 +42,12 @@ Traceback (most recent call last):
File "/home/dan/work/voip/blink-qt/sipsimple/bonjour.py", line 1125, in DNSServiceRegister File "/home/dan/work/voip/blink-qt/sipsimple/bonjour.py", line 1125, in DNSServiceRegister
TypeError: an integer is required TypeError: an integer is required
Presence
--------
- Is picking the most recent timestamp a good winning method?
- Calculate user idleness
- Add a GUI element for the offline note
- Delete own icon if we don't get anything back from XCAP?
- Unify settings for inbound and outbound presence
...@@ -41,6 +41,7 @@ from blink.configuration.datatypes import InvalidToken ...@@ -41,6 +41,7 @@ from blink.configuration.datatypes import InvalidToken
from blink.configuration.settings import SIPSimpleSettingsExtension from blink.configuration.settings import SIPSimpleSettingsExtension
from blink.logging import LogManager from blink.logging import LogManager
from blink.mainwindow import MainWindow from blink.mainwindow import MainWindow
from blink.presence import PresenceManager
from blink.resources import ApplicationData from blink.resources import ApplicationData
from blink.sessions import SessionManager from blink.sessions import SessionManager
from blink.update import UpdateManager from blink.update import UpdateManager
...@@ -96,6 +97,7 @@ class Blink(QApplication): ...@@ -96,6 +97,7 @@ class Blink(QApplication):
self.main_window = MainWindow() self.main_window = MainWindow()
self.ip_address_monitor = IPAddressMonitor() self.ip_address_monitor = IPAddressMonitor()
self.log_manager = LogManager() self.log_manager = LogManager()
self.presence_manager = PresenceManager()
self.update_manager = UpdateManager() self.update_manager = UpdateManager()
self.main_window.check_for_updates_action.triggered.connect(self.update_manager.check_for_updates) self.main_window.check_for_updates_action.triggered.connect(self.update_manager.check_for_updates)
...@@ -212,6 +214,7 @@ class Blink(QApplication): ...@@ -212,6 +214,7 @@ class Blink(QApplication):
def _NH_SIPApplicationWillStart(self, notification): def _NH_SIPApplicationWillStart(self, notification):
self.log_manager.start() self.log_manager.start()
self.presence_manager.start()
@run_in_gui_thread @run_in_gui_thread
def _NH_SIPApplicationDidStart(self, notification): def _NH_SIPApplicationDidStart(self, notification):
...@@ -229,6 +232,9 @@ class Blink(QApplication): ...@@ -229,6 +232,9 @@ class Blink(QApplication):
def _NH_SIPApplicationWillEnd(self, notification): def _NH_SIPApplicationWillEnd(self, notification):
self.ip_address_monitor.stop() self.ip_address_monitor.stop()
def _NH_SIPApplicationDidEnd(self, notification):
self.presence_manager.stop()
def _initialize_sipsimple(self): def _initialize_sipsimple(self):
if not os.path.exists(ApplicationData.get('config')): if not os.path.exists(ApplicationData.get('config')):
self.first_run = True self.first_run = True
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
__all__ = ['AccountExtension', 'BonjourAccountExtension'] __all__ = ['AccountExtension', 'BonjourAccountExtension']
from sipsimple.account import BonjourMSRPSettings, MessageSummarySettings, MSRPSettings, RTPSettings, SIPSettings, TLSSettings, XCAPSettings from sipsimple.account import BonjourMSRPSettings, MessageSummarySettings, MSRPSettings, PresenceSettings, RTPSettings, SIPSettings, TLSSettings, XCAPSettings
from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension from sipsimple.configuration import Setting, SettingsGroup, SettingsObjectExtension
from sipsimple.configuration.datatypes import AudioCodecList, Hostname, MSRPConnectionModel, MSRPTransport, NonNegativeInteger, SIPTransportList, SRTPEncryption from sipsimple.configuration.datatypes import AudioCodecList, Hostname, MSRPConnectionModel, MSRPTransport, NonNegativeInteger, SIPTransportList, SRTPEncryption
from sipsimple.util import user_info from sipsimple.util import user_info
...@@ -29,6 +29,10 @@ class MSRPSettingsExtension(MSRPSettings): ...@@ -29,6 +29,10 @@ class MSRPSettingsExtension(MSRPSettings):
connection_model = Setting(type=MSRPConnectionModel, default='relay') connection_model = Setting(type=MSRPConnectionModel, default='relay')
class PresenceSettingsExtension(PresenceSettings):
enabled = Setting(type=bool, default=True)
class PSTNSettings(SettingsGroup): class PSTNSettings(SettingsGroup):
idd_prefix = Setting(type=unicode, default=None, nillable=True) idd_prefix = Setting(type=unicode, default=None, nillable=True)
prefix = Setting(type=unicode, default=None, nillable=True) prefix = Setting(type=unicode, default=None, nillable=True)
...@@ -71,6 +75,7 @@ class AccountExtension(SettingsObjectExtension): ...@@ -71,6 +75,7 @@ class AccountExtension(SettingsObjectExtension):
message_summary = MessageSummarySettingsExtension message_summary = MessageSummarySettingsExtension
msrp = MSRPSettingsExtension msrp = MSRPSettingsExtension
pstn = PSTNSettings pstn = PSTNSettings
presence = PresenceSettingsExtension
rtp = RTPSettingsExtension rtp = RTPSettingsExtension
server = ServerSettings server = ServerSettings
sip = SIPSettingsExtension sip = SIPSettingsExtension
......
...@@ -94,6 +94,7 @@ class SIPSimpleSettingsExtension(SettingsObjectExtension): ...@@ -94,6 +94,7 @@ class SIPSimpleSettingsExtension(SettingsObjectExtension):
class BlinkPresenceSettings(SettingsGroup): class BlinkPresenceSettings(SettingsGroup):
current_state = Setting(type=PresenceState, default=PresenceState('Available')) current_state = Setting(type=PresenceState, default=PresenceState('Available'))
state_history = Setting(type=PresenceStateList, default=PresenceStateList()) state_history = Setting(type=PresenceStateList, default=PresenceStateList())
offline_note = Setting(type=unicode, nillable=True)
icon = Setting(type=IconDescriptor, nillable=True) icon = Setting(type=IconDescriptor, nillable=True)
......
...@@ -790,7 +790,8 @@ class Contact(object): ...@@ -790,7 +790,8 @@ class Contact(object):
def __init__(self, contact, group): def __init__(self, contact, group):
self.settings = contact self.settings = contact
self.group = group self.group = group
self.status = 'unknown' self.state = 'unknown'
self.note = None
notification_center = NotificationCenter() notification_center = NotificationCenter()
notification_center.add_observer(ObserverWeakrefProxy(self), sender=contact) notification_center.add_observer(ObserverWeakrefProxy(self), sender=contact)
...@@ -818,7 +819,7 @@ class Contact(object): ...@@ -818,7 +819,7 @@ class Contact(object):
return '%s(%r, %r)' % (self.__class__.__name__, self.settings, self.group) return '%s(%r, %r)' % (self.__class__.__name__, self.settings, self.group)
def __getstate__(self): def __getstate__(self):
return (self.settings.id, dict(group=self.group, status=self.status)) return (self.settings.id, dict(group=self.group, state=self.state))
def __setstate__(self, state): def __setstate__(self, state):
contact_id, state = state contact_id, state = state
...@@ -851,7 +852,7 @@ class Contact(object): ...@@ -851,7 +852,7 @@ class Contact(object):
@property @property
def info(self): def info(self):
return self.uri return self.note or self.uri
@property @property
def uri(self): def uri(self):
...@@ -909,6 +910,19 @@ class Contact(object): ...@@ -909,6 +910,19 @@ class Contact(object):
self.__dict__.pop('pixmap', None) self.__dict__.pop('pixmap', None)
notification.center.post_notification('BlinkContactDidChange', sender=self) notification.center.post_notification('BlinkContactDidChange', sender=self)
def _NH_AddressbookContactGotPresenceUpdate(self, notification):
if notification.data.state in ('available', 'away', 'busy', 'offline'):
self.__dict__['state'] = notification.data.state
else:
self.__dict__['state'] = 'unknown'
self.note = notification.data.note
if notification.data.icon_data:
icon = IconManager().store_data(self.settings.id, notification.data.icon_data)
if icon:
self.settings.icon = notification.data.icon_descriptor
self.settings.save()
notification.center.post_notification('BlinkContactDidChange', sender=self)
ui_class, base_class = uic.loadUiType(Resources.get('google_contacts_dialog.ui')) ui_class, base_class = uic.loadUiType(Resources.get('google_contacts_dialog.ui'))
...@@ -1293,9 +1307,9 @@ class ContactDelegate(QStyledItemDelegate): ...@@ -1293,9 +1307,9 @@ class ContactDelegate(QStyledItemDelegate):
widget.render(pixmap) widget.render(pixmap)
painter.drawPixmap(option.rect, pixmap) painter.drawPixmap(option.rect, pixmap)
if contact.status not in ('offline', 'unknown'): if contact.state not in ('offline', 'unknown'):
status_colors = dict(available='#00ff00', away='#ffff00', busy='#ff0000') status_colors = dict(available='#00ff00', away='#ffff00', busy='#ff0000')
color = QColor(status_colors[contact.status]) color = QColor(status_colors[contact.state])
painter.setRenderHint(QPainter.Antialiasing, True) painter.setRenderHint(QPainter.Antialiasing, True)
painter.setBrush(color) painter.setBrush(color)
painter.setPen(color.darker(200)) painter.setPen(color.darker(200))
...@@ -2708,11 +2722,16 @@ class ContactEditorDialog(base_class, ui_class): ...@@ -2708,11 +2722,16 @@ class ContactEditorDialog(base_class, ui_class):
self.display_name_editor.setText(contact.name) self.display_name_editor.setText(contact.name)
if contact.settings.icon is not None and contact.settings.icon.is_local: if contact.settings.icon is not None and contact.settings.icon.is_local:
self.icon_selector.filename = contact.settings.icon.url[len('file://'):] self.icon_selector.filename = contact.settings.icon.url[len('file://'):]
elif contact.settings.icon:
icon = IconManager().get(contact.settings.id)
if icon:
self.icon_selector.setPixmap(icon.pixmap(32))
else: else:
self.icon_selector.filename = None self.icon_selector.filename = None
self.preferred_media.setCurrentIndex(self.preferred_media.findText(contact.settings.preferred_media.title())) self.preferred_media.setCurrentIndex(self.preferred_media.findText(contact.settings.preferred_media.title()))
self.accept_button.setText(u'Ok') self.accept_button.setText(u'Ok')
self.accept_button.setEnabled(True) self.accept_button.setEnabled(True)
self.subscribe_presence.setChecked(contact.settings.presence.subscribe)
self.show() self.show()
def reset_icon(self): def reset_icon(self):
...@@ -2736,6 +2755,12 @@ class ContactEditorDialog(base_class, ui_class): ...@@ -2736,6 +2755,12 @@ class ContactEditorDialog(base_class, ui_class):
uri.uri = self.sip_address_editor.text() uri.uri = self.sip_address_editor.text()
contact.name = self.display_name_editor.text() contact.name = self.display_name_editor.text()
contact.preferred_media = self.preferred_media.currentText().lower() contact.preferred_media = self.preferred_media.currentText().lower()
if self.subscribe_presence.isChecked():
contact.presence.policy = 'allow'
contact.presence.subscribe = True
else:
contact.presence.policy = 'default'
contact.presence.subscribe = False
if self.icon_selector.filename is not None: if self.icon_selector.filename is not None:
icon_file = ApplicationData.get(self.icon_selector.filename) icon_file = ApplicationData.get(self.icon_selector.filename)
icon_descriptor = IconDescriptor('file://' + icon_file) icon_descriptor = IconDescriptor('file://' + icon_file)
......
...@@ -29,6 +29,7 @@ from blink.preferences import PreferencesWindow ...@@ -29,6 +29,7 @@ from blink.preferences import PreferencesWindow
from blink.sessions import ConferenceDialog, SessionManager, SessionModel from blink.sessions import ConferenceDialog, SessionManager, SessionModel
from blink.configuration.datatypes import IconDescriptor, InvalidToken, PresenceState from blink.configuration.datatypes import IconDescriptor, InvalidToken, PresenceState
from blink.configuration.settings import BlinkSettings from blink.configuration.settings import BlinkSettings
from blink.presence import PendingWatcherDialog
from blink.resources import IconManager, Resources from blink.resources import IconManager, Resources
from blink.util import run_in_gui_thread from blink.util import run_in_gui_thread
from blink.widgets.buttons import AccountState, SwitchViewButton from blink.widgets.buttons import AccountState, SwitchViewButton
...@@ -47,8 +48,11 @@ class MainWindow(base_class, ui_class): ...@@ -47,8 +48,11 @@ class MainWindow(base_class, ui_class):
notification_center.add_observer(self, name='SIPApplicationWillStart') notification_center.add_observer(self, name='SIPApplicationWillStart')
notification_center.add_observer(self, name='SIPApplicationDidStart') notification_center.add_observer(self, name='SIPApplicationDidStart')
notification_center.add_observer(self, name='SIPAccountGotMessageSummary') notification_center.add_observer(self, name='SIPAccountGotMessageSummary')
notification_center.add_observer(self, name='SIPAccountGotPendingWatcher')
notification_center.add_observer(self, sender=AccountManager()) notification_center.add_observer(self, sender=AccountManager())
self.pending_watcher_dialogs = []
self.mwi_icons = [QIcon(Resources.get('icons/mwi-%d.png' % i)) for i in xrange(0, 11)] self.mwi_icons = [QIcon(Resources.get('icons/mwi-%d.png' % i)) for i in xrange(0, 11)]
self.mwi_icons.append(QIcon(Resources.get('icons/mwi-many.png'))) self.mwi_icons.append(QIcon(Resources.get('icons/mwi-many.png')))
...@@ -58,7 +62,7 @@ class MainWindow(base_class, ui_class): ...@@ -58,7 +62,7 @@ class MainWindow(base_class, ui_class):
self.setWindowTitle('Blink') self.setWindowTitle('Blink')
self.setWindowIconText('Blink') self.setWindowIconText('Blink')
self.default_icon_path = Resources.get('icons/avatar.jpg') self.default_icon_path = Resources.get('icons/default-avatar.png')
self.default_icon = QIcon(self.default_icon_path) self.default_icon = QIcon(self.default_icon_path)
self.last_icon_directory = os.path.expanduser('~') self.last_icon_directory = os.path.expanduser('~')
self.set_user_icon(IconManager().get('myicon')) self.set_user_icon(IconManager().get('myicon'))
...@@ -212,6 +216,8 @@ class MainWindow(base_class, ui_class): ...@@ -212,6 +216,8 @@ class MainWindow(base_class, ui_class):
self.google_contacts_dialog.close() self.google_contacts_dialog.close()
self.preferences_window.close() self.preferences_window.close()
self.server_tools_window.close() self.server_tools_window.close()
for dialog in self.pending_watcher_dialogs[:]:
dialog.close()
def set_user_icon(self, icon): def set_user_icon(self, icon):
self.account_state.setIcon(icon or self.default_icon) self.account_state.setIcon(icon or self.default_icon)
...@@ -590,6 +596,9 @@ class MainWindow(base_class, ui_class): ...@@ -590,6 +596,9 @@ class MainWindow(base_class, ui_class):
action = self.received_calls_menu.addAction(unicode(entry)) action = self.received_calls_menu.addAction(unicode(entry))
action.entry = entry action.entry = entry
def _SH_PendingWatcherDialogFinished(self, dialog, code):
self.pending_watcher_dialogs.remove(dialog)
@run_in_gui_thread @run_in_gui_thread
def handle_notification(self, notification): def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null) handler = getattr(self, '_NH_%s' % notification.name, Null)
...@@ -646,6 +655,7 @@ class MainWindow(base_class, ui_class): ...@@ -646,6 +655,7 @@ class MainWindow(base_class, ui_class):
def _NH_CFGSettingsObjectDidChange(self, notification): def _NH_CFGSettingsObjectDidChange(self, notification):
settings = SIPSimpleSettings() settings = SIPSimpleSettings()
blink_settings = BlinkSettings()
if notification.sender is settings: if notification.sender is settings:
if 'audio.silent' in notification.data.modified: if 'audio.silent' in notification.data.modified:
self.silent_action.setChecked(settings.audio.silent) self.silent_action.setChecked(settings.audio.silent)
...@@ -673,6 +683,15 @@ class MainWindow(base_class, ui_class): ...@@ -673,6 +683,15 @@ class MainWindow(base_class, ui_class):
self.google_contacts_action.setText(u'Disable Google Contacts') self.google_contacts_action.setText(u'Disable Google Contacts')
if authorization_token is InvalidToken: if authorization_token is InvalidToken:
self.google_contacts_dialog.open_for_incorrect_password() self.google_contacts_dialog.open_for_incorrect_password()
elif notification.sender is blink_settings:
if 'presence.current_state' in notification.data.modified:
state = getattr(AccountState, blink_settings.presence.current_state.state, AccountState.Available)
self.account_state.setState(state, blink_settings.presence.current_state.note)
if 'presence.icon' in notification.data.modified:
self.set_user_icon(IconManager().get('myicon'))
if 'presence.offline_note' in notification.data.modified:
# TODO: set offline note -Saul
pass
elif isinstance(notification.sender, (Account, BonjourAccount)): elif isinstance(notification.sender, (Account, BonjourAccount)):
account_manager = AccountManager() account_manager = AccountManager()
account = notification.sender account = notification.sender
...@@ -730,6 +749,13 @@ class MainWindow(base_class, ui_class): ...@@ -730,6 +749,13 @@ class MainWindow(base_class, ui_class):
new_messages = 0 new_messages = 0
action.setIcon(self.mwi_icons[new_messages]) action.setIcon(self.mwi_icons[new_messages])
def _NH_SIPAccountGotPendingWatcher(self, notification):
dialog = PendingWatcherDialog(notification.sender, notification.data.uri, notification.data.display_name)
dialog.finished.connect(partial(self._SH_PendingWatcherDialogFinished, dialog))
self.pending_watcher_dialogs.append(dialog)
dialog.show()
del ui_class, base_class del ui_class, base_class
# Copyright (C) 2013 AG Projects. See LICENSE for details.
#
__all__ = ['PresenceManager', 'PendingWatcherDialog']
import base64
import hashlib
import re
import socket
import uuid
from PyQt4 import uic
from PyQt4.QtCore import Qt, QTimer
from application.notification import IObserver, NotificationCenter, NotificationData
from application.python import Null, limit
from datetime import datetime
from eventlib.green import urllib2
from itertools import chain
from twisted.internet import reactor
from zope.interface import implements
from sipsimple import addressbook
from sipsimple.account import AccountManager, BonjourAccount
from sipsimple.account.xcap import Icon, OfflineStatus
from sipsimple.configuration.settings import SIPSimpleSettings
from sipsimple.payloads import caps, cipid, pidf, prescontent, rpid
from sipsimple.threading import run_in_twisted_thread
from sipsimple.threading.green import run_in_green_thread
from sipsimple.util import ISOTimestamp
from blink.configuration.datatypes import IconDescriptor, PresenceState
from blink.configuration.settings import BlinkSettings
from blink.resources import IconManager, Resources
from blink.util import run_in_gui_thread
epoch = datetime.fromtimestamp(0)
sip_prefix_re = re.compile("^sips?:")
unknown_icon = "blink://unknown"
class PresencePublicationHandler(object):
implements(IObserver)
def start(self):
notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPAccountWillActivate')
notification_center.add_observer(self, name='SIPAccountWillDeactivate')
notification_center.add_observer(self, name='SystemDidWakeUpFromSleep')
notification_center.add_observer(self, name='XCAPManagerDidDiscoverServerCapabilities')
notification_center.add_observer(self, name='XCAPManagerDidReloadData')
notification_center.add_observer(self, sender=BlinkSettings(), name='CFGSettingsObjectDidChange')
try:
self.hostname = socket.gethostname()
except Exception:
self.hostname = 'localhost'
def stop(self):
notification_center = NotificationCenter()
notification_center.remove_observer(self, name='SIPAccountWillActivate')
notification_center.remove_observer(self, name='SIPAccountWillDeactivate')
notification_center.remove_observer(self, name='SystemDidWakeUpFromSleep')
notification_center.remove_observer(self, name='XCAPManagerDidDiscoverServerCapabilities')
notification_center.remove_observer(self, name='XCAPManagerDidReloadData')
notification_center.remove_observer(self, sender=BlinkSettings(), name='CFGSettingsObjectDidChange')
def publish(self, account=None):
if not account:
account_manager = AccountManager()
bonjour_account = BonjourAccount()
accounts = [account for account in account_manager.get_accounts() if account is not bonjour_account and account.enabled and account.presence.enabled]
else:
accounts = [account]
for account in accounts:
account.presence_state = self.build_pidf(account)
def build_pidf(self, account):
blink_settings = BlinkSettings()
presence_state = blink_settings.presence.current_state.state
presence_note = blink_settings.presence.current_state.note
if presence_state == 'Invisible':
# Publish an empty offline state so that other clients are also synced
return self.build_offline_pidf(account, False)
doc = pidf.PIDF(str(account.uri))
timestamp = ISOTimestamp.now()
person = pidf.Person('PID-%s' % hashlib.md5(account.id).hexdigest())
person.timestamp = pidf.PersonTimestamp(timestamp)
doc.add(person)
status = pidf.Status(basic='open')
status.extended = presence_state.lower()
person.activities = rpid.Activities()
person.activities.add(unicode(status.extended))
settings = SIPSimpleSettings()
instance_id = str(uuid.UUID(settings.instance_id))
service = pidf.Service('SID-%s' % instance_id, status=status)
if presence_note:
service.notes.add(presence_note)
service.timestamp = pidf.ServiceTimestamp(timestamp)
service.contact = pidf.Contact(str(account.contact.public_gruu or account.uri))
if account.display_name:
service.display_name = cipid.DisplayName(account.display_name)
if account.icon:
service.icon = cipid.Icon("%s#blink-icon%s" % (account.icon.url, account.icon.etag))
else:
service.icon = cipid.Icon(unknown_icon)
service.device_info = pidf.DeviceInfo(instance_id, description=self.hostname, user_agent=settings.user_agent)
service.device_info.time_offset = pidf.TimeOffset()
service.capabilities = caps.ServiceCapabilities(audio=True, text=False)
service.capabilities.message = False
service.capabilities.file_transfer = False
service.capabilities.screen_sharing_server = False
service.capabilities.screen_sharing_client = False
# TODO: Add real user input data -Saul
service.user_input = rpid.UserInput()
service.user_input.idle_threshold = 600
service.add(pidf.DeviceID(instance_id))
doc.add(service)
device = pidf.Device('DID-%s' % instance_id, device_id=pidf.DeviceID(instance_id))
device.timestamp = pidf.DeviceTimestamp(timestamp)
device.notes.add(u'%s at %s' % (settings.user_agent, self.hostname))
doc.add(device)
return doc
def build_offline_pidf(self, account, offline_note=None):
doc = pidf.PIDF(str(account.uri))
timestamp = ISOTimestamp.now()
account_hash = hashlib.md5(account.id).hexdigest()
person = pidf.Person('PID-%s' % account_hash)
person.timestamp = pidf.PersonTimestamp(timestamp)
doc.add(person)
person.activities = rpid.Activities()
person.activities.add(u'offline')
service = pidf.Service('SID-%s' % account_hash)
service.status = pidf.Status(basic='closed')
service.status.extended = u'offline'
service.contact = pidf.Contact(str(account.uri))
service.capabilities = caps.ServiceCapabilities()
service.timestamp = pidf.ServiceTimestamp(timestamp)
if offline_note:
service.notes.add(offline_note)
doc.add(service)
return doc
def set_xcap_offline_note(self, account=None):
settings = BlinkSettings()
if not account:
account_manager = AccountManager()
accounts = [account for account in account_manager.get_accounts() if hasattr(account, 'xcap') and account.xcap.discovered]
else:
accounts = [account]
for account in accounts:
if settings.presence.offline_note:
account.xcap_manager.set_offline_status(OfflineStatus(self.build_offline_pidf(account, settings.presence.offline_note)))
else:
account.xcap_manager.set_offline_status(None)
def set_xcap_icon(self, account=None):
settings = BlinkSettings()
if not account:
account_manager = AccountManager()
accounts = [account for account in account_manager.get_accounts() if hasattr(account, 'xcap') and account.xcap.discovered]
else:
accounts = [account]
icon = None
if settings.presence.icon:
try:
data = open(settings.presence.icon.url[7:], 'r').read() # strip 'file://'
except Exception:
pass
else:
icon = Icon(data, 'image/png')
for account in accounts:
account.xcap_manager.set_status_icon(icon)
@run_in_gui_thread
def _save_icon(self, icon_data, icon_hash):
settings = BlinkSettings()
if None not in (icon_data, icon_hash):
icon = IconManager().store_data('myicon', icon_data)
if icon:
settings.presence.icon = IconDescriptor('file://'+icon.filename, icon_hash)
else:
settings.presence.icon = None
settings.save()
@run_in_twisted_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_CFGSettingsObjectDidChange(self, notification):
settings = BlinkSettings()
if notification.sender is settings:
if 'presence.offline_note' in notification.data.modified:
self.set_xcap_offline_note()
if 'presence.icon' in notification.data.modified:
self.set_xcap_icon()
if 'presence.current_state' in notification.data.modified:
self.publish()
else:
account = notification.sender
if 'presence.enabled' in notification.data.modified:
if account.presence.enabled:
self.publish(account)
# The account itself will unpublish when presence is disabled
return
if set(['xcap.enabled', 'xcap.xcap_root']).intersection(notification.data.modified):
account.icon = None
if set(['display_name', 'xcap.enabled', 'xcap.discovered']).intersection(notification.data.modified):
if 'xcap.discovered' in notification.data.modified and account.xcap.enabled and account.xcap.discovered:
# TODO: group these in a transaction? Needs to be done in the file-io thread -Saul
self.set_xcap_offline_note(account)
self.set_xcap_icon(account)
self.publish(account)
def _NH_SIPAccountWillActivate(self, notification):
if notification.sender is not BonjourAccount():
account = notification.sender
notification.center.add_observer(self, sender=account, name='CFGSettingsObjectDidChange')
notification.center.add_observer(self, sender=account, name='SIPAccountGotSelfPresenceState')
account.icon = None
def _NH_SIPAccountWillDeactivate(self, notification):
if notification.sender is not BonjourAccount():
account = notification.sender
notification.center.remove_observer(self, sender=account, name='CFGSettingsObjectDidChange')
notification.center.remove_observer(self, sender=account, name='SIPAccountGotSelfPresenceState')
account.icon = None
def _NH_SIPAccountGotSelfPresenceState(self, notification):
pidf_doc = notification.data.pidf
services = [service for service in pidf_doc.services if service.status.extended is not None]
if not services:
return
settings = BlinkSettings()
services.sort(key=lambda obj: obj.timestamp.value if obj.timestamp else epoch, reverse=True)
service = services[0]
if service.id in ('SID-%s' % uuid.UUID(SIPSimpleSettings().instance_id), 'SID-%s' % hashlib.md5(notification.sender.id).hexdigest()):
# Our current state is the winning one
return
status = unicode(service.status.extended).title()
note = None if not service.notes else unicode(list(service.notes)[0])
if status == 'Offline':
status = 'Invisible'
note = None
new_state = PresenceState(status, note)
settings.presence.current_state = new_state
if new_state.note:
try:
state = next(state for state in settings.presence.state_history if state==new_state)
except StopIteration:
settings.presence.state_history = [new_state] + settings.presence.state_history
else:
settings.presence.state_history = [new_state] + [state for state in settings.presence.state_history if state!=new_state]
settings.save()
def _NH_XCAPManagerDidDiscoverServerCapabilities(self, notification):
account = notification.sender.account
if account.enabled and account.presence.enabled:
self.publish(account)
def _NH_XCAPManagerDidReloadData(self, notification):
account = notification.sender.account
settings = BlinkSettings()
offline_status = notification.data.offline_status
status_icon = notification.data.status_icon
offline_note = None
if offline_status:
offline_pidf = offline_status.pidf
try:
service = next(offline_pidf.services)
note = next(iter(service.notes))
except StopIteration:
pass
else:
offline_note = unicode(note)
settings.presence.offline_note = offline_note
settings.save()
if status_icon:
icon_desc = IconDescriptor(notification.sender.status_icon.uri, notification.sender.status_icon.etag)
icon_hash = hashlib.sha512(status_icon.data).hexdigest()
if settings.presence.icon and settings.presence.icon.etag == icon_hash:
# Icon didn't change
pass
else:
self._save_icon(status_icon.data, icon_hash)
if icon_desc != account.icon:
account.icon = icon_desc
self.publish(account)
else:
# TODO: remove local icon?
pass
class PresenceSubscriptionHandler(object):
implements(IObserver)
def __init__(self):
self._pidf_map = {}
self._winfo_map = {}
self._winfo_timers = {}
def start(self):
notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPAccountWillActivate')
notification_center.add_observer(self, name='SIPAccountWillDeactivate')
notification_center.add_observer(self, name='SIPAccountGotPresenceState')
notification_center.add_observer(self, name='SIPAccountGotPresenceWinfo')
def stop(self):
notification_center = NotificationCenter()
notification_center.remove_observer(self, name='SIPAccountWillActivate')
notification_center.remove_observer(self, name='SIPAccountWillDeactivate')
notification_center.remove_observer(self, name='SIPAccountGotPresenceState')
notification_center.remove_observer(self, name='SIPAccountGotPresenceWinfo')
self._pidf_map.clear()
self._winfo_map.clear()
for timer in self._winfo_timers.values():
if timer.active():
timer.cancel()
self._winfo_timers.clear()
def _download_icon(self, url, etag):
headers = {'If-None-Match': etag} if etag else {}
req = urllib2.Request(url, headers=headers)
try:
response = urllib2.urlopen(req)
content = response.read()
info = response.info()
except (urllib2.HTTPError, urllib2.URLError):
return None, None
content_type = info.getheader('content-type')
etag = info.getheader('etag')
if etag.startswith('W/'):
etag = etag[2:]
etag = etag.replace('\"', '')
if content_type == prescontent.PresenceContentDocument.content_type:
try:
pres_content = prescontent.PresenceContentDocument.parse(content)
content = base64.decodestring(pres_content.data.value)
except Exception:
return None, None
return content, etag
@run_in_green_thread
def _process_presence_data(self, uris=None):
addressbook_manager = addressbook.AddressbookManager()
notification_center = NotificationCenter()
current_pidf_map = {}
contact_pidf_map = {}
# If no URIs were provided, process all of them
if not uris:
uris = list(chain(*(item.iterkeys() for item in self._pidf_map.itervalues())))
for uri, pidf_list in chain(*(x.iteritems() for x in self._pidf_map.itervalues())):
current_pidf_map.setdefault(uri, []).extend(pidf_list)
for uri in uris:
pidf_list = current_pidf_map.get(uri, [])
for contact in (contact for contact in addressbook_manager.get_contacts() if uri in (sip_prefix_re.sub('', contact_uri.uri) for contact_uri in contact.uris)):
contact_pidf_map.setdefault(contact, []).extend(pidf_list)
for contact, pidf_list in contact_pidf_map.iteritems():
if not pidf_list:
state = note = icon_descriptor = icon_data = None
else:
services = list(chain(*(list(pidf_doc.services) for pidf_doc in pidf_list)))
services.sort(key=lambda obj: obj.timestamp.value if obj.timestamp else epoch, reverse=True)
service = services[0]
if service.status.extended:
state = unicode(service.status.extended)
else:
state = 'available' if service.status.basic=='open' else 'offline'
note = unicode(next(iter(service.notes))) if service.notes else None
icon = unicode(service.icon) if service.icon else None
icon_data = icon_descriptor = None
if icon and icon != unknown_icon and (not contact.icon or (contact.icon and not contact.icon.is_local)):
if 'blink-icon' in icon and contact.icon and icon == contact.icon.url:
# Fast path, icon hasn't changed
pass
else:
icon_data, etag = self._download_icon(icon, contact.icon.etag if contact.icon else None)
if icon_data:
icon_descriptor = IconDescriptor(icon, etag)
data = NotificationData(state=state, note=note, icon_descriptor=icon_descriptor, icon_data=icon_data)
notification_center.post_notification('AddressbookContactGotPresenceUpdate', sender=contact, data=data)
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_CFGSettingsObjectDidChange(self, notification):
account = notification.sender
if '__id__' in notification.data.modified:
old_id = notification.data.modified['__id__'].old
self._pidf_map.pop(old_id, None)
self._winfo_map.pop(old_id, None)
self._process_presence_data()
return
if set(['enabled', 'presence.enabled']).intersection(notification.data.modified):
if not account.enabled or not account.presence.enabled:
self._pidf_map.pop(account.id, None)
self._winfo_map.pop(account.id, None)
self._process_presence_data()
def _NH_SIPAccountWillActivate(self, notification):
if notification.sender is not BonjourAccount():
notification.center.add_observer(self, sender=notification.sender, name='CFGSettingsObjectDidChange')
notification.center.add_observer(self, sender=notification.sender, name='SIPAccountGotPresenceState')
notification.center.add_observer(self, sender=notification.sender, name='SIPAccountGotPresenceWinfo')
def _NH_SIPAccountWillDeactivate(self, notification):
if notification.sender is not BonjourAccount():
notification.center.remove_observer(self, sender=notification.sender, name='CFGSettingsObjectDidChange')
notification.center.remove_observer(self, sender=notification.sender, name='SIPAccountGotPresenceState')
notification.center.remove_observer(self, sender=notification.sender, name='SIPAccountGotPresenceWinfo')
def _NH_SIPAccountGotPresenceState(self, notification):
account = notification.sender
new_pidf_map = dict((sip_prefix_re.sub('', uri), resource.pidf_list) for uri, resource in notification.data.resource_map.iteritems())
if notification.data.full_state:
self._pidf_map.setdefault(account.id, {}).clear()
self._pidf_map[account.id].update(new_pidf_map)
self._process_presence_data(new_pidf_map.keys())
def _NH_SIPAccountGotPresenceWinfo(self, notification):
addressbook_manager = addressbook.AddressbookManager()
account = notification.sender
watcher_list = notification.data.watcher_list
if notification.data.state == 'full':
self._winfo_map.setdefault(account.id, {}).clear()
for watcher in watcher_list:
uri = sip_prefix_re.sub('', watcher.sipuri)
if uri != account.id:
# Skip own URI, XCAP may be down and policy may not be inplace yet
self._winfo_map[account.id].setdefault(watcher.status, set()).add(uri)
pending_watchers = self._winfo_map[account.id].setdefault('pending', set()) | self._winfo_map[account.id].setdefault('waiting', set())
for uri in pending_watchers:
# check if there is a policy
try:
next(policy for policy in addressbook_manager.get_policies() if policy.uri == uri and policy.presence.policy != 'default')
except StopIteration:
# check if there is a contact
try:
next(contact for contact in addressbook_manager.get_contacts() if contact.presence.policy != 'default' and uri in (addr.uri for addr in contact.uris))
except StopIteration:
# TODO: add display name -Saul
if uri not in self._winfo_timers:
self._winfo_timers[uri] = reactor.callLater(600, self._winfo_timers.pop, uri, None)
notification.center.post_notification('SIPAccountGotPendingWatcher', sender=account, data=NotificationData(uri=uri, display_name=None, event='presence'))
class PresenceManager(object):
def __init__(self):
self.publication_handler = PresencePublicationHandler()
self.subscription_handler = PresenceSubscriptionHandler()
def start(self):
self.publication_handler.start()
self.subscription_handler.start()
def stop(self):
self.publication_handler.stop()
self.subscription_handler.stop()
ui_class, base_class = uic.loadUiType(Resources.get('pending_watcher.ui'))
class PendingWatcherDialog(base_class, ui_class):
def __init__(self, account, uri, display_name, parent=None):
super(PendingWatcherDialog, self).__init__(parent)
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_DeleteOnClose)
with Resources.directory:
self.setupUi(self)
addressbook_manager = addressbook.AddressbookManager()
try:
self.contact = next(contact for contact in addressbook_manager.get_contacts() if uri in (addr.uri for addr in contact.uris))
except StopIteration:
self.contact = None
else:
display_name = self.contact.name
icon = IconManager().get(self.contact.id)
if icon:
self.user_icon.setPixmap(icon.pixmap(32))
self.account_label.setText(u'For account %s' % account.id)
self.name_label.setText(display_name or uri)
self.uri_label.setText(uri)
font = self.name_label.font()
font.setPointSizeF(self.uri_label.fontInfo().pointSizeF() + 3)
font.setFamily("Sans Serif")
self.name_label.setFont(font)
self.accept_button.released.connect(self._accept_watcher)
self.block_button.released.connect(self._block_watcher)
self.position = None
self.timer = QTimer()
self.timer.timeout.connect(self._SH_TimerFired)
self.timer.start(60000)
def _SH_TimerFired(self):
self.timer.stop()
self.close()
def _accept_watcher(self):
self.timer.stop()
if not self.contact:
self.contact = addressbook.Contact()
self.contact.name = self.name_label.text()
self.contact.uris = [addressbook.ContactURI(uri=self.uri_label.text())]
self.contact.presence.policy = 'allow'
self.contact.presence.subscribe = True
self.contact.save()
def _block_watcher(self):
self.timer.stop()
policy = addressbook.Policy()
policy.uri = self.uri_label.text()
policy.name = self.name_label.text()
policy.presence.policy = 'block'
policy.save()
def show(self, position=1):
from blink import Blink
blink = Blink()
screen_geometry = blink.desktop().screenGeometry(self)
available_geometry = blink.desktop().availableGeometry(self)
main_window_geometry = blink.main_window.geometry()
main_window_framegeometry = blink.main_window.frameGeometry()
horizontal_decorations = main_window_framegeometry.width() - main_window_geometry.width()
vertical_decorations = main_window_framegeometry.height() - main_window_geometry.height()
width = limit(self.sizeHint().width(), min=self.minimumSize().width(), max=min(self.maximumSize().width(), available_geometry.width()-horizontal_decorations))
height = limit(self.sizeHint().height(), min=self.minimumSize().height(), max=min(self.maximumSize().height(), available_geometry.height()-vertical_decorations))
total_width = width + horizontal_decorations
total_height = height + vertical_decorations
x = limit(screen_geometry.center().x() - total_width/2, min=available_geometry.left(), max=available_geometry.right()-total_width)
if position is None:
y = -1
elif position % 2 == 0:
y = screen_geometry.center().y() + (position-1)*total_height/2
else:
y = screen_geometry.center().y() - position*total_height/2
if available_geometry.top() <= y <= available_geometry.bottom() - total_height:
self.setGeometry(x, y, width, height)
else:
self.resize(width, height)
self.position = position
super(PendingWatcherDialog, self).show()
del ui_class, base_class
<?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>480</width>
<height>170</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>480</width>
<height>170</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>170</height>
</size>
</property>
<property name="windowTitle">
<string>Availability subscription request</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>icons/blink48.png</normaloff>icons/blink48.png</iconset>
</property>
<layout class="QVBoxLayout" name="dialog_layout">
<property name="spacing">
<number>10</number>
</property>
<item>
<widget class="QFrame" name="frame">
<property name="styleSheet">
<string>QFrame#frame {
background-color: #f8f8f8;
border-color: #545454;
border-radius: 4px;
border-width: 2px;
border-style: solid;
}
</string>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<layout class="QGridLayout" name="frame_layout">
<property name="topMargin">
<number>7</number>
</property>
<property name="bottomMargin">
<number>7</number>
</property>
<property name="verticalSpacing">
<number>0</number>
</property>
<item row="0" column="0" rowspan="2">
<layout class="QVBoxLayout" name="user_icon_layout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="user_icon">
<property name="minimumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>36</width>
<height>36</height>
</size>
</property>
<property name="pixmap">
<pixmap>icons/default-avatar.png</pixmap>
</property>
<property name="alignment">
<set>Qt::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="user_icon_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="0" column="1">
<widget class="QLabel" name="name_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>26</height>
</size>
</property>
<property name="font">
<font>
<family>Sans Serif</family>
<pointsize>12</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Watcher name</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="uri_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string>Watcher URI</string>
</property>
<property name="indent">
<number>1</number>
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<widget class="QLabel" name="description_label">
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string extracomment="is offering to share his desktop">Wants to subscribe to your availability information</string>
</property>
<property name="indent">
<number>3</number>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<spacer name="frame_spacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>407</width>
<height>22</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLabel" name="account_label">
<property name="palette">
<palette>
<active>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>68</red>
<green>68</green>
<blue>68</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="WindowText">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
<colorrole role="Text">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>118</red>
<green>118</green>
<blue>117</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string>For account user@domain.com</string>
</property>
<property name="indent">
<number>3</number>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="button_layout">
<property name="spacing">
<number>5</number>
</property>
<item>
<spacer name="button_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="block_button">
<property name="minimumSize">
<size>
<width>85</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>24</height>
</size>
</property>
<property name="toolTip">
<string>Refuse the call but leave other devices ringing</string>
</property>
<property name="text">
<string>Block</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="accept_button">
<property name="minimumSize">
<size>
<width>85</width>
<height>24</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>24</height>
</size>
</property>
<property name="text">
<string>Accept</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<tabstops>
<tabstop>accept_button</tabstop>
<tabstop>block_button</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>accept_button</sender>
<signal>clicked()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>345</x>
<y>117</y>
</hint>
<hint type="destinationlabel">
<x>196</x>
<y>67</y>
</hint>
</hints>
</connection>
<connection>
<sender>block_button</sender>
<signal>clicked()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>138</x>
<y>117</y>
</hint>
<hint type="destinationlabel">
<x>196</x>
<y>67</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