summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Edmundson <[email protected]>2016-12-16 11:17:23 +0000
committerDavid Edmundson <[email protected]>2016-12-16 11:17:23 +0000
commitea3e8d39731b28b3da298f48c40e2dd813851db6 (patch)
tree3c235f06f1e7d539563f40c881abe4c68e1c0891
parentbb9ea8e544e887790376ce6212383f404b6eea7a (diff)
Add SDDM theme install/uninstall with GHNS to the SDDM KCM
Summary: Add SDDM theme install/uninstall with Get Hot New Stuff to the SDDM KCM. Button arrangement is copied from the colour KCM. We can install from either a local .zip file or the KDE store. It assumes a zip file containing a folder/folders of themes to be installed into the SDDM directory. metadata is very loosely checked. As it needs to be root to install, this is added into the helper. An additional executable is needed to make KNS work properly. We need to keep track of which KNS packages contain which themes, which is done in a local config file. Test Plan: Installed a theme via GHNS Uninstalled it Installed a theme manually Uninstalled it Reviewers: #plasma, mart Reviewed By: mart Subscribers: plasma-devel Tags: #plasma Differential Revision: https://phabricator.kde.org/D3685
-rw-r--r--CMakeLists.txt18
-rw-r--r--kcm_sddm.actions12
-rw-r--r--sddmauthhelper.cpp115
-rw-r--r--sddmauthhelper.h2
-rw-r--r--sddmtheme.knsrc6
-rw-r--r--sddmthemeinstaller.cpp98
-rw-r--r--src/CMakeLists.txt3
-rw-r--r--src/sddmkcm.cpp1
-rw-r--r--src/themeconfig.cpp71
-rw-r--r--src/themeconfig.h8
-rw-r--r--src/themesmodel.cpp6
-rw-r--r--src/themesmodel.h1
-rw-r--r--src/ui/themeconfig.ui122
13 files changed, 419 insertions, 44 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e674692..eb22514 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,6 +19,8 @@ find_package(KF5 REQUIRED COMPONENTS
Auth
ConfigWidgets
KIO
+ Archive
+ NewStuff
)
@@ -45,12 +47,26 @@ install(FILES kcm_sddm.desktop DESTINATION ${CMAKE_INSTALL_KSERVICES5DIR})
kauth_install_actions(org.kde.kcontrol.kcmsddm kcm_sddm.actions)
add_executable(kcmsddm_authhelper sddmauthhelper.cpp)
-target_link_libraries(kcmsddm_authhelper KF5::Auth KF5::ConfigCore)
+target_link_libraries(kcmsddm_authhelper KF5::Auth KF5::ConfigCore KF5::Archive KF5::I18n)
kauth_install_helper_files(kcmsddm_authhelper org.kde.kcontrol.kcmsddm root)
install(TARGETS kcmsddm_authhelper DESTINATION ${KAUTH_HELPER_INSTALL_DIR})
+#installer tool for knewstuff
+add_executable(sddmthemeinstaller
+ sddmthemeinstaller.cpp
+)
+
+target_link_libraries(sddmthemeinstaller
+ KF5::I18n
+ KF5::Auth
+ KF5::CoreAddons
+ KF5::ConfigCore
+ KF5::WidgetsAddons)
+install(TARGETS sddmthemeinstaller ${KF5_INSTALL_TARGETS_DEFAULT_ARGS})
+
add_subdirectory(src)
+install(FILES sddmtheme.knsrc DESTINATION ${KDE_INSTALL_CONFDIR})
feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES)
diff --git a/kcm_sddm.actions b/kcm_sddm.actions
index 2487ba4..a581340 100644
--- a/kcm_sddm.actions
+++ b/kcm_sddm.actions
@@ -130,3 +130,15 @@ Description[zh_CN]=保存 SDDM 设置
Description[zh_TW]=儲存 SDDM 設定
Policy=auth_admin
Persistence=session
+
+[org.kde.kcontrol.kcmsddm.installtheme]
+Name=Install an SDDM theme
+Description=Extracts a zip file containing an SDDM theme to the SDDM theme location
+Policy=auth_admin
+Persistence=session
+
+[org.kde.kcontrol.kcmsddm.uninstalltheme]
+Name=Uninstall an SDDM theme
+Description=Removes a previously installed SDDM theme
+Policy=auth_admin
+Persistence=session
diff --git a/sddmauthhelper.cpp b/sddmauthhelper.cpp
index 5c67e84..ffbedec 100644
--- a/sddmauthhelper.cpp
+++ b/sddmauthhelper.cpp
@@ -22,7 +22,13 @@
#include <QDir>
#include <QSharedPointer>
#include <QDebug>
+#include <QMimeType>
+#include <QMimeDatabase>
+#include <KLocalizedString>
+#include <KZip>
+#include <KTar>
+#include <KArchive>
#include <KConfig>
#include <KConfigGroup>
@@ -111,6 +117,115 @@ ActionReply SddmAuthHelper::save(const QVariantMap &args)
return ActionReply::SuccessReply();
}
+ActionReply SddmAuthHelper::installtheme(const QVariantMap &args)
+{
+ const QString filePath = args["filePath"].toString();
+ if (filePath.isEmpty()) {
+ return ActionReply::HelperErrorReply();
+ }
+
+ const QString themesBaseDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "sddm/themes", QStandardPaths::LocateDirectory);
+ QDir dir(themesBaseDir);
+ if (!dir.exists()) {
+ return ActionReply::HelperErrorReply();
+ }
+
+ qDebug() << "Installing " << filePath << " into " << themesBaseDir;
+
+ if (!QFile::exists(filePath)) {
+ return ActionReply::HelperErrorReply();
+ }
+
+ QMimeDatabase db;
+ QMimeType mimeType = db.mimeTypeForFile(filePath);
+ qWarning() << "Postinstallation: uncompress the file";
+
+ QScopedPointer<KArchive> archive;
+
+ //there must be a better way to do this? If not, make a static bool KZip::supportsMimeType(const QMimeType &type); ?
+ //or even a factory class in KArchive
+
+ if (mimeType.inherits(QStringLiteral("application/zip"))) {
+ archive.reset(new KZip(filePath));
+ } else if (mimeType.inherits(QStringLiteral("application/tar"))
+ || mimeType.inherits(QStringLiteral("application/x-gzip"))
+ || mimeType.inherits(QStringLiteral("application/x-bzip"))
+ || mimeType.inherits(QStringLiteral("application/x-lzma"))
+ || mimeType.inherits(QStringLiteral("application/x-xz"))
+ || mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar"))
+ || mimeType.inherits(QStringLiteral("application/x-compressed-tar"))) {
+ archive.reset(new KTar(filePath));
+ } else {
+ auto e = ActionReply::HelperErrorReply();
+ e.setErrorDescription(i18n("Invalid theme package"));
+ return e; }
+
+ if (!archive->open(QIODevice::ReadOnly)) {
+ auto e = ActionReply::HelperErrorReply();
+ e.setErrorDescription("Could not open file");
+ return e;
+ }
+
+ auto directory = archive->directory();
+
+ QStringList installedPaths;
+
+ //some basic validation
+ //the top level should only have folders, and those folders should contain a valid metadata.desktop file
+ //if we get anything else, abort everything before copying
+ for(const QString &name: directory->entries()) {
+ auto entry = directory->entry(name);
+ if (!entry->isDirectory()) {
+ auto e = ActionReply::HelperErrorReply();
+ e.setErrorDescription(i18n("Invalid theme package"));
+ return e;
+ }
+ auto subDirectory = static_cast<const KArchiveDirectory*>(entry);
+ auto metadataFile = subDirectory->file("metadata.desktop");
+ if(!metadataFile || !metadataFile->data().contains("[SddmGreeterTheme]")) {
+ auto e = ActionReply::HelperErrorReply();
+ e.setErrorDescription(i18n("Invalid theme package"));
+ return e;
+ }
+ installedPaths.append(themesBaseDir + '/' + name);
+ }
+
+ if (!directory->copyTo(themesBaseDir)) {
+ auto e = ActionReply::HelperErrorReply();
+ e.setErrorDescription(i18n("Could not decompress archive"));
+ return e;
+ }
+
+ auto rc = ActionReply::SuccessReply();
+ rc.addData(QStringLiteral("installedPaths"), installedPaths);
+ return rc;
+}
+
+ActionReply SddmAuthHelper::uninstalltheme(const QVariantMap &args)
+{
+ const QString themePath = args["filePath"].toString();
+ const QString themesBaseDir = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "sddm/themes", QStandardPaths::LocateDirectory);
+
+ QDir dir(themePath);
+ if (!dir.exists()) {
+ return ActionReply::HelperErrorReply();
+ }
+
+ //validate the themePath is directly inside the themesBaseDir
+ QDir baseDir(themesBaseDir);
+ if(baseDir.absoluteFilePath(dir.dirName()) != dir.absolutePath()) {
+ return ActionReply::HelperErrorReply();
+ }
+
+ if (!dir.removeRecursively()) {
+ return ActionReply::HelperErrorReply();
+ }
+
+ return ActionReply::SuccessReply();
+}
+
+
KAUTH_HELPER_MAIN("org.kde.kcontrol.kcmsddm", SddmAuthHelper);
#include "moc_sddmauthhelper.cpp"
+
diff --git a/sddmauthhelper.h b/sddmauthhelper.h
index a7f3537..92c1dfd 100644
--- a/sddmauthhelper.h
+++ b/sddmauthhelper.h
@@ -26,6 +26,8 @@ class SddmAuthHelper: public QObject
Q_OBJECT
public slots:
ActionReply save(const QVariantMap &args);
+ ActionReply installtheme(const QVariantMap &args);
+ ActionReply uninstalltheme(const QVariantMap &args);
};
#endif //SDDMAUTHHELPER_H
diff --git a/sddmtheme.knsrc b/sddmtheme.knsrc
new file mode 100644
index 0000000..206a638
--- /dev/null
+++ b/sddmtheme.knsrc
@@ -0,0 +1,6 @@
+[KNewStuff3]
+ProvidersUrl=https://autoconfig.kde.org/ocs/providers.xml
+Categories=SDDM Theme
+StandardResource=tmp
+InstallationCommand=sddmthemeinstaller -i %f
+UninstallCommand=sddmthemeinstaller -u %f
diff --git a/sddmthemeinstaller.cpp b/sddmthemeinstaller.cpp
new file mode 100644
index 0000000..b3f7da4
--- /dev/null
+++ b/sddmthemeinstaller.cpp
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 Marco Martin <[email protected]>
+ * Copyright (C) 2016 David Edmundson <[email protected]>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include <QApplication>
+#include <QDir>
+#include <QFile>
+#include <QDebug>
+#include <QProcess>
+#include <QCommandLineOption>
+#include <QCommandLineParser>
+
+#include <KLocalizedString>
+#include <KAuthAction>
+#include <KAuthExecuteJob>
+#include <KMessageBox>
+#include <KSharedConfig>
+#include <KConfigGroup>
+
+int main(int argc, char **argv)
+{
+ QCommandLineParser parser;
+ QApplication app(argc, argv); //because GHNS doesn't do it's own error reporting on installation failing..
+
+ const QString description = i18n("SDDM theme installer");
+ const char version[] = "1.0";
+
+ app.setApplicationVersion(version);
+ parser.addVersionOption();
+ parser.addHelpOption();
+ parser.setApplicationDescription(description);
+ parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("i") << QStringLiteral("install"), i18n("Install a theme.")));
+ parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("u") << QStringLiteral("uninstall"), i18n("Uninstall a theme.")));
+
+ parser.addPositionalArgument("themefile", i18n("The theme to install, must be an existing archive file."));
+
+ parser.process(app);
+
+ const QStringList args = parser.positionalArguments();
+ if (args.isEmpty()) {
+ qWarning() << "No theme file specified.";
+ return 0;
+ }
+
+ if (parser.isSet(QLatin1String("install"))) {
+ const QFileInfo themefile(args.first());
+ if (!themefile.exists()) {
+ qWarning() << "Specified theme file does not exists";
+ return 0;
+ }
+
+ KAuth::Action action(QStringLiteral("org.kde.kcontrol.kcmsddm.installtheme"));
+ action.setHelperId("org.kde.kcontrol.kcmsddm");
+ action.addArgument(QStringLiteral("filePath"), themefile.absoluteFilePath());
+
+ KAuth::ExecuteJob *job = action.execute();
+ bool rc = job->exec();
+ if (!rc) {
+ KMessageBox::sorry(0, i18n("Unable to install theme"), job->errorString());
+ qWarning() << job->error() << job->errorString();
+ return -1;
+ }
+
+ KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("sddmthemeinstallerrc"), KConfig::SimpleConfig), "DownloadedThemes");
+ cg.writeEntry(themefile.absoluteFilePath(), job->data().value("installedPaths").toStringList());
+ return 0;
+ }
+ if (parser.isSet(QLatin1String("uninstall"))) {
+ KConfigGroup cg(KSharedConfig::openConfig(QStringLiteral("sddmthemeinstallerrc"), KConfig::SimpleConfig), "DownloadedThemes");
+ QStringList installed = cg.readEntry(args.first(), QStringList());
+ for (const QString &installedTheme: installed) {
+ KAuth::Action action(QStringLiteral("org.kde.kcontrol.kcmsddm.uninstalltheme"));
+ action.setHelperId("org.kde.kcontrol.kcmsddm");
+ action.addArgument(QStringLiteral("filePath"), installed);
+ KAuth::ExecuteJob *job = action.execute();
+ job->exec();
+ }
+ return 0;
+ }
+ qWarning() << "either install or uninstall must be passed as an argument";
+ return -1;
+}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 65be8bf..05a9d8a 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,5 +1,5 @@
set(SDDM_CONFIG_FILE "/etc/sddm.conf" CACHE PATH "Path of the sddm config file")
-
+
configure_file(config.h.in config.h IMMEDIATE @ONLY)
# add_subdirectory(configwidgets)
@@ -39,6 +39,7 @@ target_link_libraries(kcm_sddm
KF5::ConfigWidgets
KF5::Auth
KF5::KIOWidgets
+ KF5::NewStuff
${X11_LIBRARIES}
XCB::XCB # For mouse cursor themes
diff --git a/src/sddmkcm.cpp b/src/sddmkcm.cpp
index be07230..3dd8d07 100644
--- a/src/sddmkcm.cpp
+++ b/src/sddmkcm.cpp
@@ -22,7 +22,6 @@
#include <KPluginFactory>
#include <KAuth/KAuthActionReply>
-#include <KAuth/KAuthActionReply>
#include <KAboutData>
diff --git a/src/themeconfig.cpp b/src/themeconfig.cpp
index fd867f8..2a7c8d2 100644
--- a/src/themeconfig.cpp
+++ b/src/themeconfig.cpp
@@ -25,9 +25,15 @@
#include <QQmlContext>
#include <QDebug>
#include <QStandardPaths>
+#include <QPointer>
+#include <QFileDialog>
#include <KMessageBox>
#include <KConfigGroup>
+#include <KNewStuff3/KNS3/DownloadDialog>
+#include <KAuthAction>
+#include <KAuthActionReply>
+#include <KAuthExecuteJob>
#include "config.h"
@@ -38,8 +44,8 @@ ThemeConfig::ThemeConfig(QWidget *parent) :
configUi = new Ui::ThemeConfig();
configUi->setupUi(this);
-// configUi->customizeBox->setVisible(false);
-
+ configUi->messageWidget->setVisible(false);
+
ThemesModel *model = new ThemesModel(this);
configUi->themesListView->setModel(model);
@@ -47,11 +53,17 @@ ThemeConfig::ThemeConfig(QWidget *parent) :
delegate->setPreviewSize(QSize(128,128));
configUi->themesListView->setItemDelegate(delegate);
model->populate();
+ connect(this, &ThemeConfig::themesChanged, model, &ThemesModel::populate);
connect(configUi->themesListView, SIGNAL(activated(QModelIndex)), SLOT(themeSelected(QModelIndex)));
connect(configUi->themesListView, SIGNAL(clicked(QModelIndex)), SLOT(themeSelected(QModelIndex)));
connect(configUi->selectBackgroundButton, SIGNAL(imagePathChanged(QString)), SLOT(backgroundChanged(QString)));
+ connect(configUi->getNewButton, &QPushButton::clicked, this, &ThemeConfig::getNewStuffClicked);
+ connect(configUi->installFromFileButton, &QPushButton::clicked, this, &ThemeConfig::installFromFileClicked);
+ connect(configUi->removeThemeButton, &QPushButton::clicked, this, &ThemeConfig::removeThemeClicked);
+
+
prepareInitialTheme();
}
@@ -165,3 +177,58 @@ void ThemeConfig::dump()
qDebug() << "Current theme:" << config.readEntry("CurrentTheme");
}
+
+void ThemeConfig::getNewStuffClicked()
+{
+ QPointer<KNS3::DownloadDialog> dialog(new KNS3::DownloadDialog(QStringLiteral("sddmtheme.knsrc"), this));
+
+ dialog->setWindowTitle(i18n("Download New SDDM Themes"));
+ if (dialog->exec()) {
+ emit themesChanged();
+ }
+ delete dialog.data();
+}
+
+void ThemeConfig::installFromFileClicked()
+{
+ QPointer<QFileDialog> dialog(new QFileDialog(this));
+ dialog->exec();
+ QStringList files = dialog->selectedFiles();
+ if (files.count() == 1) {
+ QString file = files.first();
+ KAuth::Action saveAction(QStringLiteral("org.kde.kcontrol.kcmsddm.installtheme"));
+ saveAction.setHelperId("org.kde.kcontrol.kcmsddm");
+ saveAction.addArgument(QStringLiteral("filePath"), file);
+ auto job = saveAction.execute();
+ if (!job->exec()) {
+ configUi->messageWidget->setMessageType(KMessageWidget::Warning);
+ configUi->messageWidget->setText(job->errorString());
+ configUi->messageWidget->animatedShow();
+ } else {
+ emit themesChanged();
+ }
+ }
+
+ delete dialog.data();
+}
+
+void ThemeConfig::removeThemeClicked()
+{
+ if (!configUi->themesListView->currentIndex().isValid()) {
+ return;
+ }
+
+ const QString path = configUi->themesListView->currentIndex().data(ThemesModel::PathRole).toString();
+ KAuth::Action saveAction(QStringLiteral("org.kde.kcontrol.kcmsddm.uninstalltheme"));
+ saveAction.setHelperId("org.kde.kcontrol.kcmsddm");
+ saveAction.addArgument(QStringLiteral("filePath"), path);
+ auto job = saveAction.execute();
+ if (!job->exec()) {
+ configUi->messageWidget->setMessageType(KMessageWidget::Warning);
+ configUi->messageWidget->setText(job->errorString());
+ configUi->messageWidget->animatedShow();
+ } else {
+ emit themesChanged();
+ }
+}
+
diff --git a/src/themeconfig.h b/src/themeconfig.h
index 294b1b9..22c2172 100644
--- a/src/themeconfig.h
+++ b/src/themeconfig.h
@@ -39,11 +39,15 @@ public:
signals:
void changed(bool);
+ void themesChanged();
private slots:
void themeSelected(const QModelIndex &index);
void backgroundChanged(const QString &imagePath);
-
+ void getNewStuffClicked();
+ void installFromFileClicked();
+ void removeThemeClicked();
+
private:
Ui::ThemeConfig *configUi;
KSharedConfigPtr mConfig;
@@ -56,4 +60,4 @@ private:
void dump();
};
-#endif // THEMECONFIG_H \ No newline at end of file
+#endif // THEMECONFIG_H
diff --git a/src/themesmodel.cpp b/src/themesmodel.cpp
index 8613945..af52210 100644
--- a/src/themesmodel.cpp
+++ b/src/themesmodel.cpp
@@ -84,6 +84,12 @@ QVariant ThemesModel::data(const QModelIndex &index, int role) const
void ThemesModel::populate()
{
+ if (!mThemeList.isEmpty()) {
+ beginResetModel();
+ mThemeList.clear();
+ endResetModel();
+ }
+
QString themesBaseDir = KSharedConfig::openConfig(SDDM_CONFIG_FILE, KConfig::SimpleConfig)->group("Theme").readEntry("ThemeDir");
if (themesBaseDir.isEmpty()) {
diff --git a/src/themesmodel.h b/src/themesmodel.h
index ee77ab6..c9dd1c9 100644
--- a/src/themesmodel.h
+++ b/src/themesmodel.h
@@ -47,6 +47,7 @@ public:
int rowCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role) const;
+public Q_SLOTS:
void populate();
private:
diff --git a/src/ui/themeconfig.ui b/src/ui/themeconfig.ui
index b256c82..049ae1d 100644
--- a/src/ui/themeconfig.ui
+++ b/src/ui/themeconfig.ui
@@ -7,43 +7,46 @@
<x>0</x>
<y>0</y>
<width>620</width>
- <height>452</height>
+ <height>555</height>
</rect>
</property>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <layout class="QVBoxLayout" name="verticalLayout">
<item>
- <widget class="QListView" name="themesListView">
- <property name="minimumSize">
- <size>
- <width>175</width>
- <height>0</height>
- </size>
- </property>
- <property name="movement">
- <enum>QListView::Static</enum>
- </property>
- <property name="flow">
- <enum>QListView::LeftToRight</enum>
- </property>
- <property name="isWrapping" stdset="0">
- <bool>true</bool>
- </property>
- <property name="resizeMode">
- <enum>QListView::Adjust</enum>
- </property>
- <property name="layoutMode">
- <enum>QListView::Batched</enum>
- </property>
- <property name="spacing">
- <number>2</number>
- </property>
- <property name="viewMode">
- <enum>QListView::IconMode</enum>
- </property>
- </widget>
+ <widget class="KMessageWidget" name="messageWidget"/>
</item>
<item>
- <layout class="QVBoxLayout" name="detailLayout" stretch="0,0">
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QListView" name="themesListView">
+ <property name="minimumSize">
+ <size>
+ <width>175</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="movement">
+ <enum>QListView::Static</enum>
+ </property>
+ <property name="flow">
+ <enum>QListView::LeftToRight</enum>
+ </property>
+ <property name="isWrapping" stdset="0">
+ <bool>true</bool>
+ </property>
+ <property name="resizeMode">
+ <enum>QListView::Adjust</enum>
+ </property>
+ <property name="layoutMode">
+ <enum>QListView::Batched</enum>
+ </property>
+ <property name="spacing">
+ <number>2</number>
+ </property>
+ <property name="viewMode">
+ <enum>QListView::IconMode</enum>
+ </property>
+ </widget>
+ </item>
<item>
<widget class="QGroupBox" name="customizeBox">
<property name="title">
@@ -90,31 +93,76 @@
</item>
</layout>
</item>
+ <item>
+ <spacer name="verticalSpacer">
+ <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>
</widget>
</item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QPushButton" name="getNewButton">
+ <property name="text">
+ <string>Get New Theme</string>
+ </property>
+ <property name="icon">
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="installFromFileButton">
+ <property name="text">
+ <string>Install From File</string>
+ </property>
+ </widget>
+ </item>
<item>
- <spacer name="verticalSpacer">
+ <spacer name="horizontalSpacer">
<property name="orientation">
- <enum>Qt::Vertical</enum>
+ <enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
- <width>20</width>
- <height>40</height>
+ <width>40</width>
+ <height>20</height>
</size>
</property>
</spacer>
</item>
+ <item>
+ <widget class="QPushButton" name="removeThemeButton">
+ <property name="text">
+ <string>Remove Theme</string>
+ </property>
+ </widget>
+ </item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
+ <class>KMessageWidget</class>
+ <extends>QFrame</extends>
+ <header>kmessagewidget.h</header>
+ </customwidget>
+ <customwidget>
<class>QQuickWidget</class>
<extends>QWidget</extends>
- <header>QQuickWidget</header>
+ <header>QtQuickWidgets/QQuickWidget</header>
</customwidget>
<customwidget>
<class>SelectImageButton</class>