Commit cfc8489d authored by Dan Pascu's avatar Dan Pascu

Implemented contact list view

parent 88df2801
......@@ -3,7 +3,7 @@
__all__ = ['Blink']
from PyQt4.QtCore import Qt, SIGNAL, SLOT
from PyQt4.QtCore import Qt
from PyQt4.QtGui import QApplication, QBrush, QColor, QPainter, QPen, QPixmap
# We need to fix __path__ in order be able to import the ui module when used
......@@ -20,6 +20,7 @@ import sys
_qt_application = QApplication(sys.argv)
from blink import ui
from blink.contacts import ContactDelegate, ContactModel
from blink.resources import Resources
......@@ -27,28 +28,29 @@ class Blink(object):
def __init__(self):
self.app = _qt_application
self.main_window = ui.main_window
#self.main_window.setWindowTitle('Blink')
self.main_window.setWindowTitle('Blink')
self.main_window.setWindowIconText('Blink')
self._setup_identities()
#self.contacts_widget = uic.loadUi("contacts.ui", self.main_window.widget)
#self.contacts_widget.hide()
self.contact_model = ContactModel(self.main_window)
self.main_window.contact_list.setModel(self.contact_model)
self.main_window.contact_list.setItemDelegate(ContactDelegate(self.main_window.contact_list))
self.contact_model.test()
self.main_window.main_view.setCurrentWidget(self.main_window.contacts_panel)
self.main_window.contacts_view.setCurrentWidget(self.main_window.contact_list_panel)
self.main_window.search_view.setCurrentWidget(self.main_window.search_list_panel)
self.main_window.connect(self.main_window.search_box, SIGNAL("textChanged(const QString&)"), self.text_changed)
self.main_window.connect(self.main_window.back_to_contacts, SIGNAL("clicked()"), self.main_window.search_box, SLOT("clear()"))
self.main_window.search_box.textChanged.connect(self.text_changed)
self.main_window.back_to_contacts.clicked.connect(self.main_window.search_box.clear)
self.main_window.add_contact.clicked.connect(self.test_add_contact)
#self.main_window.search_box.setStyleSheet(search_css) # this method is not working properly with all themes. -Dan
self.main_window.connect(self.main_window.identity, SIGNAL("currentIndexChanged (const QString&)"), self.set_identity)
#self.main_window.connect(self.main_window.identity, QtCore.SIGNAL("activated(const QString&)"), self.set_identity2)
self.main_window.identity.currentIndexChanged[str].connect(self.set_identity)
#self.main_window.connect(self.main_window.icon_view, QtCore.SIGNAL("clicked()"), self.set_icon_view_mode)
#self.main_window.connect(self.main_window.list_view, QtCore.SIGNAL("clicked()"), self.set_list_view_mode)
#self.main_window.connect(self.main_window.list_view, QtCore.SIGNAL("doubleClicked(const QModelIndex &)"), self.play_game)
#self.main_window.connect(self.main_window.list_view.selectionModel(), QtCore.SIGNAL("selectionChanged(const QItemSelection &, const QItemSelection &)"), self.selection_changed)
#self.main_window.connect(self.main_window.contact_list, QtCore.SIGNAL("doubleClicked(const QModelIndex &)"), self.double_click_action)
#self.main_window.connect(self.main_window.contact_list.selectionModel(), QtCore.SIGNAL("selectionChanged(const QItemSelection &, const QItemSelection &)"), self.selection_changed)
def run(self):
self.main_window.show()
......@@ -80,13 +82,18 @@ class Blink(object):
def set_identity(self, string):
print "identity changed", string
def set_identity2(self, string):
print "identity (re)selected", string
def text_changed(self, text):
active_widget = self.main_window.contact_list_panel if text.isEmpty() else self.main_window.search_panel
self.main_window.contacts_view.setCurrentWidget(active_widget)
active_widget = self.main_window.search_list_panel if len(text)<3 else self.main_window.not_found_panel
self.main_window.search_view.setCurrentWidget(active_widget)
def test_add_contact(self):
from blink.contacts import Contact, ContactGroup
import random
no = random.randrange(1, 100)
contact = Contact(ContactGroup('Test'), 'John Doe %02d' % no, 'user%02d@test.com' % no)
contact.status = random.choice(('online', 'away', 'busy', 'offline'))
self.contact_model.addContact(contact)
# Copyright (C) 2010 AG Projects. See LICENSE for details.
#
from PyQt4.QtCore import Qt, QAbstractListModel, QModelIndex, QSize
from PyQt4.QtGui import QAbstractItemDelegate, QColor, QPainter, QPalette, QPixmap, QStyle, QStyledItemDelegate
from application.python.util import Null
from functools import partial
from weakref import WeakValueDictionary
from blink.resources import Resources
from blink.ui import ContactWidget, ContactGroupWidget
class ContactGroup(object):
instances = WeakValueDictionary()
def __new__(cls, name):
obj = cls.instances.get(name, None)
if obj is None:
obj = object.__new__(cls)
obj.name = name
obj.widget = Null
cls.instances[name] = obj
return obj
def __repr__(self):
return "%s(%r)" % (self.__class__.__name__, self.name)
def __str__(self):
return self.name
@property
def collapsed(self):
return self.widget.collapsed
def _get_name(self):
return self.__dict__['name']
def _set_name(self, name):
old_name = self.__dict__.get('name')
if name == old_name:
return
if old_name is not None:
del self.instances[old_name]
self.__dict__['name'] = name
self.instances[name] = self
name = property(_get_name, _set_name)
del _get_name, _set_name
class ContactIconDescriptor(object):
def __init__(self, filename):
self.filename = Resources.get(filename)
self.icon = None
def __get__(self, obj, objtype):
if self.icon is None:
pixmap = QPixmap()
if pixmap.load(self.filename):
self.icon = pixmap.scaled(32, 32, Qt.KeepAspectRatio, Qt.SmoothTransformation)
else:
self.icon = pixmap
return self.icon
def __set__(self, obj, value):
raise AttributeError("attribute cannot be set")
def __delete__(self, obj):
raise AttributeError("attribute cannot be deleted")
class Contact(object):
default_user_icon = ContactIconDescriptor('icons/default_user_icon.png')
def __init__(self, group, name, uri, icon=None):
self.group = group
self.name = name
self.uri = uri
self.icon = self.default_user_icon if icon is None else ContactIconDescriptor(icon).__get__(self, self.__class__)
self.status = 'unknown'
class ContactDelegate(QStyledItemDelegate):
item_size_hints = {Contact: QSize(200, 36), ContactGroup: QSize(200, 18)}
def __init__(self, parent=None):
super(ContactDelegate, self).__init__(parent)
self.oddline_widget = ContactWidget(None)
self.evenline_widget = ContactWidget(None)
self.selected_widget = ContactWidget(None)
palette = self.oddline_widget.palette()
palette.setColor(QPalette.Window, QColor("#ffffff"))
self.oddline_widget.setPalette(palette)
palette = self.evenline_widget.palette()
palette.setColor(QPalette.Window, QColor("#f0f4ff"))
self.evenline_widget.setPalette(palette)
palette = self.selected_widget.palette()
palette.setBrush(QPalette.Window, palette.highlight())
palette.setBrush(QPalette.WindowText, palette.highlightedText())
self.selected_widget.setPalette(palette)
def _update_list_view(self, group, collapsed):
list_view = self.parent()
list_items = list_view.model().items
for position in xrange(list_items.index(group)+1, len(list_items)):
if type(list_items[position]) is ContactGroup:
break
list_view.setRowHidden(position, collapsed)
def createEditor(self, parent, options, index):
item = index.model().data(index, Qt.DisplayRole)
if type(item) is ContactGroup:
if item.widget is Null:
item.widget = ContactGroupWidget(item.name, parent)
item.widget.arrow.toggled.connect(partial(self._update_list_view, item))
return item.widget
else:
return None
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
def paintContact(self, contact, painter, option, index):
if option.state & QStyle.State_Selected:
widget = self.selected_widget
elif index.row() % 2 == 1:
widget = self.evenline_widget
else:
widget = self.oddline_widget
widget.set_contact(contact)
item_size = option.rect.size()
if widget.size() != item_size:
widget.resize(item_size)
painter.save()
pixmap = QPixmap(item_size)
widget.render(pixmap)
painter.drawPixmap(option.rect, pixmap)
if contact.status not in ('offline', 'unknown'):
status_colors = dict(online='#00ff00', away='#ffff00', busy='#ff0000')
color = QColor(status_colors[contact.status])
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setBrush(color)
painter.setPen(color.darker(200))
width, border, radius = 4, 2, 2
painter.drawRoundedRect(option.rect.topRight().x()-width-border, option.rect.y()+border, width, option.rect.height()-2*border, radius, radius)
if 0 and (option.state & QStyle.State_MouseOver):
painter.setRenderHint(QPainter.Antialiasing, True)
if option.state & QStyle.State_Selected:
painter.fillRect(option.rect, QColor(240, 244, 255, 40))
else:
painter.setCompositionMode(QPainter.CompositionMode_DestinationIn)
painter.fillRect(option.rect, QColor(240, 244, 255, 230))
painter.restore()
def paintContactGroup(self, group, painter, option, index):
item = index.model().data(index, Qt.DisplayRole)
if item.widget.size() != option.rect.size():
# For some reason updateEditorGeometry only receives the peak value of
# the size that the widget ever had, so it will never shrink it. -Dan
item.widget.resize(option.rect.size())
item.widget.selected = bool(option.state & QStyle.State_Selected)
def paint(self, painter, option, index):
item = index.model().data(index, Qt.DisplayRole)
handler = getattr(self, 'paint%s' % item.__class__.__name__, Null)
handler(item, painter, option, index)
def sizeHint(self, option, index):
return self.item_size_hints[type(index.model().data(index, Qt.DisplayRole))]
class ContactModel(QAbstractListModel):
def __init__(self, parent=None):
super(ContactModel, self).__init__(parent)
self.items = []
self.contact_list = parent.contact_list
@property
def contact_groups(self):
return [item for item in self.items if type(item) is ContactGroup]
def flags(self, index):
if not index.isValid():
return Qt.ItemIsEnabled
return Qt.ItemFlags(QAbstractListModel.flags(self, index) | Qt.ItemIsEditable)
def rowCount(self, parent=QModelIndex()):
return len(self.items)
def data(self, index, role=Qt.DisplayRole):
if not index.isValid() or role != Qt.DisplayRole:
return None
return self.items[index.row()]
def addContact(self, contact):
if contact.group in self.items:
for position in xrange(self.items.index(contact.group)+1, len(self.items)):
item = self.items[position]
if type(item) is ContactGroup or item.name > contact.name:
break
else:
position = len(self.items)
self.beginInsertRows(QModelIndex(), position, position)
self.items.append(contact)
self.endInsertRows()
self.contact_list.setRowHidden(position, contact.group.collapsed)
else:
position = len(self.items)
self.beginInsertRows(QModelIndex(), position, position+1)
self.items.append(contact.group)
self.items.append(contact)
self.contact_list.openPersistentEditor(self.index(position))
self.endInsertRows()
def deleteContacts(self, indexes):
rows = sorted(index.row() for index in indexes if index.isValid())
self.beginRemoveRows(QModelIndex(), rows[0], rows[-1])
for row in reversed(rows):
self.items.pop(row)
self.endRemoveRows()
# temporary function for testing
def addGroup(self, group):
if group in self.items:
return
position = len(self.items)
self.beginInsertRows(QModelIndex(), position, position)
self.items.append(group)
self.contact_list.openPersistentEditor(self.index(position))
self.endInsertRows()
def test(self):
work_group = ContactGroup('Work')
test_group = ContactGroup('Test')
for contact in [Contact(work_group, 'Dan Pascu', '31208005167@ag-projects.com', 'icons/avatar.png'), Contact(work_group, 'Lucian Stanescu', '31208005164@ag-projects.com'), Contact(work_group, 'Test number', '3333@ag-projects.com')]:
if contact.uri.startswith('3333@') or contact.uri.startswith('31208005167@'):
contact.status = 'online'
else:
contact.status = 'busy'
self.addContact(contact)
self.addGroup(test_group)
# Copyright (C) 2010 AG Projects. See LICENSE for details.
#
__all__ = ['main_window']
__all__ = ['main_window', 'ContactWidget', 'ContactGroupWidget']
import os
from PyQt4 import uic
from PyQt4.QtCore import Qt, QEvent, QPointF
from PyQt4.QtGui import QBrush, QColor, QLinearGradient, QKeyEvent, QMouseEvent, QPainter, QPen, QPolygonF
from blink.resources import Resources
original_directory = os.getcwd()
os.chdir(Resources.directory)
main_window = uic.loadUi(Resources.get('blink.ui'))
ui_class, base_class = uic.loadUiType(Resources.get('contact.ui'))
class ContactWidget(base_class, ui_class):
def __init__(self, parent=None):
super(ContactWidget, self).__init__(parent)
self.setupUi(self)
def set_contact(self, contact):
self.name.setText(contact.name)
self.uri.setText(contact.uri)
self.icon.setPixmap(contact.icon)
del ui_class, base_class
ui_class, base_class = uic.loadUiType(Resources.get('contact_group.ui'))
class ContactGroupWidget(base_class, ui_class):
def __init__(self, name, parent=None):
super(ContactGroupWidget, self).__init__(parent)
self.setupUi(self)
self.name = name
self.selected = False
self.setFocusProxy(parent)
self.label_widget.setFocusProxy(self)
self.name_view.setCurrentWidget(self.label_widget)
self.name_editor.editingFinished.connect(self._end_editing)
@property
def collapsed(self):
return self.arrow.isChecked()
@property
def editing(self):
return self.name_view.currentWidget() is self.editor_widget
def _get_name(self):
return self.name_label.text()
def _set_name(self, value):
self.name_label.setText(value)
self.name_editor.setText(value)
name = property(_get_name, _set_name)
del _get_name, _set_name
def _get_selected(self):
return self.__dict__['selected']
def _set_selected(self, value):
if self.__dict__.get('selected', None) == value:
return
self.__dict__['selected'] = value
self.name_label.setStyleSheet("color: #ffffff; font-weight: bold;" if value else "color: #000000;")
#self.name_label.setForegroundRole(QPalette.BrightText if value else QPalette.WindowText)
self.update()
selected = property(_get_selected, _set_selected)
del _get_selected, _set_selected
def _start_editing(self):
#self.name_editor.setText(self.name_label.text())
self.name_editor.selectAll()
self.name_view.setCurrentWidget(self.editor_widget)
self.name_editor.setFocus()
def _end_editing(self):
self.name_label.setText(self.name_editor.text())
self.name_view.setCurrentWidget(self.label_widget)
def paintEvent(self, event):
painter = QPainter(self)
background = QLinearGradient(0, 0, self.width(), self.height())
if self.selected:
background.setColorAt(0.0, QColor('#dadada'))
background.setColorAt(1.0, QColor('#c4c4c4'))
foreground = QColor('#ffffff')
else:
background.setColorAt(0.0, QColor('#eeeeee'))
background.setColorAt(1.0, QColor('#d8d8d8'))
foreground = QColor('#888888')
rect = self.rect()
painter.fillRect(rect, QBrush(background))
painter.setPen(QColor('#f8f8f8'))
painter.drawLine(rect.topLeft(), rect.topRight())
#painter.drawLine(option.rect.topLeft(), option.rect.bottomLeft())
painter.setPen(QColor('#b8b8b8'))
painter.drawLine(rect.bottomLeft(), rect.bottomRight())
#painter.drawLine(option.rect.topRight(), option.rect.bottomRight())
if self.collapsed:
arrow = QPolygonF([QPointF(0, 0), QPointF(0, 9), QPointF(8, 4.5)])
arrow.translate(QPointF(5, 4))
else:
arrow = QPolygonF([QPointF(0, 0), QPointF(9, 0), QPointF(4.5, 8)])
arrow.translate(QPointF(5, 5))
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setBrush(foreground)
painter.setPen(QPen(painter.brush(), 0, Qt.NoPen))
painter.drawPolygon(arrow)
painter.end()
def event(self, event):
if type(event) is QKeyEvent and self.editing:
return True # do not propagate keyboard events while editing
elif type(event) is QMouseEvent and event.type() == QEvent.MouseButtonDblClick and event.button() == Qt.LeftButton:
self._start_editing()
return super(ContactGroupWidget, self).event(event)
del ui_class, base_class
os.chdir(original_directory)
del original_directory
......@@ -278,7 +278,7 @@
<item>
<widget class="QStackedWidget" name="contacts_view">
<property name="currentIndex">
<number>1</number>
<number>0</number>
</property>
<widget class="QWidget" name="contact_list_panel">
<layout class="QVBoxLayout" name="verticalLayout_7">
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>contact</class>
<widget class="QWidget" name="contact">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>275</width>
<height>36</height>
</rect>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="255">
<red>255</red>
<green>255</green>
<blue>255</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>5</number>
</property>
<property name="leftMargin">
<number>2</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>2</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="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="text">
<string/>
</property>
<property name="pixmap">
<pixmap>icons/default_user_icon.png</pixmap>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="name_uri_layout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="name">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="font">
<font>
<pointsize>10</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Display Name</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="uri">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>URI</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>group</class>
<widget class="QWidget" name="group">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>275</width>
<height>18</height>
</rect>
</property>
<property name="windowTitle">
<string>Group</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<property name="spacing">
<number>2</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QToolButton" name="arrow">
<property name="maximumSize">
<size>
<width>18</width>
<height>18</height>
</size>
</property>
<property name="styleSheet">
<string>background-color: transparent;
border-style: none;
border-width: 0;
</string>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<property name="checkable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="name_view">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>18</height>
</size>
</property>
<property name="styleSheet">
<string>background-color: transparent;</string>
</property>
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="label_widget">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="name_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="palette">
<palette>
<active>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="0">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="0">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="0">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</active>
<inactive>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="0">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="0">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="0">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</inactive>
<disabled>
<colorrole role="Button">
<brush brushstyle="SolidPattern">
<color alpha="0">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Base">
<brush brushstyle="SolidPattern">
<color alpha="0">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
<colorrole role="Window">
<brush brushstyle="SolidPattern">
<color alpha="0">
<red>0</red>
<green>0</green>
<blue>0</blue>
</color>
</brush>
</colorrole>
</disabled>
</palette>
</property>
<property name="text">
<string>Group Name</string>
</property>
</widget>
</item>
</layout>
<zorder>name_label</zorder>
<zorder>name_editor</zorder>
</widget>
<widget class="QWidget" name="editor_widget">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="LineEdit" name="name_editor">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>18</height>
</size>
</property>
<property name="styleSheet">
<string>background-color: white;
border-style: outset;
border-width: 2px;
border-radius: 4px;
border-color: #3278c8;
</string>
</property>
<property name="text">
<string>Group Name</string>
</property>
<property name="frame">
<bool>false</bool>
</property>
<property name="widgetSpacing" stdset="0">
<number>0</number>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LineEdit</class>
<extends>QLineEdit</extends>
<header>blink.widgets.lineedit</header>
</customwidget>
</customwidgets>
<resources/>
<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