Commit cb229a55 authored by Dan Pascu's avatar Dan Pascu

Added web viewer for server tools extensions

parent 16621f36
......@@ -3,7 +3,7 @@
from __future__ import with_statement
__all__ = ['AccountModel', 'ActiveAccountModel', 'AccountSelector', 'AddAccountDialog']
__all__ = ['AccountModel', 'ActiveAccountModel', 'AccountSelector', 'AddAccountDialog', 'ServerToolsAccountModel', 'ServerToolsWindow']
import os
import re
......@@ -13,8 +13,10 @@ import urllib2
from collections import defaultdict
from PyQt4 import uic
from PyQt4.QtCore import Qt, QAbstractListModel, QModelIndex
from PyQt4.QtGui import QButtonGroup, QComboBox, QIcon, QPalette, QPixmap, QSortFilterProxyModel, QStyledItemDelegate
from PyQt4.QtCore import Qt, QAbstractListModel, QModelIndex, QUrl, QVariant
from PyQt4.QtGui import QAction, QButtonGroup, QComboBox, QIcon, QMenu, QPalette, QPixmap, QSortFilterProxyModel, QStyledItemDelegate
from PyQt4.QtNetwork import QNetworkAccessManager
from PyQt4.QtWebKit import QWebView
import cjson
from application.notification import IObserver, NotificationCenter
......@@ -57,8 +59,7 @@ class AccountModel(QAbstractListModel):
self.accounts = []
notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPAccountDidActivate')
notification_center.add_observer(self, name='SIPAccountDidDeactivate')
notification_center.add_observer(self, name='CFGSettingsObjectDidChange')
notification_center.add_observer(self, name='SIPAccountWillRegister')
notification_center.add_observer(self, name='SIPAccountRegistrationDidSucceed')
notification_center.add_observer(self, name='SIPAccountRegistrationDidFail')
......@@ -102,20 +103,17 @@ class AccountModel(QAbstractListModel):
self.accounts.append(AccountInfo(name, account, icon))
self.endInsertRows()
def _NH_CFGSettingsObjectDidChange(self, notification):
if isinstance(notification.sender, (Account, BonjourAccount)):
position = self.accounts.index(notification.sender)
self.dataChanged.emit(self.index(position), self.index(position))
def _NH_SIPAccountManagerDidRemoveAccount(self, notification):
position = self.accounts.index(notification.data.account)
self.beginRemoveRows(QModelIndex(), position, position)
del self.accounts[position]
self.endRemoveRows()
def _NH_SIPAccountDidActivate(self, notification):
position = self.accounts.index(notification.sender)
self.dataChanged.emit(self.index(position), self.index(position))
def _NH_SIPAccountDidDeactivate(self, notification):
position = self.accounts.index(notification.sender)
self.dataChanged.emit(self.index(position), self.index(position))
def _NH_SIPAccountWillRegister(self, notification):
position = self.accounts.index(notification.sender)
self.accounts[position].registration_state = 'started'
......@@ -487,3 +485,169 @@ class AddAccountDialog(base_class, ui_class):
del ui_class, base_class
# Account server tools
#
class ServerToolsAccountModel(QSortFilterProxyModel):
def __init__(self, model, parent=None):
super(ServerToolsAccountModel, self).__init__(parent)
self.setSourceModel(model)
self.setDynamicSortFilter(True)
def filterAcceptsRow(self, source_row, source_parent):
source_model = self.sourceModel()
source_index = source_model.index(source_row, 0, source_parent)
account_info = source_model.data(source_index, Qt.UserRole)
return bool(account_info.account is not BonjourAccount() and account_info.account.enabled and account_info.account.server.settings_url)
class ServerToolsWebView(QWebView):
implements(IObserver)
def __init__(self, parent=None):
super(ServerToolsWebView, self).__init__(parent)
self.access_manager = Null
self.authenticated = False
self.account = None
self.user_agent = 'blink'
self.tab = None
self.task = None
self.urlChanged.connect(self._SH_URLChanged)
@property
def query_items(self):
all_items = ('user_agent', 'tab', 'task')
return [(name, value) for name, value in self.__dict__.iteritems() if name in all_items and value is not None]
def _get_account(self):
return self.__dict__['account']
def _set_account(self, account):
notification_center = NotificationCenter()
old_account = self.__dict__.get('account', Null)
if account is old_account:
return
self.__dict__['account'] = account
self.authenticated = False
if old_account:
notification_center.remove_observer(self, sender=old_account)
if account:
notification_center.add_observer(self, sender=account)
self.access_manager.authenticationRequired.disconnect(self._SH_AuthenticationRequired)
self.access_manager = QNetworkAccessManager(self)
self.access_manager.authenticationRequired.connect(self._SH_AuthenticationRequired)
self.page().setNetworkAccessManager(self.access_manager)
account = property(_get_account, _set_account)
del _get_account, _set_account
@run_in_gui_thread
def handle_notification(self, notification):
handler = getattr(self, '_NH_%s' % notification.name, Null)
handler(notification)
def _NH_CFGSettingsObjectDidChange(self, notification):
if 'id' in notification.data.modified or 'auth.password' in notification.data.modified:
self.authenticated = False
self.reload()
def _SH_AuthenticationRequired(self, reply, auth):
if self.account and not self.authenticated:
auth.setUser(self.account.id)
auth.setPassword(self.account.auth.password)
self.authenticated = True
else:
# we were already authenticated, yet it asks for the auth again. this means our credentials are not good.
# we do not provide credentials anymore in order to fail and not try indefinitely, but we also reset the
# authenticated status so that we try again when the page is reloaded.
self.authenticated = False
def _SH_URLChanged(self, url):
query_items = dict((unicode(name), unicode(value)) for name, value in url.queryItems())
self.tab = query_items.get('tab') or self.tab
self.task = query_items.get('task') or self.task
def load_account_page(self, account, tab=None, task=None):
self.tab = tab
self.task = task
self.account = account
url = QUrl(account.server.settings_url)
for name, value in self.query_items:
url.addQueryItem(name, value)
self.load(url)
ui_class, base_class = uic.loadUiType(Resources.get('server_tools.ui'))
class ServerToolsWindow(base_class, ui_class):
__metaclass__ = QSingleton
def __init__(self, model, parent=None):
super(ServerToolsWindow, self).__init__(parent)
with Resources.directory:
self.setupUi(self)
while self.tab_widget.count():
self.tab_widget.removeTab(0) # remove the tab(s) added in designer
self.tab_widget.tabBar().hide()
self.account_button.setMenu(QMenu(self.account_button))
self.setWindowTitle('Blink Server Tools')
self.setWindowIconText('Server Tools')
self.model = model
self.tab_widget.addTab(ServerToolsWebView(self), '')
font = self.account_label.font()
font.setPointSizeF(self.account_label.fontInfo().pointSizeF() + 2)
font.setFamily("Sans Serif")
self.account_label.setFont(font)
self.model.rowsInserted.connect(self._SH_ModelChanged)
self.model.rowsRemoved.connect(self._SH_ModelChanged)
self.account_button.menu().triggered.connect(self._SH_AccountButtonMenuTriggered)
def _SH_AccountButtonMenuTriggered(self, action):
view = self.tab_widget.currentWidget()
account = action.data().toPyObject()
self.account_label.setText(account.id)
self.tab_widget.setTabText(self.tab_widget.currentIndex(), account.id)
view.load_account_page(account, tab=view.tab, task=view.task)
def _SH_ModelChanged(self, parent_index, start, end):
menu = self.account_button.menu()
menu.clear()
for row in xrange(self.model.rowCount()):
account_info = self.model.data(self.model.index(row, 0), Qt.UserRole).toPyObject()
action = QAction(account_info.name, self)
action.setData(QVariant(account_info.account))
menu.addAction(action)
def open_settings_page(self, account):
view = self.tab_widget.currentWidget()
account = account or view.account
if account is None or account.server.settings_url is None:
account = self.account_button.menu().actions()[0].data().toPyObject()
self.account_label.setText(account.id)
self.tab_widget.setTabText(self.tab_widget.currentIndex(), account.id)
view.load_account_page(account, tab='settings')
self.show()
def open_search_for_people_page(self, account):
view = self.tab_widget.currentWidget()
account = account or view.account
if account is None or account.server.settings_url is None:
account = self.account_button.menu().actions()[0].data().toPyObject()
self.account_label.setText(account.id)
self.tab_widget.setTabText(self.tab_widget.currentIndex(), account.id)
view.load_account_page(account, tab='contacts', task='directory')
self.show()
def open_history_page(self, account):
view = self.tab_widget.currentWidget()
account = account or view.account
if account is None or account.server.settings_url is None:
account = self.account_button.menu().actions()[0].data().toPyObject()
self.account_label.setText(account.id)
self.tab_widget.setTabText(self.tab_widget.currentIndex(), account.id)
view.load_account_page(account, tab='calls')
self.show()
del ui_class, base_class
......@@ -21,7 +21,7 @@ from sipsimple.application import SIPApplication
from sipsimple.configuration.settings import SIPSimpleSettings
from blink.aboutpanel import AboutPanel
from blink.accounts import AccountModel, ActiveAccountModel, AddAccountDialog
from blink.accounts import AccountModel, ActiveAccountModel, AddAccountDialog, ServerToolsAccountModel, ServerToolsWindow
from blink.contacts import BonjourNeighbour, Contact, ContactGroup, ContactEditorDialog, ContactModel, ContactSearchModel
from blink.sessions import SessionManager, SessionModel
from blink.resources import Resources
......@@ -36,111 +36,127 @@ class MainWindow(base_class, ui_class):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.idle_status_index = 0
notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPApplicationWillStart')
with Resources.directory:
self.setupUi()
self.setWindowTitle('Blink')
self.setWindowIconText('Blink')
self.set_user_icon(Resources.get("icons/default-avatar.png")) # ":/resources/icons/default-avatar.png"
self.enable_call_buttons(False)
self.active_sessions_label.hide()
self.enable_call_buttons(False)
self.conference_button.setEnabled(False)
self.hangup_all_button.setEnabled(False)
self.sip_server_settings_action.setEnabled(False)
self.search_for_people_action.setEnabled(False)
self.history_on_server_action.setEnabled(False)
self.main_view.setCurrentWidget(self.contacts_panel)
self.contacts_view.setCurrentWidget(self.contact_list_panel)
self.search_view.setCurrentWidget(self.search_list_panel)
# Accounts
self.account_model = AccountModel(self)
self.enabled_account_model = ActiveAccountModel(self.account_model, self)
self.server_tools_account_model = ServerToolsAccountModel(self.account_model, self)
self.identity.setModel(self.enabled_account_model)
# Contacts
self.contact_model = ContactModel(self)
self.contact_search_model = ContactSearchModel(self.contact_model, self)
self.contact_list.setModel(self.contact_model)
self.search_list.setModel(self.contact_search_model)
self.contact_list.selectionModel().selectionChanged.connect(self._SH_ContactListSelectionChanged)
self.search_list.selectionModel().selectionChanged.connect(self._SH_SearchListSelectionChanged)
self.search_box.textChanged.connect(self.contact_search_model.setFilterFixedString)
self.contact_model.load()
self.about_panel = AboutPanel(self)
self.add_account_dialog = AddAccountDialog(self)
self.contact_editor_dialog = ContactEditorDialog(self.contact_model, self)
# Sessions
self.session_model = SessionModel(self)
self.session_list.setModel(self.session_model)
self.session_list.selectionModel().selectionChanged.connect(self._SH_SessionListSelectionChanged)
self.main_view.setCurrentWidget(self.contacts_panel)
self.contacts_view.setCurrentWidget(self.contact_list_panel)
self.search_view.setCurrentWidget(self.search_list_panel)
self.conference_button.setEnabled(False)
self.hangup_all_button.setEnabled(False)
# Windows, dialogs and panels
self.about_panel = AboutPanel(self)
self.add_account_dialog = AddAccountDialog(self)
self.contact_editor_dialog = ContactEditorDialog(self.contact_model, self)
self.server_tools_window = ServerToolsWindow(self.server_tools_account_model, None)
self.switch_view_button.viewChanged.connect(self._SH_SwitchViewButtonChangedView)
# Signals
self.add_contact_button.clicked.connect(self._SH_AddContactButtonClicked)
self.add_search_contact_button.clicked.connect(self._SH_AddContactButtonClicked)
self.audio_call_button.clicked.connect(self._SH_AudioCallButtonClicked)
self.back_to_contacts_button.clicked.connect(self.search_box.clear) # this can be set in designer -Dan
self.conference_button.makeConference.connect(self._SH_MakeConference)
self.conference_button.breakConference.connect(self._SH_BreakConference)
self.search_box.textChanged.connect(self._SH_SearchBoxTextChanged)
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)
self.back_to_contacts_button.clicked.connect(self.search_box.clear) # this can be set in designer -Dan
self.add_contact_button.clicked.connect(self._SH_AddContactButtonClicked)
self.add_search_contact_button.clicked.connect(self._SH_AddContactButtonClicked)
self.display_name.editingFinished.connect(self._SH_DisplayNameEditingFinished)
self.hangup_all_button.clicked.connect(self._SH_HangupAllButtonClicked)
self.identity.activated[int].connect(self._SH_IdentityChanged)
self.identity.currentIndexChanged[int].connect(self._SH_IdentityCurrentIndexChanged)
self.display_name.editingFinished.connect(self._SH_DisplayNameEditingFinished)
self.status.activated[int].connect(self._SH_StatusChanged)
self.mute_button.clicked.connect(self._SH_MuteButtonClicked)
self.silent_button.clicked.connect(self._SH_SilentButtonClicked)
self.search_box.textChanged.connect(self._SH_SearchBoxTextChanged)
self.search_box.textChanged.connect(self.contact_search_model.setFilterFixedString)
self.search_box.returnPressed.connect(self._SH_SearchBoxReturnPressed)
self.search_box.shortcut.activated.connect(self.search_box.setFocus)
self.audio_call_button.clicked.connect(self._SH_AudioCallButtonClicked)
self.contact_list.doubleClicked.connect(self._SH_ContactDoubleClicked) # activated is emitted on single click
self.search_list.selectionModel().selectionChanged.connect(self._SH_SearchListSelectionChanged)
self.search_list.doubleClicked.connect(self._SH_ContactDoubleClicked) # activated is emitted on single click
self.search_box.returnPressed.connect(self._SH_SearchBoxReturnPressed)
self.server_tools_account_model.rowsInserted.connect(self._SH_ServerToolsAccountModelChanged)
self.server_tools_account_model.rowsRemoved.connect(self._SH_ServerToolsAccountModelChanged)
self.session_model.sessionAdded.connect(self._SH_SessionModelAddedSession)
self.session_model.structureChanged.connect(self._SH_SessionModelChangedStructure)
self.hangup_all_button.clicked.connect(self._SH_HangupAllButtonClicked)
self.conference_button.makeConference.connect(self._SH_MakeConference)
self.conference_button.breakConference.connect(self._SH_BreakConference)
self.mute_button.clicked.connect(self._SH_MuteButtonClicked)
self.search_box.shortcut = QShortcut(self.search_box)
self.search_box.shortcut.setKey('CTRL+F')
self.search_box.shortcut.activated.connect(self.search_box.setFocus)
self.silent_button.clicked.connect(self._SH_SilentButtonClicked)
self.status.activated[int].connect(self._SH_StatusChanged)
self.switch_view_button.viewChanged.connect(self._SH_SwitchViewButtonChangedView)
# menu actions
# Blink menu actions
self.about_action.triggered.connect(self.about_panel.show)
self.add_account_action.triggered.connect(self.add_account_dialog.open_for_add)
self.mute_action.triggered.connect(self._SH_MuteButtonClicked)
self.redial_action.triggered.connect(self._SH_RedialActionTriggered)
self.silent_action.triggered.connect(self._SH_SilentButtonClicked)
self.quit_action.triggered.connect(self.close)
# menu actions that link to external web pages
self.donate_action.triggered.connect(partial(QDesktopServices.openUrl, QUrl(u'http://icanblink.com/payments.phtml')))
self.add_account_action.triggered.connect(self.add_account_dialog.open_for_add)
self.help_action.triggered.connect(partial(QDesktopServices.openUrl, QUrl(u'http://icanblink.com/help-qt.phtml')))
self.release_notes_action.triggered.connect(partial(QDesktopServices.openUrl, QUrl(u'http://icanblink.com/changelog-qt.phtml')))
self.quit_action.triggered.connect(self.close)
self.idle_status_index = 0
# Audio menu actions
self.mute_action.triggered.connect(self._SH_MuteButtonClicked)
self.silent_action.triggered.connect(self._SH_SilentButtonClicked)
self.output_devices_group.triggered.connect(self._AH_AudioOutputDeviceChanged)
self.input_devices_group.triggered.connect(self._AH_AudioInputDeviceChanged)
self.alert_devices_group.triggered.connect(self._AH_AudioAlertDeviceChanged)
self.output_devices_group = QActionGroup(self)
self.input_devices_group = QActionGroup(self)
self.alert_devices_group = QActionGroup(self)
self.output_devices_group.triggered.connect(self._SH_AudioOutputDeviceChanged)
self.input_devices_group.triggered.connect(self._SH_AudioInputDeviceChanged)
self.alert_devices_group.triggered.connect(self._SH_AudioAlertDeviceChanged)
# History menu actions
self.redial_action.triggered.connect(self._AH_RedialActionTriggered)
notification_center = NotificationCenter()
notification_center.add_observer(self, name='SIPApplicationWillStart')
# Tools menu actions
self.sip_server_settings_action.triggered.connect(self._AH_SIPServerSettings)
self.search_for_people_action.triggered.connect(self._AH_SearchForPeople)
self.history_on_server_action.triggered.connect(self._AH_HistoryOnServer)
self.contact_model.load()
def setupUi(self):
super(MainWindow, self).setupUi(self)
self.search_box.shortcut = QShortcut(self.search_box)
self.search_box.shortcut.setKey('CTRL+F')
self.output_devices_group = QActionGroup(self)
self.input_devices_group = QActionGroup(self)
self.alert_devices_group = QActionGroup(self)
# adjust search box height depending on theme as the value set in designer isn't suited for all themes
search_box = self.search_box
option = QStyleOptionFrameV2()
......@@ -179,6 +195,7 @@ class MainWindow(base_class, ui_class):
self.about_panel.close()
self.add_account_dialog.close()
self.contact_editor_dialog.close()
self.server_tools_window.close()
def set_user_icon(self, image_file_name):
pixmap = QPixmap(32, 32)
......@@ -258,6 +275,41 @@ class MainWindow(base_class, ui_class):
account.enabled = enabled
account.save()
def _AH_AudioAlertDeviceChanged(self, action):
settings = SIPSimpleSettings()
settings.audio.alert_device = action.data().toPyObject()
call_in_auxiliary_thread(settings.save)
def _AH_AudioInputDeviceChanged(self, action):
settings = SIPSimpleSettings()
settings.audio.input_device = action.data().toPyObject()
call_in_auxiliary_thread(settings.save)
def _AH_AudioOutputDeviceChanged(self, action):
settings = SIPSimpleSettings()
settings.audio.output_device = action.data().toPyObject()
call_in_auxiliary_thread(settings.save)
def _AH_RedialActionTriggered(self):
session_manager = SessionManager()
if session_manager.last_dialed_uri is not None:
session_manager.start_call(None, unicode(session_manager.last_dialed_uri))
def _AH_SIPServerSettings(self, checked):
account = self.identity.itemData(self.identity.currentIndex()).toPyObject().account
account = account if account is not BonjourAccount() and account.server.settings_url else None
self.server_tools_window.open_settings_page(account)
def _AH_SearchForPeople(self, checked):
account = self.identity.itemData(self.identity.currentIndex()).toPyObject().account
account = account if account is not BonjourAccount() and account.server.settings_url else None
self.server_tools_window.open_search_for_people_page(account)
def _AH_HistoryOnServer(self, checked):
account = self.identity.itemData(self.identity.currentIndex()).toPyObject().account
account = account if account is not BonjourAccount() and account.server.settings_url else None
self.server_tools_window.open_history_page(account)
def _SH_AddContactButtonClicked(self, clicked):
model = self.contact_model
selected_items = ((index.row(), model.data(index)) for index in self.contact_list.selectionModel().selectedIndexes())
......@@ -280,21 +332,6 @@ class MainWindow(base_class, ui_class):
session_manager = SessionManager()
session_manager.start_call(name, address, contact=contact, account=BonjourAccount() if isinstance(contact, BonjourNeighbour) else None)
def _SH_AudioAlertDeviceChanged(self, action):
settings = SIPSimpleSettings()
settings.audio.alert_device = action.data().toPyObject()
call_in_auxiliary_thread(settings.save)
def _SH_AudioInputDeviceChanged(self, action):
settings = SIPSimpleSettings()
settings.audio.input_device = action.data().toPyObject()
call_in_auxiliary_thread(settings.save)
def _SH_AudioOutputDeviceChanged(self, action):
settings = SIPSimpleSettings()
settings.audio.output_device = action.data().toPyObject()
call_in_auxiliary_thread(settings.save)
def _SH_BreakConference(self):
active_session = self.session_model.data(self.session_list.selectionModel().selectedIndexes()[0])
self.session_model.breakConference(active_session.conference)
......@@ -367,11 +404,6 @@ class MainWindow(base_class, ui_class):
self.mute_button.setChecked(muted)
SIPApplication.voice_audio_bridge.mixer.muted = muted
def _SH_RedialActionTriggered(self):
session_manager = SessionManager()
if session_manager.last_dialed_uri is not None:
session_manager.start_call(None, unicode(session_manager.last_dialed_uri))
def _SH_SearchBoxReturnPressed(self):
address = unicode(self.search_box.text())
if address:
......@@ -399,6 +431,12 @@ class MainWindow(base_class, ui_class):
selected_items = self.search_list.selectionModel().selectedIndexes()
self.enable_call_buttons(account_manager.default_account is not None and len(selected_items)<=1)
def _SH_ServerToolsAccountModelChanged(self, parent_index, start, end):
server_tools_enabled = self.server_tools_account_model.rowCount() > 0
self.sip_server_settings_action.setEnabled(server_tools_enabled)
self.search_for_people_action.setEnabled(server_tools_enabled)
self.history_on_server_action.setEnabled(server_tools_enabled)
def _SH_SessionListSelectionChanged(self, selected, deselected):
selected_indexes = selected.indexes()
active_session = self.session_model.data(selected_indexes[0]) if selected_indexes else Null
......
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>server_window</class>
<widget class="QWidget" name="server_window">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>700</width>
<height>670</height>
</rect>
</property>
<property name="windowTitle">
<string>Blink Server Tools</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>icons/blink48.png</normaloff>icons/blink48.png</iconset>
</property>
<layout class="QVBoxLayout" name="window_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="account_selector" native="true">
<layout class="QHBoxLayout" name="account_layout">
<property name="spacing">
<number>3</number>
</property>
<property name="margin">
<number>5</number>
</property>
<item>
<widget class="ToolButton" name="account_button">
<property name="minimumSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="focusPolicy">
<enum>Qt::NoFocus</enum>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset>
<normaloff>icons/default-avatar.png</normaloff>icons/default-avatar.png</iconset>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="popupMode">
<enum>QToolButton::InstantPopup</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="account_label">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="styleSheet">
<string>QLabel {
background: #2060c0;
border-style: outset;
border-width: 1px;
border-radius: 4px;
border-color: #104080;
margin-top: 4px;
margin-bottom: 4px;
padding-left: 2px;
padding-right: 2px;
color: white;
}
</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tab_widget">
<property name="currentIndex">
<number>0</number>
</property>
<property name="documentMode">
<bool>true</bool>
</property>
<property name="tabsClosable">
<bool>true</bool>
</property>
<property name="movable">
<bool>true</bool>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Tab 1</string>
</attribute>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ToolButton</class>
<extends>QToolButton</extends>
<header>blink.widgets.buttons</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