1
0
Fork 0
mirror of synced 2024-06-26 10:11:19 +12:00

Merge pull request #139 from Dummerle/add_ubi_activation

Add Ubisoft activation in legendary settings
This commit is contained in:
Dummerle 2021-12-13 22:12:54 +01:00 committed by GitHub
commit f12c633211
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 314 additions and 130 deletions

View file

@ -22,27 +22,28 @@ AppDir:
version: 1.7.0 version: 1.7.0
exec: usr/bin/python3 exec: usr/bin/python3
exec_args: $APPDIR/usr/src/__main__.py $@ exec_args: $APPDIR/usr/src/__main__.py $@
apt:d apt:
arch: amd64 arch: amd64
allow_unauthenticated: true allow_unauthenticated: true
sources: sources:
- sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy main restricted - sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy main restricted
- sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy-updates main restricted - sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy-updates main restricted
- sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy universe - sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy universe
- sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy-updates universe - sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy-updates universe
- sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy multiverse - sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy multiverse
- sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy-updates multiverse - sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy-updates multiverse
- sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy-backports main restricted - sourceline: deb http://de.archive.ubuntu.com/ubuntu/ groovy-backports main restricted
universe multiverse universe multiverse
- sourceline: deb http://security.ubuntu.com/ubuntu groovy-security main restricted - sourceline: deb http://security.ubuntu.com/ubuntu groovy-security main restricted
- sourceline: deb http://security.ubuntu.com/ubuntu groovy-security universe - sourceline: deb http://security.ubuntu.com/ubuntu groovy-security universe
- sourceline: deb http://security.ubuntu.com/ubuntu groovy-security multiverse - sourceline: deb http://security.ubuntu.com/ubuntu groovy-security multiverse
include:
- python3 include:
- python3-distutils - python3
- python3-pyqt5 - python3-distutils
- python3-psutil - python3-pyqt5
- python3-requests - python3-psutil
- python3-requests
runtime: runtime:
env: env:

View file

@ -78,6 +78,6 @@ if __name__ == '__main__':
# insert raw legendary submodule # insert raw legendary submodule
sys.path.insert(0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary")) sys.path.insert(0, os.path.join(pathlib.Path(__file__).parent.absolute(), "legendary"))
# insert source directory # insert source directory
sys.path.insert(0, str(pathlib.Path(__file__).parents[2].absolute())) sys.path.insert(0, str(pathlib.Path(__file__).parents[1].absolute()))
main() main()

View file

@ -5,6 +5,7 @@ from typing import Tuple
from PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox from PyQt5.QtWidgets import QSizePolicy, QWidget, QFileDialog, QMessageBox
import rare.shared as shared import rare.shared as shared
from rare.components.tabs.settings.ubisoft_activation import UbiActivationHelper
from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings from rare.ui.components.tabs.settings.legendary import Ui_LegendarySettings
from rare.utils.extra_widgets import PathEdit, IndicatorLineEdit from rare.utils.extra_widgets import PathEdit, IndicatorLineEdit
from rare.utils.utils import get_size from rare.utils.utils import get_size
@ -59,6 +60,8 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
) )
self.locale_layout.addWidget(self.locale_edit) self.locale_layout.addWidget(self.locale_edit)
self.ubi_helper = UbiActivationHelper(self.ubisoft_gb)
@staticmethod @staticmethod
def locale_edit_cb(text: str) -> Tuple[bool, str]: def locale_edit_cb(text: str) -> Tuple[bool, str]:
if text: if text:
@ -111,7 +114,7 @@ class LegendarySettings(QWidget, Ui_LegendarySettings):
self.core.lgd.config.set("Legendary", "disable_https", str(bool(checked)).lower()) self.core.lgd.config.set("Legendary", "disable_https", str(bool(checked)).lower())
self.core.lgd.save_config() self.core.lgd.save_config()
def cleanup(self, keep_manifests): def cleanup(self, keep_manifests: bool):
before = self.core.lgd.get_dir_size() before = self.core.lgd.get_dir_size()
logger.debug('Removing app metadata...') logger.debug('Removing app metadata...')
app_names = set(g.app_name for g in self.core.get_assets(update_assets=False)) app_names = set(g.app_name for g in self.core.get_assets(update_assets=False))

View file

