diff --git a/src/libcalamares/utils/CalamaresUtilsSystem.h b/src/libcalamares/utils/CalamaresUtilsSystem.h index afdd4ec34..e11ecae05 100644 --- a/src/libcalamares/utils/CalamaresUtilsSystem.h +++ b/src/libcalamares/utils/CalamaresUtilsSystem.h @@ -139,32 +139,33 @@ public: RunInTarget }; - /** - * Runs the specified command in the chroot of the target system. - * @param args the command with arguments, as a string list. - * @param workingPath the current working directory for the QProcess - * call (optional). - * @param stdInput the input string to send to the running process as - * standard input (optional). - * @param timeoutSec the timeout after which the process will be - * killed (optional, default is 0 i.e. no timeout). - * - * @returns the program's exit code and its output (if any). Special - * exit codes (which will never have any output) are: - * Crashed = QProcess crash - * FailedToStart = QProcess cannot start - * NoWorkingDirectory = bad arguments - * TimedOut = QProcess timeout - */ + /** @brief Runs a command in the host or the target (select explicitly) + * + * @param location whether to run in the host or the target + * @param args the command with arguments, as a string list. + * @param workingPath the current working directory for the QProcess + * call (optional). + * @param stdInput the input string to send to the running process as + * standard input (optional). + * @param timeoutSec the timeout after which the process will be + * killed (optional, default is 0 i.e. no timeout). + * + * @returns the program's exit code and its output (if any). Special + * exit codes (which will never have any output) are: + * Crashed = QProcess crash + * FailedToStart = QProcess cannot start + * NoWorkingDirectory = bad arguments + * TimedOut = QProcess timeout + */ static DLLEXPORT ProcessResult runCommand( RunLocation location, const QStringList& args, const QString& workingPath = QString(), const QString& stdInput = QString(), std::chrono::seconds timeoutSec = std::chrono::seconds( 0 ) ); - /** @brief Convenience wrapper for runCommand() + /** @brief Convenience wrapper for runCommand() in the host * - * Runs the given command-line @p args in the host in the current direcory + * Runs the given command-line @p args in the **host** in the current direcory * with no input, and the given @p timeoutSec for completion. */ static inline ProcessResult runCommand( const QStringList& args, std::chrono::seconds timeoutSec ) @@ -173,10 +174,11 @@ public: } /** @brief Convenience wrapper for runCommand(). - * Runs the command in the location specified through the boolean - * doChroot(), which is what you usually want for running commands - * during installation. - */ + * + * Runs the command in the location specified through the boolean + * doChroot(), which is what you usually want for running commands + * during installation. + */ inline ProcessResult targetEnvCommand( const QStringList& args, const QString& workingPath = QString(), const QString& stdInput = QString(), diff --git a/src/modules/plasmalnf/CMakeLists.txt b/src/modules/plasmalnf/CMakeLists.txt index 15c836b5b..3a85d9598 100644 --- a/src/modules/plasmalnf/CMakeLists.txt +++ b/src/modules/plasmalnf/CMakeLists.txt @@ -10,16 +10,18 @@ find_package(ECM ${ECM_VERSION} REQUIRED NO_MODULE) set( lnf_ver 5.41 ) find_package( KF5Config ${lnf_ver} ) -find_package( KF5Plasma ${lnf_ver} ) -find_package( KF5Package ${lnf_ver} ) set_package_properties( KF5Config PROPERTIES PURPOSE "For finding default Plasma Look-and-Feel" ) + +find_package( KF5Plasma ${lnf_ver} ) set_package_properties( KF5Plasma PROPERTIES PURPOSE "For Plasma Look-and-Feel selection" ) + +find_package( KF5Package ${lnf_ver} ) set_package_properties( KF5Package PROPERTIES PURPOSE "For Plasma Look-and-Feel selection" @@ -32,10 +34,11 @@ if ( KF5Plasma_FOUND AND KF5Package_FOUND ) COMPILE_DEFINITIONS ${option_defs} SOURCES + Config.cpp PlasmaLnfViewStep.cpp PlasmaLnfPage.cpp PlasmaLnfJob.cpp - ThemeWidget.cpp + ThemeInfo.cpp RESOURCES page_plasmalnf.qrc UI diff --git a/src/modules/plasmalnf/Config.cpp b/src/modules/plasmalnf/Config.cpp new file mode 100644 index 000000000..e022109b4 --- /dev/null +++ b/src/modules/plasmalnf/Config.cpp @@ -0,0 +1,164 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + */ + +#include "Config.h" + +#include "PlasmaLnfJob.h" +#include "ThemeInfo.h" + +#include "utils/CalamaresUtilsSystem.h" +#include "utils/Logger.h" +#include "utils/Variant.h" + +#ifdef WITH_KCONFIG +#include +#include +#endif + +#include + +static QString +currentPlasmaTheme() +{ +#ifdef WITH_KCONFIG + KConfigGroup cg( KSharedConfig::openConfig( QStringLiteral( "kdeglobals" ) ), "KDE" ); + return cg.readEntry( "LookAndFeelPackage", QString() ); +#else + cWarning() << "No KConfig support, cannot determine Plasma theme."; + return QString(); +#endif +} + +Config::Config( QObject* parent ) + : QObject( parent ) + , m_themeModel( new ThemesModel( this ) ) +{ + auto* filter = new QSortFilterProxyModel( m_themeModel ); + filter->setFilterRole( ThemesModel::ShownRole ); + filter->setFilterFixedString( QStringLiteral( "true" ) ); + filter->setSourceModel( m_themeModel ); + filter->setSortRole( ThemesModel::LabelRole ); + filter->sort( 0 ); + + m_filteredModel = filter; +} + +void +Config::setConfigurationMap( const QVariantMap& configurationMap ) +{ + m_lnfPath = CalamaresUtils::getString( configurationMap, "lnftool" ); + + if ( m_lnfPath.isEmpty() ) + { + cWarning() << "no lnftool given for plasmalnf module."; + } + + m_liveUser = CalamaresUtils::getString( configurationMap, "liveuser" ); + + QString preselect = CalamaresUtils::getString( configurationMap, "preselect" ); + if ( preselect == QStringLiteral( "*" ) ) + { + preselect = currentPlasmaTheme(); + } + m_preselectThemeId = preselect; + + if ( configurationMap.contains( "themes" ) && configurationMap.value( "themes" ).type() == QVariant::List ) + { + QMap< QString, QString > listedThemes; + auto themeList = configurationMap.value( "themes" ).toList(); + // Create the ThemInfo objects for the listed themes; information + // about the themes from Plasma (e.g. human-readable name and description) + // are filled in by update_names() in PlasmaLnfPage. + for ( const auto& i : themeList ) + if ( i.type() == QVariant::Map ) + { + auto iv = i.toMap(); + listedThemes.insert( iv.value( "theme" ).toString(), iv.value( "image" ).toString() ); + } + else if ( i.type() == QVariant::String ) + { + listedThemes.insert( i.toString(), QString() ); + } + + if ( listedThemes.count() == 1 ) + { + cWarning() << "only one theme enabled in plasmalnf"; + } + m_themeModel->setThemeImage( listedThemes ); + + bool showAll = CalamaresUtils::getBool( configurationMap, "showAll", false ); + if ( !listedThemes.isEmpty() && !showAll ) + { + m_themeModel->showOnlyThemes( listedThemes ); + } + } + + m_themeModel->select( m_preselectThemeId ); +} + +Calamares::JobList +Config::createJobs() const +{ + Calamares::JobList l; + + cDebug() << "Creating Plasma LNF jobs .."; + if ( !theme().isEmpty() ) + { + if ( !lnfToolPath().isEmpty() ) + { + l.append( Calamares::job_ptr( new PlasmaLnfJob( lnfToolPath(), theme() ) ) ); + } + else + { + cWarning() << "no lnftool given for plasmalnf module."; + } + } + return l; +} + + +void +Config::setTheme( const QString& id ) +{ + if ( m_themeId == id ) + { + return; + } + + m_themeId = id; + if ( lnfToolPath().isEmpty() ) + { + cWarning() << "no lnftool given for plasmalnf module."; + } + else + { + QStringList command; + if ( !m_liveUser.isEmpty() ) + { + command << "sudo" + << "-E" + << "-H" + << "-u" << m_liveUser; + } + command << lnfToolPath() << "--resetLayout" + << "--apply" << id; + auto r = CalamaresUtils::System::instance()->runCommand( command, std::chrono::seconds( 10 ) ); + + if ( r.getExitCode() ) + { + cWarning() << "Failed (" << r.getExitCode() << ')'; + } + else + { + cDebug() << "Plasma look-and-feel applied" << id; + } + } + m_themeModel->select( id ); + emit themeChanged( id ); +} diff --git a/src/modules/plasmalnf/Config.h b/src/modules/plasmalnf/Config.h new file mode 100644 index 000000000..aafdf6418 --- /dev/null +++ b/src/modules/plasmalnf/Config.h @@ -0,0 +1,77 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + */ + +#ifndef PLASMALNF_CONFIG_H +#define PLASMALNF_CONFIG_H + +#include "Job.h" + +#include "ThemeInfo.h" + +#include + +class Config : public QObject +{ + Q_OBJECT + + Q_PROPERTY( QString preselectedTheme READ preselectedTheme CONSTANT ) + Q_PROPERTY( QString theme READ theme WRITE setTheme NOTIFY themeChanged ) + Q_PROPERTY( QAbstractItemModel* themeModel READ themeModel CONSTANT ) + +public: + Config( QObject* parent = nullptr ); + virtual ~Config() override = default; // QObject cleans up the model pointer + + void setConfigurationMap( const QVariantMap& ); + Calamares::JobList createJobs() const; + + /** @brief Full path to the lookandfeeltool (if it exists) + * + * This can be configured, or defaults to `lookandfeeltool` to find + * it in $PATH. + */ + QString lnfToolPath() const { return m_lnfPath; } + /** @brief For OEM mode, the name of the (current) live user + * + */ + QString liveUser() const { return m_liveUser; } + + /** @brief The id (in reverse-DNS notation) of the current theme. + */ + QString theme() const { return m_themeId; } + + /** @brief The theme we start with + * + * This can be configured, or is taken from the live environment + * if the environment is (also) KDE Plasma. + */ + QString preselectedTheme() const { return m_preselectThemeId; } + + /** @brief The (list) model of available themes. + */ + QAbstractItemModel* themeModel() const { return m_filteredModel; } + +public slots: + void setTheme( const QString& id ); + +signals: + void themeChanged( const QString& id ); + +private: + QString m_lnfPath; // Path to the lnf tool + QString m_liveUser; // Name of the live user (for OEM mode) + + QString m_preselectThemeId; + QString m_themeId; // Id of selected theme + + QAbstractItemModel* m_filteredModel = nullptr; + ThemesModel* m_themeModel = nullptr; +}; + +#endif diff --git a/src/modules/plasmalnf/PlasmaLnfPage.cpp b/src/modules/plasmalnf/PlasmaLnfPage.cpp index a44620074..0e7f05e43 100644 --- a/src/modules/plasmalnf/PlasmaLnfPage.cpp +++ b/src/modules/plasmalnf/PlasmaLnfPage.cpp @@ -10,49 +10,67 @@ #include "PlasmaLnfPage.h" +#include "Config.h" #include "ui_page_plasmalnf.h" #include "Settings.h" #include "utils/Logger.h" #include "utils/Retranslator.h" -#include +#include +#include +#include -#include -#include - -ThemeInfo::ThemeInfo( const KPluginMetaData& data ) - : id( data.pluginId() ) - , name( data.name() ) - , description( data.description() ) - , widget( nullptr ) +class ThemeDelegate : public QStyledItemDelegate { +public: + using QStyledItemDelegate::QStyledItemDelegate; + + void paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const override; + // The size of the item is constant + QSize sizeHint( const QStyleOptionViewItem&, const QModelIndex& ) const override; +}; + +QSize +ThemeDelegate::sizeHint( const QStyleOptionViewItem&, const QModelIndex& ) const +{ + QSize image( ThemesModel::imageSize() ); + return { 3 * image.width(), image.height() }; } -static ThemeInfoList -plasma_themes() +void +ThemeDelegate::paint( QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index ) const { - ThemeInfoList packages; + auto label = index.data( ThemesModel::LabelRole ).toString(); + auto description = index.data( ThemesModel::DescriptionRole ).toString(); + auto selected = index.data( ThemesModel::SelectedRole ).toBool() ? QStyle::State_On : QStyle::State_Off; + auto image_v = index.data( ThemesModel::ImageRole ); + QPixmap image = image_v.canConvert< QPixmap >() ? qvariant_cast< QPixmap >( image_v ) : QPixmap(); - QList< KPluginMetaData > pkgs = KPackage::PackageLoader::self()->listPackages( "Plasma/LookAndFeel" ); + // The delegate paints three "columns", each of which takes 1/3 + // of the space: label, description and screenshot. + QRect labelRect( option.rect ); + labelRect.setWidth( labelRect.width() / 3 ); - for ( const KPluginMetaData& data : pkgs ) - { - if ( data.isValid() && !data.isHidden() && !data.name().isEmpty() ) - { - packages << ThemeInfo { data }; - } - } + QStyleOptionButton rbOption; + rbOption.state |= QStyle::State_Enabled | selected; + rbOption.rect = labelRect; + rbOption.text = label; + option.widget->style()->drawControl( QStyle::CE_RadioButton, &rbOption, painter, option.widget ); - return packages; + labelRect.moveLeft( labelRect.width() ); + option.widget->style()->drawItemText( + painter, labelRect, Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, option.palette, false, description ); + + labelRect.moveLeft( 2 * labelRect.width() ); + option.widget->style()->drawItemPixmap( painter, labelRect, Qt::AlignHCenter | Qt::AlignVCenter, image ); } -PlasmaLnfPage::PlasmaLnfPage( QWidget* parent ) +PlasmaLnfPage::PlasmaLnfPage( Config* config, QWidget* parent ) : QWidget( parent ) , ui( new Ui::PlasmaLnfPage ) - , m_showAll( false ) - , m_buttonGroup( nullptr ) + , m_config( config ) { ui->setupUi( this ); CALAMARES_RETRANSLATE( { @@ -67,134 +85,28 @@ PlasmaLnfPage::PlasmaLnfPage( QWidget* parent ) "You can also skip this step and configure the look-and-feel " "once the system is installed. Clicking on a look-and-feel " "selection will give you a live preview of that look-and-feel." ) ); - updateThemeNames(); - fillUi(); } ) -} - -void -PlasmaLnfPage::setLnfPath( const QString& path ) -{ - m_lnfPath = path; -} - -void -PlasmaLnfPage::setEnabledThemes( const ThemeInfoList& themes, bool showAll ) -{ - m_enabledThemes = themes; - - if ( showAll ) - { - auto plasmaThemes = plasma_themes(); - for ( auto& installed_theme : plasmaThemes ) - if ( !m_enabledThemes.findById( installed_theme.id ) ) - { - m_enabledThemes.append( installed_theme ); - } - } - - updateThemeNames(); - winnowThemes(); - fillUi(); -} - -void -PlasmaLnfPage::setEnabledThemesAll() -{ - // Don't need to set showAll=true, because we're already passing in - // the complete list of installed themes. - setEnabledThemes( plasma_themes(), false ); -} - -void -PlasmaLnfPage::setPreselect( const QString& id ) -{ - m_preselect = id; - if ( !m_enabledThemes.isEmpty() ) - { - fillUi(); - } -} - -void -PlasmaLnfPage::updateThemeNames() -{ - auto plasmaThemes = plasma_themes(); - for ( auto& enabled_theme : m_enabledThemes ) - { - ThemeInfo* t = plasmaThemes.findById( enabled_theme.id ); - if ( t != nullptr ) - { - enabled_theme.name = t->name; - enabled_theme.description = t->description; - } - } -} - -void -PlasmaLnfPage::winnowThemes() -{ - auto plasmaThemes = plasma_themes(); - bool winnowed = true; - int winnow_index = 0; - while ( winnowed ) - { - winnowed = false; - winnow_index = 0; - - for ( auto& enabled_theme : m_enabledThemes ) - { - ThemeInfo* t = plasmaThemes.findById( enabled_theme.id ); - if ( t == nullptr ) - { - cDebug() << "Removing" << enabled_theme.id; - winnowed = true; - break; - } - ++winnow_index; - } - - if ( winnowed ) - { - m_enabledThemes.removeAt( winnow_index ); - } - } -} - -void -PlasmaLnfPage::fillUi() -{ - if ( m_enabledThemes.isEmpty() ) - { - return; - } - - if ( !m_buttonGroup ) - { - m_buttonGroup = new QButtonGroup( this ); - m_buttonGroup->setExclusive( true ); - } - - int c = 1; // After the general explanation - for ( auto& theme : m_enabledThemes ) - { - if ( !theme.widget ) - { - ThemeWidget* w = new ThemeWidget( theme ); - m_buttonGroup->addButton( w->button() ); - ui->verticalLayout->insertWidget( c, w ); - connect( w, &ThemeWidget::themeSelected, this, &PlasmaLnfPage::plasmaThemeSelected ); - theme.widget = w; - } - else - { - theme.widget->updateThemeName( theme ); - } - if ( theme.id == m_preselect ) - { - const QSignalBlocker b( theme.widget->button() ); - theme.widget->button()->setChecked( true ); - } - ++c; - } + + auto* view = new QListView( this ); + view->setModel( m_config->themeModel() ); + view->setItemDelegate( new ThemeDelegate( view ) ); + view->setUniformItemSizes( true ); + view->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); + ui->verticalLayout->addWidget( view ); + + connect( view->selectionModel(), + &QItemSelectionModel::selectionChanged, + [this]( const QItemSelection& selected, const QItemSelection& ) { + auto i = selected.indexes(); + if ( !i.isEmpty() ) + { + auto row = i.first().row(); + auto* model = m_config->themeModel(); + auto id = model->data( model->index( row, 0 ), ThemesModel::KeyRole ).toString(); + if ( !id.isEmpty() ) + { + m_config->setTheme( id ); + } + } + } ); } diff --git a/src/modules/plasmalnf/PlasmaLnfPage.h b/src/modules/plasmalnf/PlasmaLnfPage.h index 287da8b29..f04ecde06 100644 --- a/src/modules/plasmalnf/PlasmaLnfPage.h +++ b/src/modules/plasmalnf/PlasmaLnfPage.h @@ -24,6 +24,8 @@ namespace Ui class PlasmaLnfPage; } +class Config; + /** @brief Page for selecting a Plasma Look-and-Feel theme. * * You must call setEnabledThemes -- either overload -- once @@ -34,40 +36,11 @@ class PlasmaLnfPage : public QWidget { Q_OBJECT public: - explicit PlasmaLnfPage( QWidget* parent = nullptr ); - - void setLnfPath( const QString& path ); - /** @brief enable only the listed themes. - * - * Shows the listed @p themes with full information (e.g. screenshot). - * If @p showAll is true, then also show all installed themes - * not explicitly listed (without a screenshot). - */ - void setEnabledThemes( const ThemeInfoList& themes, bool showAll ); - /** @brief enable all installed plasma themes. */ - void setEnabledThemesAll(); - /** @brief set which theme is to be preselected. */ - void setPreselect( const QString& id ); - -signals: - void plasmaThemeSelected( const QString& id ); + explicit PlasmaLnfPage( Config* config, QWidget* parent = nullptr ); private: - /** @brief Intersect the list of enabled themes with the installed ones. */ - void winnowThemes(); - /** @brief Get the translated names for all enabled themes. */ - void updateThemeNames(); - /** @brief show enabled themes in the UI. */ - void fillUi(); - Ui::PlasmaLnfPage* ui; - QString m_lnfPath; - QString m_preselect; - bool m_showAll; // If true, don't winnow according to enabledThemes - ThemeInfoList m_enabledThemes; - - QButtonGroup* m_buttonGroup; - QList< ThemeWidget* > m_widgets; + Config* m_config; }; #endif //PLASMALNFPAGE_H diff --git a/src/modules/plasmalnf/PlasmaLnfViewStep.cpp b/src/modules/plasmalnf/PlasmaLnfViewStep.cpp index 9bda0164b..fd38bfab3 100644 --- a/src/modules/plasmalnf/PlasmaLnfViewStep.cpp +++ b/src/modules/plasmalnf/PlasmaLnfViewStep.cpp @@ -8,40 +8,22 @@ */ #include "PlasmaLnfViewStep.h" -#include "PlasmaLnfJob.h" +#include "Config.h" #include "PlasmaLnfPage.h" #include "ThemeInfo.h" #include "utils/Logger.h" #include "utils/Variant.h" -#include #include -#ifdef WITH_KCONFIG -#include -#include -#endif - CALAMARES_PLUGIN_FACTORY_DEFINITION( PlasmaLnfViewStepFactory, registerPlugin< PlasmaLnfViewStep >(); ) -static QString -currentPlasmaTheme() -{ -#ifdef WITH_KCONFIG - KConfigGroup cg( KSharedConfig::openConfig( QStringLiteral( "kdeglobals" ) ), "KDE" ); - return cg.readEntry( "LookAndFeelPackage", QString() ); -#else - cWarning() << "No KConfig support, cannot determine Plasma theme."; - return QString(); -#endif -} - PlasmaLnfViewStep::PlasmaLnfViewStep( QObject* parent ) : Calamares::ViewStep( parent ) - , m_widget( new PlasmaLnfPage ) + , m_config( new Config( this ) ) + , m_widget( new PlasmaLnfPage( m_config ) ) { - connect( m_widget, &PlasmaLnfPage::plasmaThemeSelected, this, &PlasmaLnfViewStep::themeSelected ); emit nextStatusChanged( false ); } @@ -106,112 +88,12 @@ PlasmaLnfViewStep::onLeave() Calamares::JobList PlasmaLnfViewStep::jobs() const { - Calamares::JobList l; - - cDebug() << "Creating Plasma LNF jobs .."; - if ( !m_themeId.isEmpty() ) - { - if ( !m_lnfPath.isEmpty() ) - { - l.append( Calamares::job_ptr( new PlasmaLnfJob( m_lnfPath, m_themeId ) ) ); - } - else - { - cWarning() << "no lnftool given for plasmalnf module."; - } - } - return l; + return m_config->createJobs(); } void PlasmaLnfViewStep::setConfigurationMap( const QVariantMap& configurationMap ) { - m_lnfPath = CalamaresUtils::getString( configurationMap, "lnftool" ); - m_widget->setLnfPath( m_lnfPath ); - - if ( m_lnfPath.isEmpty() ) - { - cWarning() << "no lnftool given for plasmalnf module."; - } - - m_liveUser = CalamaresUtils::getString( configurationMap, "liveuser" ); - - QString preselect = CalamaresUtils::getString( configurationMap, "preselect" ); - if ( preselect == QStringLiteral( "*" ) ) - { - preselect = currentPlasmaTheme(); - } - if ( !preselect.isEmpty() ) - { - m_widget->setPreselect( preselect ); - } - - bool showAll = CalamaresUtils::getBool( configurationMap, "showAll", false ); - - if ( configurationMap.contains( "themes" ) && configurationMap.value( "themes" ).type() == QVariant::List ) - { - ThemeInfoList listedThemes; - auto themeList = configurationMap.value( "themes" ).toList(); - // Create the ThemInfo objects for the listed themes; information - // about the themes from Plasma (e.g. human-readable name and description) - // are filled in by update_names() in PlasmaLnfPage. - for ( const auto& i : themeList ) - if ( i.type() == QVariant::Map ) - { - auto iv = i.toMap(); - listedThemes.append( ThemeInfo( iv.value( "theme" ).toString(), iv.value( "image" ).toString() ) ); - } - else if ( i.type() == QVariant::String ) - { - listedThemes.append( ThemeInfo( i.toString() ) ); - } - - if ( listedThemes.length() == 1 ) - { - cWarning() << "only one theme enabled in plasmalnf"; - } - m_widget->setEnabledThemes( listedThemes, showAll ); - } - else - { - m_widget->setEnabledThemesAll(); // All of them - } -} - -void -PlasmaLnfViewStep::themeSelected( const QString& id ) -{ - m_themeId = id; - if ( m_lnfPath.isEmpty() ) - { - cWarning() << "no lnftool given for plasmalnf module."; - return; - } - - QProcess lnftool; - if ( !m_liveUser.isEmpty() ) - lnftool.start( "sudo", { "-E", "-H", "-u", m_liveUser, m_lnfPath, "--resetLayout", "--apply", id } ); - else - lnftool.start( m_lnfPath, { "--resetLayout", "--apply", id } ); - - if ( !lnftool.waitForStarted( 1000 ) ) - { - cWarning() << "could not start look-and-feel" << m_lnfPath; - return; - } - if ( !lnftool.waitForFinished() ) - { - cWarning() << m_lnfPath << "timed out."; - return; - } - - if ( ( lnftool.exitCode() == 0 ) && ( lnftool.exitStatus() == QProcess::NormalExit ) ) - { - cDebug() << "Plasma look-and-feel applied" << id; - } - else - { - cWarning() << "could not apply look-and-feel" << id; - } + m_config->setConfigurationMap( configurationMap ); } diff --git a/src/modules/plasmalnf/PlasmaLnfViewStep.h b/src/modules/plasmalnf/PlasmaLnfViewStep.h index 74de803c9..48f03cdad 100644 --- a/src/modules/plasmalnf/PlasmaLnfViewStep.h +++ b/src/modules/plasmalnf/PlasmaLnfViewStep.h @@ -14,6 +14,7 @@ #include "utils/PluginFactory.h" #include "viewpages/ViewStep.h" +class Config; class PlasmaLnfPage; class PLUGINDLLEXPORT PlasmaLnfViewStep : public Calamares::ViewStep @@ -40,14 +41,9 @@ public: void setConfigurationMap( const QVariantMap& configurationMap ) override; -public slots: - void themeSelected( const QString& id ); - private: + Config* m_config; PlasmaLnfPage* m_widget; - QString m_lnfPath; // Path to the lnf tool - QString m_themeId; // Id of selected theme - QString m_liveUser; // Name of the live user (for OEM mode) }; CALAMARES_PLUGIN_FACTORY_DECLARATION( PlasmaLnfViewStepFactory ) diff --git a/src/modules/plasmalnf/ThemeInfo.cpp b/src/modules/plasmalnf/ThemeInfo.cpp new file mode 100644 index 000000000..82da064be --- /dev/null +++ b/src/modules/plasmalnf/ThemeInfo.cpp @@ -0,0 +1,319 @@ +/* === This file is part of Calamares - === + * + * SPDX-FileCopyrightText: 2020 Adriaan de Groot + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Calamares is Free Software: see the License-Identifier above. + * + */ +#include "ThemeInfo.h" + +#include "Branding.h" +#include "utils/CalamaresUtilsGui.h" +#include "utils/Logger.h" + +#include +#include + +#include +#include +#include + +/** @brief describes a single plasma LnF theme. + * + * A theme description has an id, which is really the name of the desktop + * file (e.g. org.kde.breeze.desktop), a name which is human-readable and + * translated, and an optional image Page, which points to a local screenshot + * of that theme. + */ +struct ThemeInfo +{ + QString id; + QString name; + QString description; + QString imagePath; + mutable QPixmap pixmap; + bool show = true; + bool selected = false; + + ThemeInfo() {} + + explicit ThemeInfo( const QString& _id ) + : id( _id ) + { + } + + explicit ThemeInfo( const QString& _id, const QString& image ) + : id( _id ) + , imagePath( image ) + { + } + + explicit ThemeInfo( const KPluginMetaData& ); + + bool isValid() const { return !id.isEmpty(); } + + /// @brief Fill in the pixmap member based on imagePath + QPixmap loadImage() const; +}; + +class ThemeInfoList : public QList< ThemeInfo > +{ +public: + std::pair< int, const ThemeInfo* > indexById( const QString& id ) const + { + int index = 0; + for ( const ThemeInfo& i : *this ) + { + if ( i.id == id ) + { + return { index, &i }; + } + } + return { -1, nullptr }; + } + + std::pair< int, ThemeInfo* > indexById( const QString& id ) + { + // Call the const version and then munge the types + auto [ i, p ] = const_cast< const ThemeInfoList* >( this )->indexById( id ); + return { i, const_cast< ThemeInfo* >( p ) }; + } + + + /** @brief Looks for a given @p id in the list of themes, returns nullptr if not found. */ + ThemeInfo* findById( const QString& id ) + { + auto [ i, p ] = indexById( id ); + return p; + } + + /** @brief Looks for a given @p id in the list of themes, returns nullptr if not found. */ + const ThemeInfo* findById( const QString& id ) const + { + auto [ i, p ] = indexById( id ); + return p; + } + + /** @brief Checks if a given @p id is in the list of themes. */ + bool contains( const QString& id ) const { return findById( id ) != nullptr; } +}; + +ThemesModel::ThemesModel( QObject* parent ) + : QAbstractListModel( parent ) + , m_themes( new ThemeInfoList ) +{ + auto packages = KPackage::PackageLoader::self()->listPackages( "Plasma/LookAndFeel" ); + m_themes->reserve( packages.length() ); + + for ( const auto& p : packages ) + { + m_themes->append( ThemeInfo { p } ); + } +} + +int +ThemesModel::rowCount( const QModelIndex& ) const +{ + return m_themes->count(); +} + +QVariant +ThemesModel::data( const QModelIndex& index, int role ) const +{ + if ( !index.isValid() ) + { + return QVariant(); + } + if ( index.row() < 0 || index.row() >= m_themes->count() ) + { + return QVariant(); + } + + const auto& item = m_themes->at( index.row() ); + switch ( role ) + { + case LabelRole: + return item.name; + case KeyRole: + return item.id; + case ShownRole: + return item.show; + case SelectedRole: + return item.selected; + case DescriptionRole: + return item.description; + case ImageRole: + return item.loadImage(); + default: + return QVariant(); + } + __builtin_unreachable(); +} + +QHash< int, QByteArray > +ThemesModel::roleNames() const +{ + return { { LabelRole, "label" }, + { KeyRole, "key" }, + { SelectedRole, "selected" }, + { ShownRole, "show" }, + { ImageRole, "image" } }; +} + +void +ThemesModel::setThemeImage( const QString& id, const QString& imagePath ) +{ + auto [ i, theme ] = m_themes->indexById( id ); + if ( theme ) + { + theme->imagePath = imagePath; + emit dataChanged( index( i, 0 ), index( i, 0 ), { ImageRole } ); + } +} + +void +ThemesModel::setThemeImage( const QMap< QString, QString >& images ) +{ + if ( m_themes->isEmpty() ) + { + return; + } + + // Don't emit signals from each call, aggregate to one call (below this block) + { + QSignalBlocker b( this ); + for ( auto k = images.constKeyValueBegin(); k != images.constKeyValueEnd(); ++k ) + { + setThemeImage( k->first, k->second ); + } + } + emit dataChanged( index( 0, 0 ), index( m_themes->count() - 1 ), { ImageRole } ); +} + +void +ThemesModel::showTheme( const QString& id, bool show ) +{ + auto [ i, theme ] = m_themes->indexById( id ); + if ( theme ) + { + theme->show = show; + emit dataChanged( index( i, 0 ), index( i, 0 ), { ShownRole } ); + } +} + +void +ThemesModel::showOnlyThemes( const QMap< QString, QString >& onlyThese ) +{ + if ( m_themes->isEmpty() ) + { + return; + } + + // No signal blocker block needed here because we're not calling showTheme() + // QSignalBlocker b( this ); + for ( auto& t : *m_themes ) + { + t.show = onlyThese.contains( t.id ); + } + emit dataChanged( index( 0, 0 ), index( m_themes->count() - 1 ), { ShownRole } ); +} + +QSize +ThemesModel::imageSize() +{ + return { qMax( 12 * CalamaresUtils::defaultFontHeight(), 120 ), + qMax( 8 * CalamaresUtils::defaultFontHeight(), 80 ) }; +} + +void +ThemesModel::select( const QString& themeId ) +{ + int i = 0; + for ( auto& t : *m_themes ) + { + if ( t.selected && t.id != themeId ) + { + t.selected = false; + emit dataChanged( index( i, 0 ), index( i, 0 ), { SelectedRole } ); + } + if ( !t.selected && t.id == themeId ) + { + t.selected = true; + emit dataChanged( index( i, 0 ), index( i, 0 ), { SelectedRole } ); + } + ++i; + } +} + + +/** + * Massage the given @p path to the most-likely + * path that actually contains a screenshot. For + * empty image paths, returns the QRC path for an + * empty screenshot. Returns blank if the path + * doesn't exist anywhere in the search paths. + */ +static QString +munge_imagepath( const QString& path ) +{ + if ( path.isEmpty() ) + { + return ":/view-preview.png"; + } + + if ( path.startsWith( '/' ) ) + { + return path; + } + + if ( QFileInfo::exists( path ) ) + { + return path; + } + + QFileInfo fi( QDir( Calamares::Branding::instance()->componentDirectory() ), path ); + if ( fi.exists() ) + { + return fi.absoluteFilePath(); + } + + return QString(); +} + +ThemeInfo::ThemeInfo( const KPluginMetaData& data ) + : id( data.pluginId() ) + , name( data.name() ) + , description( data.description() ) +{ +} + +QPixmap +ThemeInfo::loadImage() const +{ + if ( pixmap.isNull() ) + { + + const QSize image_size( ThemesModel::imageSize() ); + + const QString path = munge_imagepath( imagePath ); + cDebug() << "Loading initial image for" << id << imagePath << "->" << path; + QPixmap image( path ); + if ( image.isNull() ) + { + // Not found or not specified, so convert the name into some (horrible, likely) + // color instead. + image = QPixmap( image_size ); + auto hash_color = qHash( imagePath.isEmpty() ? id : imagePath ); + cDebug() << Logger::SubEntry << "Theme image" << imagePath << "not found, hash" << hash_color; + image.fill( QColor( QRgb( hash_color ) ) ); + } + else + { + cDebug() << Logger::SubEntry << "Theme image" << image.size(); + } + + pixmap = image.scaled( image_size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation ); + } + return pixmap; +} diff --git a/src/modules/plasmalnf/ThemeInfo.h b/src/modules/plasmalnf/ThemeInfo.h index 6848fb8c3..c859bcd2d 100644 --- a/src/modules/plasmalnf/ThemeInfo.h +++ b/src/modules/plasmalnf/ThemeInfo.h @@ -10,82 +10,66 @@ #ifndef PLASMALNF_THEMEINFO_H #define PLASMALNF_THEMEINFO_H +#include #include #include -class KPluginMetaData; -class ThemeWidget; +class ThemeInfoList; -/** @brief describes a single plasma LnF theme. - * - * A theme description has an id, which is really the name of the desktop - * file (e.g. org.kde.breeze.desktop), a name which is human-readable and - * translated, and an optional image Page, which points to a local screenshot - * of that theme. - */ -struct ThemeInfo +class ThemesModel : public QAbstractListModel { - QString id; - QString name; - QString description; - QString imagePath; - ThemeWidget* widget; + Q_OBJECT - ThemeInfo() - : widget( nullptr ) - { - } - - explicit ThemeInfo( const QString& _id ) - : id( _id ) - , widget( nullptr ) - { - } - - explicit ThemeInfo( const QString& _id, const QString& image ) - : id( _id ) - , imagePath( image ) - , widget( nullptr ) - { - } - - // Defined in PlasmaLnfPage.cpp - explicit ThemeInfo( const KPluginMetaData& ); - - bool isValid() const { return !id.isEmpty(); } -}; - -class ThemeInfoList : public QList< ThemeInfo > -{ public: - /** @brief Looks for a given @p id in the list of themes, returns nullptr if not found. */ - ThemeInfo* findById( const QString& id ) + enum { - for ( ThemeInfo& i : *this ) - { - if ( i.id == id ) - { - return &i; - } - } - return nullptr; - } + LabelRole = Qt::DisplayRole, + KeyRole = Qt::UserRole, + ShownRole, // Should theme be displayed + SelectedRole, // Is theme selected + DescriptionRole, + ImageRole + }; - /** @brief Looks for a given @p id in the list of themes, returns nullptr if not found. */ - const ThemeInfo* findById( const QString& id ) const - { - for ( const ThemeInfo& i : *this ) - { - if ( i.id == id ) - { - return &i; - } - } - return nullptr; - } + explicit ThemesModel( QObject* parent ); - /** @brief Checks if a given @p id is in the list of themes. */ - bool contains( const QString& id ) const { return findById( id ) != nullptr; } + int rowCount( const QModelIndex& = QModelIndex() ) const override; + QVariant data( const QModelIndex& index, int role ) const override; + + QHash< int, QByteArray > roleNames() const override; + + /// @brief Set the screenshot to go with the given @p id + void setThemeImage( const QString& id, const QString& imagePath ); + + /// @brief Call setThemeImage( key, value ) for all keys in @p images + void setThemeImage( const QMap< QString, QString >& images ); + + /// @brief Set whether to show the given theme @p id (or not) + void showTheme( const QString& id, bool show = true ); + + /// @brief Shows the keys in the @p onlyThese map, and hides the rest + void showOnlyThemes( const QMap< QString, QString >& onlyThese ); + + /** @brief Mark the @p themeId as current / selected + * + * One theme can be selected at a time; this will emit data + * changed signals for any (one) theme already selected, and + * the newly-selected theme. If @p themeId does not name any + * theme, none are selected. + */ + void select( const QString& themeId ); + + /** @brief The size of theme Images + * + * The size is dependent on the font size used by Calamares, + * and is constant within one run of Calamares, but may change + * if the font settings do between runs. + */ + static QSize imageSize(); + +private: + ThemeInfoList* m_themes; }; + #endif diff --git a/src/modules/plasmalnf/page_plasmalnf.ui b/src/modules/plasmalnf/page_plasmalnf.ui index 88d5a39a2..b079013d6 100644 --- a/src/modules/plasmalnf/page_plasmalnf.ui +++ b/src/modules/plasmalnf/page_plasmalnf.ui @@ -28,19 +28,6 @@ SPDX-License-Identifier: GPL-3.0-or-later - - - - Qt::Vertical - - - - 20 - 40 - - - - diff --git a/src/modules/plasmalnf/plasmalnf.conf b/src/modules/plasmalnf/plasmalnf.conf index fd59389a8..105f247ef 100644 --- a/src/modules/plasmalnf/plasmalnf.conf +++ b/src/modules/plasmalnf/plasmalnf.conf @@ -24,10 +24,18 @@ lnftool: "/usr/bin/lookandfeeltool" # # liveuser: "live" +# If *showAll* is true, then all installed themes are shown in the +# UI for selection, even if they are not listed in *themes* (below). +# This allows selection of all themes even while not all of them are +# listed in *themes* -- which is useful to show screenshots for those +# you do have a screenshot for. If *themes* is empty or missing, +# the value of *showAll* is treated as `true`. +showAll: false + # You can limit the list of Plasma look-and-feel themes by listing ids # here. If this key is not present, all of the installed themes are listed. # If the key is present, only installed themes that are **also** included -# in the list are shown (could be none!). See the *showAll* key, below, +# in the list are shown (could be none!). See the *showAll* key, above, # to change that. # # Themes may be listed by id, (e.g. fluffy-bunny, below) or as a theme @@ -58,13 +66,6 @@ themes: image: "breeze-dark.png" - org.kde.fluffy-bunny.desktop -# If *showAll* is true, then all installed themes are shown in the -# UI for selection, even if they are not listed in *themes*. This -# allows selection of all themes even while not all of them are -# listed in *themes* -- which is useful to show screenshots for those -# you do have a screenshot for. -showAll: false - # You can pre-select one of the themes; it is not applied # immediately, but its radio-button is switched on to indicate # that that is the theme (that is most likely) currently in use.