Commit 66bbc3f6 authored by Dan Pascu's avatar Dan Pascu

Implemented multiple URIs per contact

parent b2b50439
......@@ -15,10 +15,10 @@ SharedSetting.set_namespace('ag-projects:blink')
class ContactExtension(ContactExtension):
#auto_answer = SharedSetting(type=bool, default=False)
default_uri = SharedSetting(type=str, nillable=True, default=None)
preferred_media = SharedSetting(type=str, default='audio')
icon = Setting(type=IconDescriptor, nillable=True, default=None)
alternate_icon = Setting(type=IconDescriptor, nillable=True, default=None)
preferred_media = SharedSetting(type=str, default='audio')
#auto_answer = SharedSetting(type=bool, default=False)
class GroupExtension(GroupExtension):
......
......@@ -3,12 +3,7 @@
"""Definitions of datatypes for use in settings extensions."""
__all__ = ['ApplicationDataPath', 'DefaultPath',
'SoundFile', 'CustomSoundFile',
'HTTPURL',
'AuthorizationToken', 'InvalidToken',
'IconDescriptor',
'PresenceState', 'PresenceStateList']
__all__ = ['ApplicationDataPath', 'DefaultPath', 'SoundFile', 'CustomSoundFile', 'HTTPURL', 'AuthorizationToken', 'InvalidToken', 'IconDescriptor', 'PresenceState', 'PresenceStateList']
import os
import re
......@@ -141,16 +136,28 @@ class AuthorizationToken(str):
InvalidToken = AuthorizationToken() # a valid token is never empty
class ParsedURL(unicode):
fragment = property(lambda self: self.__parsed__.fragment)
netloc = property(lambda self: self.__parsed__.netloc)
params = property(lambda self: self.__parsed__.params)
path = property(lambda self: self.__parsed__.path)
query = property(lambda self: self.__parsed__.query)
scheme = property(lambda self: self.__parsed__.scheme)
def __init__(self, value):
self.__parsed__ = urlparse(self)
class IconDescriptor(object):
def __init__(self, url, etag=None):
self.url = url
self.url = ParsedURL(url)
self.etag = etag
def __getstate__(self):
if self.etag is None:
return unicode(self.url)
else:
return u'%s,%s' % (self.__dict__['url'], self.etag)
return u'%s,%s' % (self.url, self.etag)
def __setstate__(self, state):
try:
......@@ -172,26 +179,9 @@ class IconDescriptor(object):
def __repr__(self):
return '%s(%r, %r)' % (self.__class__.__name__, self.url, self.etag)
def _get_url(self):
url = self.__dict__['url']
file_scheme = 'file://'
if url.startswith(file_scheme):
url = file_scheme + ApplicationData.get(url[len(file_scheme):])
return url
def _set_url(self, url):
file_scheme = 'file://'
if url.startswith(file_scheme):
filename = os.path.normpath(url[len(file_scheme):])
if filename.startswith(ApplicationData.directory+os.path.sep):
filename = filename[len(ApplicationData.directory+os.path.sep):]
url = file_scheme + filename
self.__dict__['url'] = url
url = property(_get_url, _set_url)
del _get_url, _set_url
@property
def is_local(self):
return self.__dict__['url'].startswith('file://')
return self.url.scheme in ('', 'file')
class PresenceState(object):
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -9,7 +9,7 @@ import os
from functools import partial
from PyQt4 import uic
from PyQt4.QtCore import QUrl
from PyQt4.QtCore import Qt, QUrl
from PyQt4.QtGui import QAction, QActionGroup, QDesktopServices, QShortcut
from PyQt4.QtGui import QFileDialog, QIcon, QStyle, QStyleOptionComboBox, QStyleOptionFrameV2
......@@ -23,7 +23,7 @@ from sipsimple.configuration.settings import SIPSimpleSettings
from blink.aboutpanel import AboutPanel
from blink.accounts import AccountModel, ActiveAccountModel, ServerToolsAccountModel, ServerToolsWindow
from blink.contacts import BonjourNeighbour, Contact, Group, ContactEditorDialog, ContactModel, ContactSearchModel, GoogleContactsDialog
from blink.contacts import BonjourNeighbour, Contact, ContactEditorDialog, ContactModel, ContactSearchModel, GoogleContactsDialog
from blink.history import HistoryManager
from blink.preferences import PreferencesWindow
from blink.sessions import ConferenceDialog, SessionManager, SessionModel
......@@ -116,7 +116,6 @@ class MainWindow(base_class, ui_class):
self.conference_button.makeConference.connect(self._SH_MakeConference)
self.conference_button.breakConference.connect(self._SH_BreakConference)
self.contact_list.doubleClicked.connect(self._SH_ContactDoubleClicked) # activated is emitted on single click
self.contact_list.selectionModel().selectionChanged.connect(self._SH_ContactListSelectionChanged)
self.contact_model.itemsAdded.connect(self._SH_ContactModelAddedItems)
self.contact_model.itemsRemoved.connect(self._SH_ContactModelRemovedItems)
......@@ -134,7 +133,6 @@ class MainWindow(base_class, ui_class):
self.search_box.shortcut.activated.connect(self.search_box.setFocus)
self.search_list.selectionModel().selectionChanged.connect(self._SH_SearchListSelectionChanged)
self.search_list.doubleClicked.connect(self._SH_ContactDoubleClicked) # activated is emitted on single click
self.server_tools_account_model.rowsInserted.connect(self._SH_ServerToolsAccountModelChanged)
self.server_tools_account_model.rowsRemoved.connect(self._SH_ServerToolsAccountModelChanged)
......@@ -386,11 +384,11 @@ class MainWindow(base_class, ui_class):
if filename is not None:
icon = icon_manager.store_file('myicon', filename)
try:
hash = hashlib.sha512(open(icon.filename, 'r').read()).hexdigest()
hash = hashlib.sha512(open(icon.filename).read()).hexdigest()
except Exception:
settings.presence.icon = None
else:
settings.presence.icon = IconDescriptor('file://'+icon.filename, hash)
settings.presence.icon = IconDescriptor('file://' + icon.filename, hash)
else:
icon_manager.remove('myicon')
icon = None
......@@ -405,41 +403,28 @@ class MainWindow(base_class, ui_class):
self.account_state.setState(self.account_state.state, note)
def _SH_AddContactButtonClicked(self, clicked):
model = self.contact_model
groups = set()
for index in self.contact_list.selectionModel().selectedIndexes():
item = model.data(index)
if isinstance(item, Group) and not item.virtual:
groups.add(item)
elif isinstance(item, Contact) and not item.group.virtual:
groups.add(item.group)
preferred_group = groups.pop() if len(groups)==1 else None
self.contact_editor_dialog.open_for_add(self.search_box.text(), preferred_group)
self.contact_editor_dialog.open_for_add(self.search_box.text(), None)
def _SH_AudioCallButtonClicked(self):
list_view = self.contact_list if self.contacts_view.currentWidget() is self.contact_list_panel else self.search_list
selected_indexes = list_view.selectionModel().selectedIndexes()
contact = list_view.model().data(selected_indexes[0]) if selected_indexes else Null
address = contact.uri or self.search_box.text()
name = contact.name or None
session_manager = SessionManager()
session_manager.start_call(name, address, contact=contact, account=BonjourAccount() if isinstance(contact.settings, BonjourNeighbour) else None)
if list_view.detail_view.isVisible():
list_view.detail_view._AH_StartAudioCall()
else:
selected_indexes = list_view.selectionModel().selectedIndexes()
contact = selected_indexes[0].data(Qt.UserRole) if selected_indexes else Null
address = contact.uri or self.search_box.text()
name = contact.name or None
session_manager = SessionManager()
session_manager.start_call(name, address, contact=contact, account=BonjourAccount() if isinstance(contact.settings, BonjourNeighbour) else None)
def _SH_BreakConference(self):
active_session = self.session_model.data(self.session_list.selectionModel().selectedIndexes()[0])
active_session = self.session_list.selectionModel().selectedIndexes()[0].data()
self.session_model.breakConference(active_session.conference)
def _SH_ContactDoubleClicked(self, index):
contact = index.model().data(index)
if not isinstance(contact, Contact):
return
session_manager = SessionManager()
session_manager.start_call(contact.name, contact.uri, contact=contact, account=BonjourAccount() if isinstance(contact.settings, BonjourNeighbour) else None)
def _SH_ContactListSelectionChanged(self, selected, deselected):
account_manager = AccountManager()
selected_items = self.contact_list.selectionModel().selectedIndexes()
self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)==1 and isinstance(self.contact_model.data(selected_items[0]), Contact))
self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)==1 and isinstance(selected_items[0].data(Qt.UserRole), Contact))
def _SH_ContactModelAddedItems(self, items):
if not self.search_box.text():
......@@ -516,7 +501,9 @@ class MainWindow(base_class, ui_class):
else:
self.contacts_view.setCurrentWidget(self.contact_list_panel)
selected_items = self.contact_list.selectionModel().selectedIndexes()
self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)==1 and type(self.contact_model.data(selected_items[0])) is Contact)
self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)==1 and type(selected_items[0].data(Qt.UserRole)) is Contact)
self.search_list.detail_model.contact = None
self.search_list.detail_view.hide()
def _SH_SearchListSelectionChanged(self, selected, deselected):
account_manager = AccountManager()
......@@ -532,7 +519,7 @@ class MainWindow(base_class, ui_class):
def _SH_SessionListSelectionChanged(self, selected, deselected):
selected_indexes = selected.indexes()
active_session = self.session_model.data(selected_indexes[0]) if selected_indexes else Null
active_session = selected_indexes[0].data() if selected_indexes else Null
if active_session.conference:
self.conference_button.setEnabled(True)
self.conference_button.setChecked(True)
......@@ -550,7 +537,7 @@ class MainWindow(base_class, ui_class):
self.active_sessions_label.setVisible(any(active_sessions))
self.hangup_all_button.setEnabled(any(active_sessions))
selected_indexes = self.session_list.selectionModel().selectedIndexes()
active_session = self.session_model.data(selected_indexes[0]) if selected_indexes else Null
active_session = selected_indexes[0].data() if selected_indexes else Null
if active_session.conference:
self.conference_button.setEnabled(True)
self.conference_button.setChecked(True)
......@@ -730,7 +717,7 @@ class MainWindow(base_class, ui_class):
self.enable_call_buttons(False)
else:
selected_items = self.contact_list.selectionModel().selectedIndexes()
self.enable_call_buttons(len(selected_items)==1 and isinstance(self.contact_model.data(selected_items[0]), Contact))
self.enable_call_buttons(len(selected_items)==1 and isinstance(selected_items[0].data(Qt.UserRole), Contact))
def _NH_SIPAccountGotMessageSummary(self, notification):
account = notification.sender
......
......@@ -180,7 +180,7 @@ class PresencePublicationHandler(object):
icon = None
if settings.presence.icon:
try:
data = open(settings.presence.icon.url[7:], 'r').read() # strip 'file://'
data = open(settings.presence.icon.url.path).read()
except Exception:
pass
else:
......@@ -194,7 +194,7 @@ class PresencePublicationHandler(object):
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)
settings.presence.icon = IconDescriptor('file://' + icon.filename, icon_hash)
else:
settings.presence.icon = None
settings.save()
......
......@@ -961,7 +961,7 @@ class SessionModel(QAbstractListModel):
structureChanged = pyqtSignal()
# The MIME types we accept in drop operations, in the order they should be handled
accepted_mime_types = ['application/x-blink-session-list', 'application/x-blink-contact-list']
accepted_mime_types = ['application/x-blink-session-list', 'application/x-blink-contact-list', 'application/x-blink-contact-uri-list']
def __init__(self, parent=None):
super(SessionModel, self).__init__(parent)
......@@ -1103,13 +1103,30 @@ class SessionModel(QAbstractListModel):
def _DH_ApplicationXBlinkContactList(self, mime_data, action, index):
if not index.isValid():
return
try:
contacts = pickle.loads(str(mime_data.data('application/x-blink-contact-list')))
except Exception:
return
session = self.sessions[index.row()]
contacts = pickle.loads(str(mime_data.data('application/x-blink-contact-list')))
session_manager = SessionManager()
for contact in contacts:
session_manager.start_call(contact.name, contact.uri, contact=contact, conference_sibling=session)
return True
def _DH_ApplicationXBlinkContactUriList(self, mime_data, action, index):
if not index.isValid():
return
try:
contact_uris = pickle.loads(str(mime_data.data('application/x-blink-contact-uri-list')))
except Exception:
return
session = self.sessions[index.row()]
session_manager = SessionManager()
for contact_uri in contact_uris:
contact = contact_uri.contact
session_manager.start_call(contact.name, contact_uri.uri.uri, contact=contact, conference_sibling=session)
return True
def _add_session(self, session):
position = len(self.sessions)
self.beginInsertRows(QModelIndex(), position, position)
......@@ -1340,7 +1357,7 @@ class SessionListView(QListView):
def dragEnterEvent(self, event):
event_source = event.source()
accepted_mime_types = set(self.model().accepted_mime_types)
provided_mime_types = set(str(x) for x in event.mimeData().formats())
provided_mime_types = set(event.mimeData().formats())
acceptable_mime_types = accepted_mime_types & provided_mime_types
if not acceptable_mime_types:
event.ignore() # no acceptable mime types found
......@@ -1426,6 +1443,20 @@ class SessionListView(QListView):
else:
session.widget.drop_indicator = True
def _DH_ApplicationXBlinkContactUriList(self, event, index, rect, session):
model = self.model()
if not index.isValid():
rect = self.viewport().rect()
rect.setTop(self.visualRect(model.index(len(model.sessions)-1)).bottom())
event.ignore(rect)
else:
event.accept(rect)
if session.conference is not None:
for sibling in session.conference.sessions:
sibling.widget.drop_indicator = True
else:
session.widget.drop_indicator = True
def _SH_HangupShortcutActivated(self):
session = self.model().data(self.selectedIndexes()[0])
if session.conference is None:
......
......@@ -348,9 +348,7 @@ class SwitchViewButton(QPushButton):
self.setStyleSheet(style_sheet)
def dragEnterEvent(self, event):
if not self.dnd_active:
event.ignore()
elif event.mimeData().formats() == ['application/x-blink-contact-list']:
if self.dnd_active:
event.accept()
self._update_dnd()
self.dnd_timer.start()
......@@ -738,3 +736,4 @@ class AccountState(StateButton):
menu.insertAction(actions[0], action)
self.stateChanged.emit()
......@@ -6,46 +6,109 @@ __all__ = ['DurationLabel', 'IconSelector', 'LatencyLabel', 'PacketLossLabel', '
import os
from datetime import timedelta
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QBrush, QColor, QFileDialog, QFontMetrics, QLabel, QLinearGradient, QPalette, QPainter, QPen, QPixmap
from PyQt4.QtCore import Qt, QEvent
from PyQt4.QtGui import QAction, QBrush, QColor, QFileDialog, QFontMetrics, QIcon, QLabel, QLinearGradient, QMenu, QPainter, QPalette, QPen
from blink.resources import ApplicationData, Resources
from application.python.types import MarkerType
from blink.resources import IconManager
from blink.widgets.color import ColorHelperMixin
from blink.widgets.util import QtDynamicProperty
class IconSelector(QLabel):
default_icon = QtDynamicProperty('default_icon', unicode)
default_icon = QtDynamicProperty('default_icon', QIcon)
icon_size = QtDynamicProperty('icon_size', int)
class NotSelected: __metaclass__ = MarkerType
def __init__(self, parent=None):
super(IconSelector, self).__init__(parent)
self.setMinimumSize(36, 36)
self.filename = None
self.addAction(QAction(u'Select icon...', self, triggered=self._SH_ChangeIconActionTriggered))
self.addAction(QAction(u'Use contact provided icon', self, triggered=self._SH_ClearIconActionTriggered))
self.icon_size = 48
self.default_icon = None
self.contact_icon = None
self.icon = None
self.filename = self.NotSelected
self.last_icon_directory = os.path.expanduser('~')
def _get_icon(self):
return self.__dict__['icon']
def _set_icon(self, icon):
self.__dict__['icon'] = icon
icon = icon or self.default_icon or QIcon()
self.setPixmap(icon.pixmap(self.icon_size))
icon = property(_get_icon, _set_icon)
del _get_icon, _set_icon
def _get_filename(self):
return self.__dict__['filename']
def _set_filename(self, filename):
self.__dict__['filename'] = filename
filename = ApplicationData.get(filename) if filename else Resources.get(self.default_icon)
pixmap = QPixmap()
if pixmap.load(filename):
self.setPixmap(pixmap.scaled(32, 32, Qt.KeepAspectRatio, Qt.SmoothTransformation))
if filename is self.NotSelected:
return
elif filename is None:
self.icon = self.contact_icon
else:
self.setPixmap(pixmap)
self.icon = QIcon(filename)
self.last_icon_directory = os.path.dirname(filename)
filename = property(_get_filename, _set_filename)
del _get_filename, _set_filename
def init_with_contact(self, contact):
if contact is None:
self.icon = self.contact_icon = None
else:
icon_manager = IconManager()
self.contact_icon = icon_manager.get(contact.id)
self.icon = icon_manager.get(contact.id + '_alt') or self.contact_icon
if contact.alternate_icon is not None:
self.last_icon_directory = os.path.dirname(contact.alternate_icon.url.path)
self.filename = self.NotSelected
def update_from_contact(self, contact):
icon_manager = IconManager()
if self.icon is self.contact_icon:
self.icon = self.contact_icon = icon_manager.get(contact.id)
else:
self.contact_icon = icon_manager.get(contact.id)
def event(self, event):
if event.type() == QEvent.DynamicPropertyChange and event.propertyName() == 'icon_size':
self.setFixedSize(self.icon_size+12, self.icon_size+12)
self.update()
return super(IconSelector, self).event(event)
def enterEvent(self, event):
icon = self.icon or self.default_icon or QIcon()
self.setPixmap(icon.pixmap(self.icon_size, mode=QIcon.Selected))
super(IconSelector, self).enterEvent(event)
def leaveEvent(self, event):
icon = self.icon or self.default_icon or QIcon()
self.setPixmap(icon.pixmap(self.icon_size, mode=QIcon.Normal))
super(IconSelector, self).leaveEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton and self.rect().contains(event.pos()):
filename = QFileDialog.getOpenFileName(self, u'Select Icon', self.last_icon_directory, u"Images (*.png *.tiff *.jpg *.xmp *.svg)")
if filename:
self.last_icon_directory = os.path.dirname(filename)
self.filename = filename if os.path.realpath(filename) != os.path.realpath(Resources.get(self.default_icon)) else None
menu = QMenu(self)
menu.addActions(self.actions())
menu.exec_(self.mapToGlobal(self.rect().translated(0, 2).bottomLeft()))
super(IconSelector, self).mouseReleaseEvent(event)
def _SH_ChangeIconActionTriggered(self):
filename = QFileDialog.getOpenFileName(self, u'Select Icon', self.last_icon_directory, u"Images (*.png *.tiff *.jpg *.xmp *.svg)")
if filename:
self.filename = filename
def _SH_ClearIconActionTriggered(self):
self.filename = None
class StreamInfoLabel(QLabel):
def __init__(self, parent=None):
......@@ -198,3 +261,49 @@ class ElidedLabel(QLabel):
painter.drawText(self.rect(), Qt.TextSingleLine | int(self.alignment()), self.text())
class StateColor(QColor):
@property
def stroke(self):
return self.darker(200)
class StateColorMapping(dict):
def __missing__(self, key):
if key == 'offline':
return self.setdefault(key, StateColor('#d0d0d0'))
elif key == 'available':
return self.setdefault(key, StateColor('#00ff00'))
elif key == 'away':
return self.setdefault(key, StateColor('#ffff00'))
elif key == 'busy':
return self.setdefault(key, StateColor('#ff0000'))
else:
return StateColor(Qt.transparent) #StateColor('#d0d0d0')
class ContactState(QLabel, ColorHelperMixin):
state = QtDynamicProperty('color', unicode)
def __init__(self, parent=None):
super(ContactState, self).__init__(parent)
self.state_colors = StateColorMapping()
self.state = None
def event(self, event):
if event.type() == QEvent.DynamicPropertyChange and event.propertyName() == 'state':
self.update()
return super(ContactState, self).event(event)
def paintEvent(self, event):
color = self.state_colors[self.state]
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
gradient = QLinearGradient(0, 0, self.width(), 0)
gradient.setColorAt(0.0, Qt.transparent)
gradient.setColorAt(1.0, color)
painter.setBrush(QBrush(gradient))
gradient.setColorAt(1.0, color.stroke)
painter.setPen(QPen(QBrush(gradient), 1))
painter.drawRoundedRect(-4, 0, self.width()+4, self.height(), 3.7, 3.7)
......@@ -276,6 +276,12 @@ class SearchBox(LineEdit):
self.textChanged.connect(self._SH_TextChanged)
self.inactiveText = u"Search"
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape:
self.clear()
else:
super(SearchBox, self).keyPressEvent(event)
def _SH_TextChanged(self, text):
self.clear_button.setVisible(bool(text))
......
......@@ -342,6 +342,9 @@
</property>
<item>
<widget class="ContactListView" name="contact_list">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
......@@ -477,6 +480,9 @@
</property>
<item>
<widget class="ContactSearchListView" name="search_list">
<property name="mouseTracking">
<bool>true</bool>
</property>
<property name="dragEnabled">
<bool>true</bool>
</property>
......@@ -1348,20 +1354,20 @@ padding: 2px;</string>
</customwidgets>
<tabstops>
<tabstop>search_box</tabstop>
<tabstop>search_list</tabstop>
<tabstop>add_search_contact_button</tabstop>
<tabstop>back_to_contacts_button</tabstop>
<tabstop>contact_list</tabstop>
<tabstop>account_state</tabstop>
<tabstop>display_name</tabstop>
<tabstop>activity_note</tabstop>
<tabstop>identity</tabstop>
<tabstop>switch_view_button</tabstop>
<tabstop>contact_list</tabstop>
<tabstop>add_contact_button</tabstop>
<tabstop>audio_call_button</tabstop>
<tabstop>im_session_button</tabstop>
<tabstop>ds_session_button</tabstop>
<tabstop>silent_button</tabstop>
<tabstop>identity</tabstop>
<tabstop>add_search_contact_button</tabstop>
<tabstop>back_to_contacts_button</tabstop>
<tabstop>search_list</tabstop>
<tabstop>session_list</tabstop>
<tabstop>hangup_all_button</tabstop>
<tabstop>conference_button</tabstop>
......
......@@ -77,9 +77,9 @@
<property name="windowTitle">
<string>Contact</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QHBoxLayout" name="widget_layout">
<property name="spacing">
<number>5</number>
<number>3</number>
</property>
<property name="leftMargin">
<number>2</number>
......@@ -88,7 +88,7 @@
<number>0</number>
</property>
<property name="rightMargin">
<number>2</number>
<number>1</number>
</property>
<property name="bottomMargin">
<number>0</number>
......@@ -117,9 +117,6 @@
<property name="spacing">
<number>0</number>
</property>
<property name="rightMargin">
<number>8</number>
</property>
<item>
<widget class="ElidedLabel" name="name_label">
<property name="sizePolicy">
......@@ -148,6 +145,28 @@
</item>
</layout>
</item>
<item>
<widget class="ContactState" name="state_label">
<property name="minimumSize">
<size>
<width>14</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>14</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="state" stdset="0">
<string notr="true">unknown</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
......@@ -156,6 +175,11 @@
<extends>QLabel</extends>
<header>blink.widgets.labels</header>
</customwidget>
<customwidget>
<class>ContactState</class>
<extends>QLabel</extends>
<header>blink.widgets.labels</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
......
This diff is collapsed.
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