@ -0,0 +1,181 @@
import time
import webbrowser
from logging import getLogger
from PyQt5.QtCore import QObject, pyqtSignal, QRunnable, QThreadPool, QSize
from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QSizePolicy, QPushButton
from qtawesome import icon
from legendary.models.game import Game
from rare import shared
logger = getLogger("Ubisoft")
class Signals(QObject):
worker_finished = pyqtSignal(set, set, str)
linked = pyqtSignal(str)
class UbiGetInfoWorker(QRunnable):
def __init__(self):
super(UbiGetInfoWorker, self).__init__()
self.signals = Signals()
self.setAutoDelete(True)
def run(self) -> None:
try:
external_auths = shared.core.egs.get_external_auths()
for ext_auth in external_auths:
if ext_auth['type'] != 'ubisoft':
continue
ubi_account_id = ext_auth['externalAuthId']
break
else:
self.signals.worker_finished.emit(set(), set(), "")
return
uplay_keys = shared.core.egs.store_get_uplay_codes()
key_list = uplay_keys['data']['PartnerIntegration']['accountUplayCodes']
redeemed = {k['gameId'] for k in key_list if k['redeemedOnUplay']}
entitlements = shared.core.egs.get_user_entitlements()
entitlements = {i['entitlementName'] for i in entitlements}
self.signals.worker_finished.emit(redeemed, entitlements, ubi_account_id)
except Exception as e:
logger.error(str(e))
self.signals.worker_finished.emit(set(), set(), "error")
class UbiConnectWorker(QRunnable):
def __init__(self, ubi_account_id, partner_link_id):
super(UbiConnectWorker, self).__init__()
self.signals = Signals()
self.setAutoDelete(True)
self.ubi_account_id = ubi_account_id
self.partner_link_id = partner_link_id
def run(self) -> None:
if not self.ubi_account_id: # debug
time.sleep(2)
self.signals.linked.emit("")
return
try:
shared.core.egs.store_claim_uplay_code(self.ubi_account_id, self.partner_link_id)
shared.core.egs.store_redeem_uplay_codes(self.ubi_account_id)
except Exception as e:
self.signals.linked.emit(str(e))
return
else:
self.signals.linked.emit("")
class UbiLinkWidget(QWidget):
def __init__(self, game: Game, ubi_account_id):
super(UbiLinkWidget, self).__init__()
self.setLayout(QHBoxLayout())
self.game = game
self.ubi_account_id = ubi_account_id
self.title_label = QLabel(game.app_title)
self.layout().addWidget(self.title_label)
self.ok_indicator = QLabel()
self.ok_indicator.setPixmap(icon("fa.info-circle", color="grey").pixmap(20, 20))
self.ok_indicator.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
self.layout().addWidget(self.ok_indicator)
self.link_button = QPushButton(self.tr("Redeem to Ubisoft") + ": Test" if shared.args.debug else "")
self.layout().addWidget(self.link_button)
self.link_button.clicked.connect(self.activate)
def activate(self):
self.link_button.setDisabled(True)
# self.ok_indicator.setPixmap(icon("mdi.loading", color="grey").pixmap(20, 20))
self.ok_indicator.setPixmap(icon("mdi.transit-connection-horizontal", color="grey").pixmap(20, 20))
if shared.args.debug:
worker = UbiConnectWorker(None, None)
else:
worker = UbiConnectWorker(self.ubi_account_id, self.game.partner_link_id)
worker.signals.linked.connect(self.worker_finished)
QThreadPool.globalInstance().start(worker)
def worker_finished(self, error):
if not error:
self.ok_indicator.setPixmap(icon("ei.ok-circle", color="green").pixmap(QSize(20, 20)))
self.link_button.setDisabled(True)
self.link_button.setText(self.tr("Already activated"))
else:
self.ok_indicator.setPixmap(icon("fa.info-circle", color="red").pixmap(QSize(20, 20)))
self.ok_indicator.setToolTip(error)
self.link_button.setText(self.tr("Try again"))
self.link_button.setDisabled(False)
class UbiActivationHelper(QObject):
def __init__(self, widget: QWidget):
super(UbiActivationHelper, self).__init__()
self.widget = widget
self.core = shared.core
self.thread_pool = QThreadPool.globalInstance()
worker = UbiGetInfoWorker()
worker.signals.worker_finished.connect(self.show_ubi_games)
self.thread_pool.start(worker)
def show_ubi_games(self, redeemed: set, entitlements: set, ubi_account_id: str):
if not redeemed and ubi_account_id != "error":
logger.error('No linked ubisoft account found! Link your accounts via your browser and try again.')
self.widget.layout().addWidget(
QLabel(self.tr("Your account is not linked with Ubisoft. Please link your account first")))
open_browser_button = QPushButton(self.tr("Open link page"))
open_browser_button.clicked.connect(lambda: webbrowser.open("https://www.epicgames.com/id/link/ubisoft"))
self.widget.layout().addWidget(open_browser_button)
return
elif ubi_account_id == "error":
self.widget.layout().addWidget(QLabel(self.tr("An error occurred")))
return
games = self.core.get_game_list(False)
uplay_games = []
activated = 0
for game in games:
for dlc_data in game.metadata.get('dlcItemList', []):
if dlc_data['entitlementName'] not in entitlements:
continue
try:
app_name = dlc_data['releaseInfo'][0]['appId']
except (IndexError, KeyError):
app_name = 'unknown'
dlc_game = Game(app_name=app_name, app_title=dlc_data['title'], metadata=dlc_data)
if dlc_game.partner_link_type != 'ubisoft':
continue
if dlc_game.partner_link_id in redeemed:
continue
uplay_games.append(dlc_game)
if game.partner_link_type != "ubisoft":
continue
if game.partner_link_id in redeemed:
activated += 1
continue
uplay_games.append(game)
if not uplay_games:
if activated >= 1:
self.widget.layout().addWidget(
QLabel(self.tr("All your Ubisoft games have already been activated")))
else:
self.widget.layout().addWidget(QLabel(self.tr("You don't own any Ubisoft games")))
if shared.args.debug:
widget = UbiLinkWidget(Game(app_name="Test", app_title="This is a test game"), ubi_account_id)
self.widget.layout().addWidget(widget)
return
logger.info(f'Found {len(uplay_games)} game(s) to redeem')
for game in uplay_games:
widget = UbiLinkWidget(game, ubi_account_id)
self.widget.layout().addWidget(widget)

