Auto update check (#1235)

* init commit

* bug fix

* call slot of null object bug fix

* delete extra disconnect() func

* change api and add doc

* run astyle

* some improvements

* memory leak fix

* add check on start checkbox

* add checkbox to about page

* serve version check reply using lambda instead of slot

* fix grammar mistakes

* more docs

* save some lines

* change button text

* astyle

* change message text

* dont use QApplication pointer as a parent for network manager

* proper deletion of QNetworkReply*

* VersionChecker -> UpdateWorker

* windows dll hack

* after rebase fix

* some improvements

* better determination of arch

* more docs

* improvements

* add UpdateWorker::showUpdateDialog

* remove odd condition

* more improvements

* fix windows bug

* make dialog non-blocking

* change text in download progress dialog

* bug fix

* remove debug conditions

* change docs format
This commit is contained in:
optizone 2019-03-09 16:11:39 +03:00 committed by Florian Märkl
parent d4a6b031ff
commit 3fed97ad86
13 changed files with 550 additions and 137 deletions

View File

@ -312,7 +312,8 @@ SOURCES += \
common/PythonManager.cpp \
plugins/PluginManager.cpp \
common/BasicBlockHighlighter.cpp \
dialogs/LinkTypeDialog.cpp
dialogs/LinkTypeDialog.cpp \
common/UpdateWorker.cpp
HEADERS += \
core/Cutter.h \
@ -429,6 +430,7 @@ HEADERS += \
common/PythonManager.h \
plugins/PluginManager.h \
common/BasicBlockHighlighter.h \
common/UpdateWorker.h \
dialogs/LinkTypeDialog.h
FORMS += \

View File

@ -23,6 +23,10 @@
#include <cstdlib>
#ifdef Q_OS_WIN
#include <QtNetwork/QtNetwork>
#endif // Q_OS_WIN
CutterApplication::CutterApplication(int &argc, char **argv) : QApplication(argc, argv)
{
// Setup application information
@ -32,6 +36,13 @@ CutterApplication::CutterApplication(int &argc, char **argv) : QApplication(argc
setLayoutDirection(Qt::LeftToRight);
// WARN!!! Put initialization code below this line. Code above this line is mandatory to be run First
#ifdef Q_OS_WIN
// Hack to force Cutter load internet connection related DLL's
QSslSocket s;
s.sslConfiguration();
#endif // Q_OS_WIN
// Load translations
if (!loadTranslations()) {
qWarning() << "Cannot load translations";

View File

@ -1,6 +1,8 @@
#include "CutterApplication.h"
#include "core/MainWindow.h"
#include "common/UpdateWorker.h"
#include "CutterConfig.h"
/**
* @brief Migrate Settings used before Cutter 1.8
@ -34,6 +36,18 @@ int main(int argc, char *argv[])
CutterApplication a(argc, argv);
if (Config()->getAutoUpdateEnabled()) {
UpdateWorker *updateWorker = new UpdateWorker;
QObject::connect(updateWorker, &UpdateWorker::checkComplete,
[=](const QString & version, const QString & error) {
if (error == "" && version != CUTTER_VERSION_FULL) {
updateWorker->showUpdateDialog(true);
}
updateWorker->deleteLater();
});
updateWorker->checkCurrentVersion(7000);
}
int ret = a.exec();
return ret;

View File

@ -136,6 +136,16 @@ void Configuration::resetAll()
emit fontsUpdated();
}
bool Configuration::getAutoUpdateEnabled() const
{
return s.value("autoUpdateEnabled", false).toBool();
}
void Configuration::setAutoUpdateEnabled(bool au)
{
s.setValue("autoUpdateEnabled", au);
}
/**
* @brief get the current Locale set in Cutter's user configuration
* @return a QLocale object describes user's current locale

View File

@ -46,6 +46,10 @@ public:
void resetAll();
// Auto update
bool getAutoUpdateEnabled() const;
void setAutoUpdateEnabled(bool au);
// Languages
QLocale getCurrLocale() const;
void setLocale(const QLocale &l);
@ -58,7 +62,7 @@ public:
// Colors
bool windowColorIsDark();
void setLastThemeOf(const CutterQtTheme &currQtTheme, const QString& theme);
void setLastThemeOf(const CutterQtTheme &currQtTheme, const QString &theme);
QString getLastThemeOf(const CutterQtTheme &currQtTheme) const;
const QColor getColor(const QString &name) const;
void setTheme(int theme);

211
src/common/UpdateWorker.cpp Normal file
View File

@ -0,0 +1,211 @@
#include "UpdateWorker.h"
#include <QUrl>
#include <QFile>
#include <QTimer>
#include <QEventLoop>
#include <QDataStream>
#include <QJsonObject>
#include <QApplication>
#include <QJsonDocument>
#include <QDesktopServices>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkRequest>
#include <QProgressDialog>
#include <QPushButton>
#include <QFileDialog>
#include <QMessageBox>
#include "common/Configuration.h"
#include "CutterConfig.h"
UpdateWorker::UpdateWorker(QObject *parent) :
QObject(parent), latestVersion(""), pending(false)
{
connect(&t, &QTimer::timeout, [this]() {
if (pending) {
disconnect(checkReply, nullptr, this, nullptr);
checkReply->close();
checkReply->deleteLater();
emit checkComplete("", tr("Time limit exceeded during version check. Please check your "
"internet connection and try again."));
}
});
}
void UpdateWorker::checkCurrentVersion(time_t timeoutMs)
{
QUrl url("https://api.github.com/repos/radareorg/cutter/releases/latest");
QNetworkRequest request;
request.setUrl(url);
t.setInterval(timeoutMs);
t.setSingleShot(true);
t.start();
checkReply = nm.get(request);
connect(checkReply, &QNetworkReply::finished,
this, &UpdateWorker::serveVersionCheckReply);
pending = true;
}
void UpdateWorker::download(QString filename, QString version)
{
downloadFile.setFileName(filename);
downloadFile.open(QIODevice::WriteOnly);
QNetworkRequest request;
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
QUrl url(QString("https://github.com/radareorg/cutter/releases/"
"download/v%1/%2").arg(version).arg(getRepositoryFileName()));
request.setUrl(url);
downloadReply = nm.get(request);
connect(downloadReply, &QNetworkReply::downloadProgress,
this, &UpdateWorker::process);
connect(downloadReply, &QNetworkReply::finished,
this, &UpdateWorker::serveDownloadFinish);
}
void UpdateWorker::showUpdateDialog(bool showDontCheckForUpdatesButton)
{
QMessageBox mb;
mb.setWindowTitle(tr("Version control"));
mb.setText(tr("There is an update available for Cutter.<br/>")
+ "<b>" + tr("Current version:") + "</b> " CUTTER_VERSION_FULL "<br/>"
+ "<b>" + tr("Latest version:") + "</b> " + latestVersion + "<br/><br/>"
+ tr("For update, please check the link:<br/>")
+ QString("<a href=\"https://github.com/radareorg/cutter/releases/tag/v%1\">"
"https://github.com/radareorg/cutter/releases/tag/v%1</a><br/>").arg(latestVersion)
+ tr("or click \"Download\" to download latest version of Cutter."));
if (showDontCheckForUpdatesButton) {
mb.setStandardButtons(QMessageBox::Save | QMessageBox::Reset | QMessageBox::Ok);
mb.button(QMessageBox::Reset)->setText(tr("Don't check for updates"));
} else {
mb.setStandardButtons(QMessageBox::Save | QMessageBox::Ok);
}
mb.button(QMessageBox::Save)->setText(tr("Download"));
mb.setDefaultButton(QMessageBox::Ok);
int ret = mb.exec();
if (ret == QMessageBox::Reset) {
Config()->setAutoUpdateEnabled(false);
} else if (ret == QMessageBox::Save) {
QString fullFileName =
QFileDialog::getSaveFileName(nullptr,
tr("Choose directory for downloading"),
QStandardPaths::writableLocation(QStandardPaths::HomeLocation) +
QDir::separator() + getRepositoryFileName(),
QString("%1 (*.%1)").arg(getRepositeryExt()));
if (fullFileName != "") {
QProgressDialog progressDial(tr("Downloading update..."),
tr("Cancel"),
0, 100);
connect(this, &UpdateWorker::downloadProcess,
[&progressDial](size_t curr, size_t total) {
progressDial.setValue(100.0f * curr / total);
});
connect(&progressDial, &QProgressDialog::canceled,
this, &UpdateWorker::abortDownload);
connect(this, &UpdateWorker::downloadFinished,
&progressDial, &QProgressDialog::cancel);
connect(this, &UpdateWorker::downloadFinished,
[](QString filePath){
QMessageBox info(QMessageBox::Information,
tr("Download finished!"),
tr("Latest version of Cutter was succesfully downloaded!"),
QMessageBox::Yes | QMessageBox::Open | QMessageBox::Ok,
nullptr);
info.button(QMessageBox::Open)->setText(tr("Open file"));
info.button(QMessageBox::Yes)->setText(tr("Open download folder"));
int r = info.exec();
if (r == QMessageBox::Open) {
QDesktopServices::openUrl(filePath);
} else if (r == QMessageBox::Yes) {
auto path = filePath.split('/');
path.removeLast();
QDesktopServices::openUrl(path.join('/'));
}
});
download(fullFileName, latestVersion);
// Calling show() before exec() is only way make dialog non-modal
// it seems wierd, but it works
progressDial.show();
progressDial.exec();
}
}
}
void UpdateWorker::abortDownload()
{
disconnect(downloadReply, &QNetworkReply::finished,
this, &UpdateWorker::serveDownloadFinish);
disconnect(downloadReply, &QNetworkReply::downloadProgress,
this, &UpdateWorker::process);
downloadReply->close();
downloadReply->deleteLater();
downloadFile.remove();
}
void UpdateWorker::serveVersionCheckReply()
{
pending = false;
QString versionReply = "";
QString errStr = "";
if (checkReply->error()) {
errStr = checkReply->errorString();
} else {
versionReply = QJsonDocument::fromJson(checkReply->readAll()).object().value("tag_name").toString();
versionReply.remove('v');
}
latestVersion = versionReply;
checkReply->close();
checkReply->deleteLater();
emit checkComplete(versionReply, errStr);
}
void UpdateWorker::serveDownloadFinish()
{
downloadReply->close();
downloadReply->deleteLater();
if (downloadReply->error()) {
emit downloadError(downloadReply->errorString());
} else {
emit downloadFinished(downloadFile.fileName());
}
}
void UpdateWorker::process(size_t bytesReceived, size_t bytesTotal)
{
downloadFile.write(downloadReply->readAll());
emit downloadProcess(bytesReceived, bytesTotal);
}
QString UpdateWorker::getRepositeryExt() const
{
#ifdef Q_OS_LINUX
return "AppImage";
#elif defined (Q_OS_WIN64) || defined (Q_OS_WIN32)
return "zip";
#elif defined (Q_OS_MACOS)
return "dmg";
#endif
}
QString UpdateWorker::getRepositoryFileName() const
{
QString downloadFileName;
#ifdef Q_OS_LINUX
downloadFileName = "Cutter-v%1-x%2.Linux.AppImage";
#elif defined (Q_OS_WIN64) || defined (Q_OS_WIN32)
downloadFileName = "Cutter-v%1-x%2.Windows.zip";
#elif defined (Q_OS_MACOS)
downloadFileName = "Cutter-v%1-x%2.macOS.dmg";
#endif
downloadFileName = downloadFileName
.arg(latestVersion)
.arg(QSysInfo::buildAbi().split('-').at(2).contains("64")
? "64"
: "32");
return downloadFileName;
}

123
src/common/UpdateWorker.h Normal file
View File

@ -0,0 +1,123 @@
#ifndef UPDATEWORKER_H
#define UPDATEWORKER_H
#include <QDir>
#include <QTimer>
#include <QObject>
#include <QtNetwork/QNetworkAccessManager>
class QNetworkReply;
/**
* @class UpdateWorker
* @brief The UpdateWorker class is a class providing API to check for current Cutter version
* and download specific version of one.
*/
class UpdateWorker : public QObject
{
Q_OBJECT
public:
explicit UpdateWorker(QObject *parent = nullptr);
/**
* @fn void UpdateWorker::checkCurrentVersion(time_t timeoutMs)
*
* Sends request to determine current version of Cutter.
* If there is no response in @a timeoutMs milliseconds, emits
* @fn UpdateWorker::checkComplete(const QString& currVerson, const QString& errorMsg)
* with timeout error message.
*
*
* @sa checkComplete(const QString& verson, const QString& errorMsg)
*/
void checkCurrentVersion(time_t timeoutMs);
/**
* @fn void UpdateWorker::download(QDir downloadPath, QString version)
*
* @brief Downloads provided @a version of Cutter into @a downloadDir.
*
* @sa downloadProcess(size_t bytesReceived, size_t bytesTotal)
*/
void download(QString filename, QString version);
/**
* @fn void UpdateWorker::showUpdateDialog()
*
* Shows dialog that allows user to either download latest version of Cutter from website
* or download it by clicking on a button. This dialog also has "Don't check for updates"
* button which disables on-start update checks if @a showDontCheckForUpdatesButton is true.
*
* @sa downloadProcess(size_t bytesReceived, size_t bytesTotal)
*/
void showUpdateDialog(bool showDontCheckForUpdatesButton);
public slots:
/**
* @fn void UpdateWorker::abortDownload()
*
* @brief Stops current process of downloading.
*
* @note UpdateWorker::downloadFinished(QString filename) is not send after this function.
*
* @sa download(QDir downloadDir, QString version)
*/
void abortDownload();
signals:
/**
* @fn UpdateWorker::checkComplete(const QString& verson, const QString& errorMsg)
*
* The signal is emitted when check has been done with an empty @a errorMsg string.
* In case of an error @a currVerson is empty and @a errorMsg contains description
* of error.
*/
void checkComplete(const QString &currVerson, const QString &errorMsg);
/**
* @fn UpdateWorker::downloadProcess(size_t bytesReceived, size_t bytesTotal)
*
* The signal is emitted each time when some amount of bytes was downloaded.
* May be used as indicator of download progress.
*/
void downloadProcess(size_t bytesReceived, size_t bytesTotal);
/**
* @fn UpdateWorker::downloadFinished(QString filename)
*
* @brief The signal is emitted as soon as downloading completes.
*/
void downloadFinished(QString filename);
/**
* @fn UpdateWorker::downloadError(QString errorStr)
*
* @brief The signal is emitted when error occures during download.
*/
void downloadError(QString errorStr);
private slots:
void serveVersionCheckReply();
void serveDownloadFinish();
void process(size_t bytesReceived, size_t bytesTotal);
private:
QString getRepositeryExt() const;
QString getRepositoryFileName() const;
private:
QNetworkAccessManager nm;
QString latestVersion;
QTimer t;
bool pending;
QFile downloadFile;
QNetworkReply *downloadReply;
QNetworkReply *checkReply;
};
#endif // UPDATEWORKER_H

View File

@ -8,9 +8,11 @@
#include <QUrl>
#include <QTimer>
#include <QEventLoop>
#include <QJsonObject>
#include <QProgressBar>
#include <QProgressDialog>
#include <UpdateWorker.h>
#include <QtNetwork/QNetworkRequest>
#include <QtNetwork/QNetworkAccessManager>
@ -25,29 +27,32 @@ AboutDialog::AboutDialog(QWidget *parent) :
ui->logoSvgWidget->load(Config()->getLogoFile());
QString aboutString("<h1>Cutter</h1>"
+ tr("Version") + " " CUTTER_VERSION_FULL "<br/>"
+ tr("Using r2-") + R2_GITTAP
+ "<p><b>" + tr("Optional Features:") + "</b><br/>"
+ QString("Jupyter: %1<br/>").arg(
+ tr("Version") + " " CUTTER_VERSION_FULL "<br/>"
+ tr("Using r2-") + R2_GITTAP
+ "<p><b>" + tr("Optional Features:") + "</b><br/>"
+ QString("Jupyter: %1<br/>").arg(
#ifdef CUTTER_ENABLE_JUPYTER
"ON"
"ON"
#else
"OFF"
"OFF"
#endif
)
+ QString("QtWebEngine: %2</p>").arg(
)
+ QString("QtWebEngine: %2</p>").arg(
#ifdef CUTTER_ENABLE_QTWEBENGINE
"ON"
"ON"
#else
"OFF"
"OFF"
#endif
)
+ "<h2>" + tr("License") + "</h2>"
+ tr("This Software is released under the GNU General Public License v3.0")
+ "<h2>" + tr("Authors") + "</h2>"
"xarkes, thestr4ng3r, ballessay<br/>"
"Based on work by Hugo Teso &lt;hugo.teso@gmail.org&gt; (originally Iaito).");
)
+ "<h2>" + tr("License") + "</h2>"
+ tr("This Software is released under the GNU General Public License v3.0")
+ "<h2>" + tr("Authors") + "</h2>"
"xarkes, thestr4ng3r, ballessay<br/>"
"Based on work by Hugo Teso &lt;hugo.teso@gmail.org&gt; (originally Iaito).");
ui->label->setText(aboutString);
QSignalBlocker s(ui->updatesCheckBox);
ui->updatesCheckBox->setChecked(Config()->getAutoUpdateEnabled());
}
AboutDialog::~AboutDialog() {}
@ -75,9 +80,7 @@ void AboutDialog::on_showPluginsButton_clicked()
void AboutDialog::on_checkForUpdatesButton_clicked()
{
QUrl url("https://api.github.com/repos/radareorg/cutter/releases/latest");
QNetworkRequest request;
request.setUrl(url);
UpdateWorker updateWorker;
QProgressDialog waitDialog;
QProgressBar *bar = new QProgressBar(&waitDialog);
@ -86,56 +89,25 @@ void AboutDialog::on_checkForUpdatesButton_clicked()
waitDialog.setBar(bar);
waitDialog.setLabel(new QLabel(tr("Checking for updates..."), &waitDialog));
QNetworkAccessManager nm;
QTimer timeoutTimer;
timeoutTimer.setSingleShot(true);
timeoutTimer.setInterval(7000);
connect(&nm, &QNetworkAccessManager::finished, &timeoutTimer, &QTimer::stop);
connect(&nm, &QNetworkAccessManager::finished, &waitDialog, &QProgressDialog::cancel);
connect(&nm, &QNetworkAccessManager::finished, this, &AboutDialog::serveVersionCheckReply);
QNetworkReply *reply = nm.get(request);
timeoutTimer.start();
connect(&timeoutTimer, &QTimer::timeout, []() {
QMessageBox mb;
mb.setIcon(QMessageBox::Critical);
mb.setStandardButtons(QMessageBox::Ok);
mb.setWindowTitle(tr("Timeout error!"));
mb.setText(tr("Please check your internet connection and try again."));
mb.exec();
connect(&updateWorker, &UpdateWorker::checkComplete, &waitDialog, &QProgressDialog::cancel);
connect(&updateWorker, &UpdateWorker::checkComplete,
[&updateWorker](const QString & version, const QString & error) {
if (error != "") {
QMessageBox::critical(nullptr, tr("Error!"), error);
} else {
if (version == CUTTER_VERSION_FULL) {
QMessageBox::information(nullptr, tr("Version control"), tr("Cutter is up to date!"));
} else {
updateWorker.showUpdateDialog(false);
}
}
});
updateWorker.checkCurrentVersion(7000);
waitDialog.exec();
delete reply;
}
void AboutDialog::serveVersionCheckReply(QNetworkReply *reply)
void AboutDialog::on_updatesCheckBox_stateChanged(int state)
{
QString currVersion = "";
QMessageBox mb;
mb.setStandardButtons(QMessageBox::Ok);
if (reply->error()) {
mb.setIcon(QMessageBox::Critical);
mb.setWindowTitle(tr("Error!"));
mb.setText(reply->errorString());
} else {
currVersion = QJsonDocument::fromJson(reply->readAll()).object().value("tag_name").toString();
currVersion.remove('v');
mb.setWindowTitle(tr("Version control"));
mb.setIcon(QMessageBox::Information);
if (currVersion == CUTTER_VERSION_FULL) {
mb.setText(tr("You have latest version and no need to update!"));
} else {
mb.setText("<b>" + tr("Current version:") + "</b> " CUTTER_VERSION_FULL "<br/>"
+ "<b>" + tr("Latest version:") + "</b> " + currVersion + "<br/><br/>"
+ tr("For update, please check the link:")
+ "<a href=\"https://github.com/radareorg/cutter/releases\">"
+ "https://github.com/radareorg/cutter/releases</a>");
}
}
mb.exec();
Config()->setAutoUpdateEnabled(!Config()->getAutoUpdateEnabled());
}

View File

@ -21,8 +21,20 @@ private slots:
void on_buttonBox_rejected();
void on_showVersionButton_clicked();
void on_showPluginsButton_clicked();
/**
* @fn AboutDialog::on_checkForUpdatesButton_clicked()
*
* @brief Initiates process of checking for updates.
*/
void on_checkForUpdatesButton_clicked();
void serveVersionCheckReply(QNetworkReply *reply);
/**
* @fn AboutDialog::on_updatesCheckBox_stateChanged(int state)
*
* @brief Changes value of autoUpdateEnabled option in settings.
*/
void on_updatesCheckBox_stateChanged(int state);
private:
std::unique_ptr<Ui::AboutDialog> ui;

View File

@ -76,6 +76,19 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="updatesCheckBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Maximum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Check for updates on start</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>

View File

@ -20,8 +20,9 @@ WelcomeDialog::WelcomeDialog(QWidget *parent) :
ui->logoSvgWidget->load(Config()->getLogoFile());
ui->versionLabel->setText("<font color='#a4a9b2'>" + tr("Version ") + CUTTER_VERSION_FULL + "</font>");
ui->themeComboBox->setCurrentIndex(Config()->getTheme());
ui->themeComboBox->setFixedWidth(200);
ui->themeComboBox->view()->setFixedWidth(200);
QSignalBlocker s(ui->updatesCheckBox);
ui->updatesCheckBox->setChecked(Config()->getAutoUpdateEnabled());
QStringList langs = Config()->getAvailableTranslations();
ui->languageComboBox->addItems(langs);
@ -97,3 +98,8 @@ void WelcomeDialog::on_continueButton_clicked()
{
accept();
}
void WelcomeDialog::on_updatesCheckBox_stateChanged(int state)
{
Config()->setAutoUpdateEnabled(!Config()->getAutoUpdateEnabled());
}

View File

@ -30,6 +30,7 @@ private slots:
void onLanguageComboBox_currentIndexChanged(int index);
void on_checkUpdateButton_clicked();
void on_continueButton_clicked();
void on_updatesCheckBox_stateChanged(int state);
private:
Ui::WelcomeDialog *ui;

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>806</width>
<height>488</height>
<height>620</height>
</rect>
</property>
<property name="sizePolicy">
@ -122,84 +122,118 @@
</spacer>
</item>
<item>
<layout class="QGridLayout" name="gridLayout" rowstretch="0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0" columnstretch="3,2,3,0,0,0">
<property name="sizeConstraint">
<enum>QLayout::SetFixedSize</enum>
</property>
<property name="horizontalSpacing">
<number>0</number>
</property>
<property name="verticalSpacing">
<number>9</number>
</property>
<item row="28" column="1">
<widget class="QComboBox" name="languageComboBox"/>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer_4">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="27" column="1">
<widget class="QComboBox" name="themeComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>9</number>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="iconSize">
<size>
<width>160</width>
<height>16</height>
</size>
<property name="sizeConstraint">
<enum>QLayout::SetMaximumSize</enum>
</property>
<item>
<property name="text">
<string>Native Theme</string>
</property>
<widget class="QPushButton" name="checkUpdateButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>About</string>
</property>
</widget>
</item>
<item>
<property name="text">
<string>Dark Theme</string>
</property>
<widget class="QComboBox" name="themeComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="currentIndex">
<number>0</number>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
<item>
<property name="text">
<string>Native Theme</string>
</property>
</item>
<item>
<property name="text">
<string>Dark Theme</string>
</property>
</item>
</widget>
</item>
</widget>
<item>
<widget class="QComboBox" name="languageComboBox"/>
</item>
<item>
<widget class="QCheckBox" name="updatesCheckBox">
<property name="text">
<string>Check for updates on start</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="26" column="1">
<widget class="QPushButton" name="checkUpdateButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
<item>
<spacer name="horizontalSpacer_3">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="minimumSize">
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
<width>40</width>
<height>20</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>About</string>
</property>
</widget>
</spacer>
</item>
</layout>
</item>