@ -1 +1 @@
Subproject commit ab7d209d5318f862d20081724383f523b6de4c7b Subproject commit d371c0b3c48a422ff7e6c966af649a1502300c2d

View file

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/legendary.ui' # Form implementation generated from reading ui file 'rare/ui/components/tabs/settings/legendary.ui'
# #
# Created by: PyQt5 UI code generator 5.15.4 # Created by: PyQt5 UI code generator 5.15.6
# #
# WARNING: Any manual changes made to this file will be lost when pyuic5 is # WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing. # run again. Do not edit this file unless you know what you are doing.
@ -14,10 +14,39 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_LegendarySettings(object): class Ui_LegendarySettings(object):
def setupUi(self, LegendarySettings): def setupUi(self, LegendarySettings):
LegendarySettings.setObjectName("LegendarySettings") LegendarySettings.setObjectName("LegendarySettings")
LegendarySettings.resize(552, 268) LegendarySettings.resize(564, 374)
LegendarySettings.setWindowTitle("LegendarySettings") LegendarySettings.setWindowTitle("LegendarySettings")
self.settings_layout = QtWidgets.QHBoxLayout(LegendarySettings) self.gridLayout = QtWidgets.QGridLayout(LegendarySettings)
self.settings_layout.setObjectName("settings_layout") self.gridLayout.setObjectName("gridLayout")
self.right_layout = QtWidgets.QVBoxLayout()
self.right_layout.setObjectName("right_layout")
self.locale_group = QtWidgets.QGroupBox(LegendarySettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.locale_group.sizePolicy().hasHeightForWidth())
self.locale_group.setSizePolicy(sizePolicy)
self.locale_group.setObjectName("locale_group")
self.locale_layout = QtWidgets.QVBoxLayout(self.locale_group)
self.locale_layout.setObjectName("locale_layout")
self.right_layout.addWidget(self.locale_group)
self.cleanup_group = QtWidgets.QGroupBox(LegendarySettings)
self.cleanup_group.setObjectName("cleanup_group")
self.cleanup_layout = QtWidgets.QVBoxLayout(self.cleanup_group)
self.cleanup_layout.setObjectName("cleanup_layout")
self.clean_keep_manifests_button = QtWidgets.QPushButton(self.cleanup_group)
self.clean_keep_manifests_button.setObjectName("clean_keep_manifests_button")
self.cleanup_layout.addWidget(self.clean_keep_manifests_button)
self.clean_button = QtWidgets.QPushButton(self.cleanup_group)
self.clean_button.setObjectName("clean_button")
self.cleanup_layout.addWidget(self.clean_button)
self.right_layout.addWidget(self.cleanup_group)
self.gridLayout.addLayout(self.right_layout, 0, 1, 1, 1)
self.ubisoft_gb = QtWidgets.QGroupBox(LegendarySettings)
self.ubisoft_gb.setObjectName("ubisoft_gb")
self.verticalLayout = QtWidgets.QVBoxLayout(self.ubisoft_gb)
self.verticalLayout.setObjectName("verticalLayout")
self.gridLayout.addWidget(self.ubisoft_gb, 1, 0, 1, 2)
self.left_layout = QtWidgets.QVBoxLayout() self.left_layout = QtWidgets.QVBoxLayout()
self.left_layout.setObjectName("left_layout") self.left_layout.setObjectName("left_layout")
self.install_dir_group = QtWidgets.QGroupBox(LegendarySettings) self.install_dir_group = QtWidgets.QGroupBox(LegendarySettings)
@ -96,41 +125,20 @@ class Ui_LegendarySettings(object):
self.disable_https_check.setObjectName("disable_https_check") self.disable_https_check.setObjectName("disable_https_check")
self.download_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.disable_https_check) self.download_layout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.disable_https_check)
self.left_layout.addWidget(self.download_group) self.left_layout.addWidget(self.download_group)
self.gridLayout.addLayout(self.left_layout, 0, 0, 1, 1)
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.left_layout.addItem(spacerItem) self.gridLayout.addItem(spacerItem, 2, 0, 1, 2)
self.settings_layout.addLayout(self.left_layout)
self.right_layout = QtWidgets.QVBoxLayout()
self.right_layout.setObjectName("right_layout")
self.locale_group = QtWidgets.QGroupBox(LegendarySettings)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.locale_group.sizePolicy().hasHeightForWidth())
self.locale_group.setSizePolicy(sizePolicy)
self.locale_group.setObjectName("locale_group")
self.locale_layout = QtWidgets.QVBoxLayout(self.locale_group)
self.locale_layout.setObjectName("locale_layout")
self.right_layout.addWidget(self.locale_group)
self.cleanup_group = QtWidgets.QGroupBox(LegendarySettings)
self.cleanup_group.setObjectName("cleanup_group")
self.cleanup_layout = QtWidgets.QVBoxLayout(self.cleanup_group)
self.cleanup_layout.setObjectName("cleanup_layout")
self.clean_keep_manifests_button = QtWidgets.QPushButton(self.cleanup_group)
self.clean_keep_manifests_button.setObjectName("clean_keep_manifests_button")
self.cleanup_layout.addWidget(self.clean_keep_manifests_button)
self.clean_button = QtWidgets.QPushButton(self.cleanup_group)
self.clean_button.setObjectName("clean_button")
self.cleanup_layout.addWidget(self.clean_button)
self.right_layout.addWidget(self.cleanup_group)
spacerItem1 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.right_layout.addItem(spacerItem1)
self.settings_layout.addLayout(self.right_layout)
self.retranslateUi(LegendarySettings) self.retranslateUi(LegendarySettings)
QtCore.QMetaObject.connectSlotsByName(LegendarySettings) QtCore.QMetaObject.connectSlotsByName(LegendarySettings)
def retranslateUi(self, LegendarySettings): def retranslateUi(self, LegendarySettings):
_translate = QtCore.QCoreApplication.translate _translate = QtCore.QCoreApplication.translate
self.locale_group.setTitle(_translate("LegendarySettings", "Locale"))
self.cleanup_group.setTitle(_translate("LegendarySettings", "Cleanup"))
self.clean_keep_manifests_button.setText(_translate("LegendarySettings", "Clean, but keep manifests"))
self.clean_button.setText(_translate("LegendarySettings", "Remove everything"))
self.ubisoft_gb.setTitle(_translate("LegendarySettings", "Link Ubisoft Games"))
self.install_dir_group.setTitle(_translate("LegendarySettings", "Default Installation Directory")) self.install_dir_group.setTitle(_translate("LegendarySettings", "Default Installation Directory"))
self.download_group.setTitle(_translate("LegendarySettings", "Download Settings")) self.download_group.setTitle(_translate("LegendarySettings", "Download Settings"))
self.max_workers_label.setText(_translate("LegendarySettings", "Max Workers")) self.max_workers_label.setText(_translate("LegendarySettings", "Max Workers"))
@ -141,10 +149,6 @@ class Ui_LegendarySettings(object):
self.preferred_cdn_label.setText(_translate("LegendarySettings", "Preferred CDN")) self.preferred_cdn_label.setText(_translate("LegendarySettings", "Preferred CDN"))
self.preferred_cdn_line.setPlaceholderText(_translate("LegendarySettings", "Default")) self.preferred_cdn_line.setPlaceholderText(_translate("LegendarySettings", "Default"))
self.disable_https_label.setText(_translate("LegendarySettings", "Disable HTTPS")) self.disable_https_label.setText(_translate("LegendarySettings", "Disable HTTPS"))
self.locale_group.setTitle(_translate("LegendarySettings", "Locale"))
self.cleanup_group.setTitle(_translate("LegendarySettings", "Cleanup"))
self.clean_keep_manifests_button.setText(_translate("LegendarySettings", "Clean, but keep manifests"))
self.clean_button.setText(_translate("LegendarySettings", "Remove everything"))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -6,15 +6,64 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>552</width> <width>564</width>
<height>268</height> <height>374</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string notr="true">LegendarySettings</string> <string notr="true">LegendarySettings</string>
</property> </property>
<layout class="QHBoxLayout" name="settings_layout"> <layout class="QGridLayout" name="gridLayout">
<item> <item row="0" column="1">
<layout class="QVBoxLayout" name="right_layout">
<item>
<widget class="QGroupBox" name="locale_group">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="title">
<string>Locale</string>
</property>
<layout class="QVBoxLayout" name="locale_layout"/>
</widget>
</item>
<item>
<widget class="QGroupBox" name="cleanup_group">
<property name="title">
<string>Cleanup</string>
</property>
<layout class="QVBoxLayout" name="cleanup_layout">
<item>
<widget class="QPushButton" name="clean_keep_manifests_button">
<property name="text">
<string>Clean, but keep manifests</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clean_button">
<property name="text">
<string>Remove everything</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="1" column="0" colspan="2">
<widget class="QGroupBox" name="ubisoft_gb">
<property name="title">
<string>Link Ubisoft Games</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout"/>
</widget>
</item>
<item row="0" column="0">
<layout class="QVBoxLayout" name="left_layout"> <layout class="QVBoxLayout" name="left_layout">
<item> <item>
<widget class="QGroupBox" name="install_dir_group"> <widget class="QGroupBox" name="install_dir_group">
@ -160,74 +209,20 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<spacer name="left_vspacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout> </layout>
</item> </item>
<item> <item row="2" column="0" colspan="2">
<layout class="QVBoxLayout" name="right_layout"> <spacer name="verticalSpacer">
<item> <property name="orientation">
<widget class="QGroupBox" name="locale_group"> <enum>Qt::Vertical</enum>
<property name="sizePolicy"> </property>
<sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <property name="sizeHint" stdset="0">
<horstretch>0</horstretch> <size>
<verstretch>0</verstretch> <width>20</width>
</sizepolicy> <height>40</height>
</property> </size>
<property name="title"> </property>
<string>Locale</string> </spacer>
</property>
<layout class="QVBoxLayout" name="locale_layout"/>
</widget>
</item>
<item>
<widget class="QGroupBox" name="cleanup_group">
<property name="title">
<string>Cleanup</string>
</property>
<layout class="QVBoxLayout" name="cleanup_layout">
<item>
<widget class="QPushButton" name="clean_keep_manifests_button">
<property name="text">
<string>Clean, but keep manifests</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="clean_button">
<property name="text">
<string>Remove everything</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="right_vspacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item> </item>
</layout> </layout>
</widget> </widget>