Merge branch 'software-chooser'

FIXES #426
FIXES #1172
FIXES #706
This commit is contained in:
Adriaan de Groot 2019-08-06 22:55:13 +02:00
commit 5bdcc2c7a8
21 changed files with 1436 additions and 8 deletions

View File

@ -198,7 +198,7 @@ if( CMAKE_CXX_COMPILER_ID MATCHES "Clang" )
) )
string( APPEND CMAKE_CXX_FLAGS " ${CLANG_WARNINGS}" ) string( APPEND CMAKE_CXX_FLAGS " ${CLANG_WARNINGS}" )
endforeach() endforeach()
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DNOTREACHED='//'" ) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DNOTREACHED='//' -DFALLTHRU='[[clang::fallthrough]]'")
# Third-party code where we don't care so much about compiler warnings # Third-party code where we don't care so much about compiler warnings
# (because it's uncomfortable to patch) get different flags; use # (because it's uncomfortable to patch) get different flags; use
@ -225,7 +225,7 @@ else()
set( SUPPRESS_3RDPARTY_WARNINGS "" ) set( SUPPRESS_3RDPARTY_WARNINGS "" )
set( SUPPRESS_BOOST_WARNINGS "" ) set( SUPPRESS_BOOST_WARNINGS "" )
set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DNOTREACHED='__builtin_unreachable();'" ) set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DNOTREACHED='__builtin_unreachable();' -DFALLTHRU='/* */'" )
endif() endif()
# Use mark_thirdparty_code() to reduce warnings from the compiler # Use mark_thirdparty_code() to reduce warnings from the compiler

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<component type="desktop">
<id>io.calamares.calamares.desktop</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0+</project_license>
<name>Calamares</name>
<name xml:lang="da">Calamares</name>
<name xml:lang="en-GB">Calamares</name>
<name xml:lang="nl">Calamares</name>
<summary>Calamares Linux Installer</summary>
<summary xml:lang="en-GB">Calamares Linux Installer</summary>
<summary xml:lang="nl">Linux Installatieprogramma Calamares</summary>
<description>
<p>Calamares is an installer program for Linux distributions.</p>
<p xml:lang="nl">Calamares is een installatieprogramma voor Linux distributies.</p>
</description>
<url type="homepage">https://calamares.io</url>
<url type="bugtracker">https://https://github.com/calamares/calamares/issues/</url>
<url type="help">https://github.com/calamares/calamares/wiki</url>
<screenshots>
<screenshot type="default">
<caption>Calamares Welcome</caption>
<image>https://calamares.io/images/cal_640.png</image>
</screenshot>
</screenshots>
<provides>
<binary>calamares</binary>
</provides>
</component>

View File

@ -96,6 +96,10 @@ someLanguages()
} }
/** @brief Check consistency of test data
* Check that all the languages used in testing, are actually enabled
* in Calamares translations.
*/
void void
LocaleTests::testTranslatableLanguages() LocaleTests::testTranslatableLanguages()
{ {
@ -108,12 +112,19 @@ LocaleTests::testTranslatableLanguages()
} }
} }
/** @brief Test strings with no translations
*/
void void
LocaleTests::testTranslatableConfig1() LocaleTests::testTranslatableConfig1()
{ {
CalamaresUtils::Locale::TranslatedString ts0;
QVERIFY( ts0.isEmpty() );
QCOMPARE( ts0.count(), 1 ); // the empty string
QCOMPARE( QLocale().name(), "C" ); // Otherwise plain get() is dubious QCOMPARE( QLocale().name(), "C" ); // Otherwise plain get() is dubious
CalamaresUtils::Locale::TranslatedString ts1( "Hello" ); CalamaresUtils::Locale::TranslatedString ts1( "Hello" );
QCOMPARE( ts1.count(), 1 ); QCOMPARE( ts1.count(), 1 );
QVERIFY( !ts1.isEmpty() );
QCOMPARE( ts1.get(), "Hello" ); QCOMPARE( ts1.get(), "Hello" );
QCOMPARE( ts1.get( QLocale( "nl" ) ), "Hello" ); QCOMPARE( ts1.get( QLocale( "nl" ) ), "Hello" );
@ -122,11 +133,14 @@ LocaleTests::testTranslatableConfig1()
map.insert( "description", "description (no language)" ); map.insert( "description", "description (no language)" );
CalamaresUtils::Locale::TranslatedString ts2( map, "description" ); CalamaresUtils::Locale::TranslatedString ts2( map, "description" );
QCOMPARE( ts2.count(), 1 ); QCOMPARE( ts2.count(), 1 );
QVERIFY( !ts2.isEmpty() );
QCOMPARE( ts2.get(), "description (no language)" ); QCOMPARE( ts2.get(), "description (no language)" );
QCOMPARE( ts2.get( QLocale( "nl" ) ), "description (no language)" ); QCOMPARE( ts2.get( QLocale( "nl" ) ), "description (no language)" );
} }
/** @bref Test strings with translations.
*/
void void
LocaleTests::testTranslatableConfig2() LocaleTests::testTranslatableConfig2()
{ {
@ -143,11 +157,22 @@ LocaleTests::testTranslatableConfig2()
} }
} }
// If there's no untranslated string in the map, it is considered empty
CalamaresUtils::Locale::TranslatedString ts0( map, "description" );
QVERIFY( ts0.isEmpty() ); // Because no untranslated string
QCOMPARE( ts0.count(),
someLanguages().count() + 1 ); // But there are entries for the translations, plus an empty string
// expand the map with untranslated entries
map.insert( QString( "description" ), "description (no language)" );
map.insert( QString( "name" ), "name (no language)" );
CalamaresUtils::Locale::TranslatedString ts1( map, "description" ); CalamaresUtils::Locale::TranslatedString ts1( map, "description" );
// The +1 is because "" is always also inserted // The +1 is because "" is always also inserted
QCOMPARE( ts1.count(), someLanguages().count() + 1 ); QCOMPARE( ts1.count(), someLanguages().count() + 1 );
QVERIFY( !ts1.isEmpty() );
QCOMPARE( ts1.get(), "description" ); // it wasn't set QCOMPARE( ts1.get(), "description (no language)" ); // it wasn't set
QCOMPARE( ts1.get( QLocale( "nl" ) ), "description (language nl)" ); QCOMPARE( ts1.get( QLocale( "nl" ) ), "description (language nl)" );
for ( const auto& language : someLanguages() ) for ( const auto& language : someLanguages() )
{ {
@ -167,4 +192,10 @@ LocaleTests::testTranslatableConfig2()
CalamaresUtils::Locale::TranslatedString ts2( map, "name" ); CalamaresUtils::Locale::TranslatedString ts2( map, "name" );
// We skipped dutch this time // We skipped dutch this time
QCOMPARE( ts2.count(), someLanguages().count() ); QCOMPARE( ts2.count(), someLanguages().count() );
QVERIFY( !ts2.isEmpty() );
// This key doesn't exist
CalamaresUtils::Locale::TranslatedString ts3( map, "front" );
QVERIFY( ts3.isEmpty() );
QCOMPARE( ts3.count(), 1 ); // The empty string
} }

View File

@ -38,10 +38,6 @@ TranslatedString::TranslatedString( const QVariantMap& map, const QString& key )
{ {
// Get the un-decorated value for the key // Get the un-decorated value for the key
QString value = CalamaresUtils::getString( map, key ); QString value = CalamaresUtils::getString( map, key );
if ( value.isEmpty() )
{
value = key;
}
m_strings[ QString() ] = value; m_strings[ QString() ] = value;
for ( auto it = map.constKeyValueBegin(); it != map.constKeyValueEnd(); ++it ) for ( auto it = map.constKeyValueBegin(); it != map.constKeyValueEnd(); ++it )
@ -79,7 +75,6 @@ TranslatedString::get( const QLocale& locale ) const
localeName = QStringLiteral( "sr@latin" ); localeName = QStringLiteral( "sr@latin" );
} }
cDebug() << "Getting locale" << localeName;
if ( m_strings.contains( localeName ) ) if ( m_strings.contains( localeName ) )
{ {
return m_strings[ localeName ]; return m_strings[ localeName ];

View File

@ -44,8 +44,25 @@ public:
/** @brief Not-actually-translated string. /** @brief Not-actually-translated string.
*/ */
TranslatedString( const QString& string ); TranslatedString( const QString& string );
/// @brief Empty string
TranslatedString()
: TranslatedString( QString() )
{
}
/** @brief How many strings (translations) are there?
*
* This is always at least 1 (for the untranslated string),
* but may be more than 1 even when isEmpty() is true --
* if there is no untranslated version, for instance.
*/
int count() const { return m_strings.count(); } int count() const { return m_strings.count(); }
/** @brief Consider this string empty?
*
* Only the state of the untranslated string is considered,
* so count() may be more than 1 even while the string is empty.
*/
bool isEmpty() const { return m_strings[ QString() ].isEmpty(); }
/// @brief Gets the string in the current locale /// @brief Gets the string in the current locale
QString get() const; QString get() const;

View File

@ -0,0 +1,45 @@
find_package( Qt5 COMPONENTS Core Gui Widgets REQUIRED )
set( _extra_libraries "" )
### OPTIONAL AppData XML support in PackageModel
#
#
find_package(Qt5 COMPONENTS Xml)
if ( Qt5Xml_FOUND )
add_definitions( -DHAVE_XML )
list( APPEND _extra_libraries Qt5::Xml )
endif()
calamares_add_plugin( packagechooser
TYPE viewmodule
EXPORT_MACRO PLUGINDLLEXPORT_PRO
SOURCES
PackageChooserPage.cpp
PackageChooserViewStep.cpp
PackageModel.cpp
RESOURCES
packagechooser.qrc
UI
page_package.ui
LINK_PRIVATE_LIBRARIES
calamaresui
${_extra_libraries}
SHARED_LIB
)
if( ECM_FOUND AND BUILD_TESTING )
ecm_add_test(
Tests.cpp
TEST_NAME
packagechoosertest
LINK_LIBRARIES
${CALAMARES_LIBRARIES}
calamares_viewmodule_packagechooser
Qt5::Core
Qt5::Test
Qt5::Gui
${_extra_libraries}
)
calamares_automoc( packagechoosertest)
endif()

View File

@ -0,0 +1,143 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019, Adriaan de Groot <groot@kde.org>
*
* Calamares 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 3 of the License, or
* (at your option) any later version.
*
* Calamares 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 Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PackageChooserPage.h"
#include "ui_page_package.h"
#include "utils/Logger.h"
#include "utils/Retranslator.h"
#include <QLabel>
PackageChooserPage::PackageChooserPage( PackageChooserMode mode, QWidget* parent )
: QWidget( parent )
, ui( new Ui::PackageChooserPage )
, m_introduction( QString(),
QString(),
tr( "Package Selection" ),
tr( "Please pick a product from the list. The selected product will be installed." ) )
{
m_introduction.screenshot = QPixmap( QStringLiteral( ":/images/no-selection.png" ) );
ui->setupUi( this );
CALAMARES_RETRANSLATE( updateLabels(); )
switch ( mode )
{
case PackageChooserMode::Optional:
FALLTHRU;
case PackageChooserMode::Required:
ui->products->setSelectionMode( QAbstractItemView::SingleSelection );
break;
case PackageChooserMode::OptionalMultiple:
FALLTHRU;
case PackageChooserMode::RequiredMultiple:
ui->products->setSelectionMode( QAbstractItemView::ExtendedSelection );
}
}
void
PackageChooserPage::currentChanged( const QModelIndex& index )
{
if ( !index.isValid() || !ui->products->selectionModel()->hasSelection() )
{
ui->productName->setText( m_introduction.name.get() );
ui->productScreenshot->setPixmap( m_introduction.screenshot );
ui->productDescription->setText( m_introduction.description.get() );
}
else
{
const auto* model = ui->products->model();
ui->productName->setText( model->data( index, PackageListModel::NameRole ).toString() );
ui->productScreenshot->setPixmap( model->data( index, PackageListModel::ScreenshotRole ).value< QPixmap >() );
ui->productDescription->setText( model->data( index, PackageListModel::DescriptionRole ).toString() );
}
}
void
PackageChooserPage::updateLabels()
{
if ( ui && ui->products && ui->products->selectionModel() )
{
currentChanged( ui->products->selectionModel()->currentIndex() );
}
else
{
currentChanged( QModelIndex() );
}
emit selectionChanged();
}
void
PackageChooserPage::setModel( QAbstractItemModel* model )
{
ui->products->setModel( model );
// Check if any of the items in the model is the "none" option.
// If so, copy its values into the introduction / none item.
for ( int r = 0; r < model->rowCount(); ++r )
{
auto index = model->index( r, 0 );
if ( index.isValid() )
{
QVariant v = model->data( index, PackageListModel::IdRole );
if ( v.isValid() && v.toString().isEmpty() )
{
m_introduction.name = model->data( index, PackageListModel::NameRole ).toString();
m_introduction.description = model->data( index, PackageListModel::DescriptionRole ).toString();
m_introduction.screenshot = model->data( index, PackageListModel::ScreenshotRole ).value< QPixmap >();
currentChanged( QModelIndex() );
break;
}
}
}
connect( ui->products->selectionModel(),
&QItemSelectionModel::selectionChanged,
this,
&PackageChooserPage::updateLabels );
}
bool
PackageChooserPage::hasSelection() const
{
return ui && ui->products && ui->products->selectionModel() && ui->products->selectionModel()->hasSelection();
}
QStringList
PackageChooserPage::selectedPackageIds() const
{
if ( !( ui && ui->products && ui->products->selectionModel() ) )
{
return QStringList();
}
const auto* model = ui->products->model();
QStringList ids;
for ( const auto& index : ui->products->selectionModel()->selectedIndexes() )
{
QString pid = model->data( index, PackageListModel::IdRole ).toString();
if ( !pid.isEmpty() )
{
ids.append( pid );
}
}
return ids;
}

View File

@ -0,0 +1,60 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019, Adriaan de Groot <groot@kde.org>
*
* Calamares 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 3 of the License, or
* (at your option) any later version.
*
* Calamares 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 Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PACKAGECHOOSERPAGE_H
#define PACKAGECHOOSERPAGE_H
#include "PackageModel.h"
#include <QAbstractItemModel>
#include <QWidget>
namespace Ui
{
class PackageChooserPage;
}
class PackageChooserPage : public QWidget
{
Q_OBJECT
public:
explicit PackageChooserPage( PackageChooserMode mode, QWidget* parent = nullptr );
void setModel( QAbstractItemModel* model );
/// @brief Is anything selected?
bool hasSelection() const;
/** @brief Get the list of selected ids
*
* This list may be empty (if none is selected).
*/
QStringList selectedPackageIds() const;
public slots:
void currentChanged( const QModelIndex& index );
void updateLabels();
signals:
void selectionChanged();
private:
Ui::PackageChooserPage* ui;
PackageItem m_introduction;
};
#endif // PACKAGECHOOSERPAGE_H

View File

@ -0,0 +1,256 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019, Adriaan de Groot <groot@kde.org>
*
* Calamares 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 3 of the License, or
* (at your option) any later version.
*
* Calamares 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 Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PackageChooserViewStep.h"
#include "PackageChooserPage.h"
#include "PackageModel.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "utils/CalamaresUtilsSystem.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include <QDesktopServices>
#include <QVariantMap>
CALAMARES_PLUGIN_FACTORY_DEFINITION( PackageChooserViewStepFactory, registerPlugin< PackageChooserViewStep >(); )
PackageChooserViewStep::PackageChooserViewStep( QObject* parent )
: Calamares::ViewStep( parent )
, m_widget( nullptr )
, m_model( nullptr )
, m_mode( PackageChooserMode::Required )
{
emit nextStatusChanged( false );
}
PackageChooserViewStep::~PackageChooserViewStep()
{
if ( m_widget && m_widget->parent() == nullptr )
{
m_widget->deleteLater();
}
delete m_model;
}
QString
PackageChooserViewStep::prettyName() const
{
return tr( "Packages" );
}
QWidget*
PackageChooserViewStep::widget()
{
if ( !m_widget )
{
m_widget = new PackageChooserPage( m_mode, nullptr );
connect( m_widget, &PackageChooserPage::selectionChanged, [=]() {
emit nextStatusChanged( this->isNextEnabled() );
} );
if ( m_model )
{
hookupModel();
}
else
{
cWarning() << "PackageChooser Widget created before model.";
}
}
return m_widget;
}
bool
PackageChooserViewStep::isNextEnabled() const
{
if ( !m_model )
{
return false;
}
if ( !m_widget )
{
// No way to have changed anything
return true;
}
switch ( m_mode )
{
case PackageChooserMode::Optional:
case PackageChooserMode::OptionalMultiple:
// zero or one OR zero or more
return true;
case PackageChooserMode::Required:
case PackageChooserMode::RequiredMultiple:
// exactly one OR one or more
return m_widget->hasSelection();
}
NOTREACHED return true;
}
bool
PackageChooserViewStep::isBackEnabled() const
{
return true;
}
bool
PackageChooserViewStep::isAtBeginning() const
{
return true;
}
bool
PackageChooserViewStep::isAtEnd() const
{
return true;
}
void
PackageChooserViewStep::onLeave()
{
QString key = QStringLiteral( "packagechooser_%1" ).arg( m_id );
QString value;
if ( m_widget->hasSelection() )
{
value = m_widget->selectedPackageIds().join( ',' );
}
Calamares::JobQueue::instance()->globalStorage()->insert( key, value );
cDebug() << "PackageChooser" << key << "selected" << value;
}
Calamares::JobList
PackageChooserViewStep::jobs() const
{
Calamares::JobList l;
return l;
}
void
PackageChooserViewStep::setConfigurationMap( const QVariantMap& configurationMap )
{
QString mode = CalamaresUtils::getString( configurationMap, "mode" );
bool ok = false;
if ( !mode.isEmpty() )
{
m_mode = roleNames().find( mode, ok );
}
if ( !ok )
{
m_mode = PackageChooserMode::Required;
}
m_id = CalamaresUtils::getString( configurationMap, "id" );
if ( m_id.isEmpty() )
{
// Not set, so use the instance id
// TODO: use a stronger type than QString for structured IDs
m_id = moduleInstanceKey().split( '@' ).last();
}
bool first_time = !m_model;
if ( configurationMap.contains( "items" ) )
{
fillModel( configurationMap.value( "items" ).toList() );
}
// TODO: replace this hard-coded model
if ( !m_model )
{
m_model = new PackageListModel( nullptr );
m_model->addPackage( PackageItem { QString(),
QString(),
"No Desktop",
"Please pick a desktop environment from the list. "
"If you don't want to install a desktop, that's fine, "
"your system will start up in text-only mode and you can "
"install a desktop environment later.",
":/images/no-selection.png" } );
m_model->addPackage( PackageItem { "kde", "kde", "Plasma", "Plasma Desktop", ":/images/kde.png" } );
m_model->addPackage( PackageItem {
"gnome", "gnome", "GNOME", "GNU Networked Object Modeling Environment Desktop", ":/images/gnome.png" } );
}
if ( first_time && m_widget && m_model )
{
hookupModel();
}
}
void
PackageChooserViewStep::fillModel( const QVariantList& items )
{
if ( !m_model )
{
m_model = new PackageListModel( nullptr );
}
if ( items.isEmpty() )
{
cWarning() << "No *items* for PackageChooser module.";
return;
}
cDebug() << "Loading PackageChooser model items from config";
int item_index = 0;
for ( const auto& item_it : items )
{
++item_index;
QVariantMap item_map = item_it.toMap();
if ( item_map.isEmpty() )
{
cWarning() << "PackageChooser entry" << item_index << "is not valid.";
continue;
}
if ( item_map.contains( "appdata" ) )
{
m_model->addPackage( PackageItem::fromAppData( item_map ) );
}
else
{
m_model->addPackage( PackageItem( item_map ) );
}
}
}
void
PackageChooserViewStep::hookupModel()
{
if ( !m_model || !m_widget )
{
cError() << "Can't hook up model until widget and model both exist.";
return;
}
m_widget->setModel( m_model );
}

View File

@ -0,0 +1,72 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019, Adriaan de Groot <groot@kde.org>
*
* Calamares 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 3 of the License, or
* (at your option) any later version.
*
* Calamares 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 Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PACKAGECHOOSERVIEWSTEP_H
#define PACKAGECHOOSERVIEWSTEP_H
#include "PluginDllMacro.h"
#include "utils/PluginFactory.h"
#include "viewpages/ViewStep.h"
#include "PackageModel.h"
#include <QObject>
#include <QUrl>
#include <QVariantMap>
class PackageChooserPage;
class PLUGINDLLEXPORT PackageChooserViewStep : public Calamares::ViewStep
{
Q_OBJECT
public:
explicit PackageChooserViewStep( QObject* parent = nullptr );
virtual ~PackageChooserViewStep() override;
QString prettyName() const override;
QWidget* widget() override;
bool isNextEnabled() const override;
bool isBackEnabled() const override;
bool isAtBeginning() const override;
bool isAtEnd() const override;
void onLeave() override;
Calamares::JobList jobs() const override;
void setConfigurationMap( const QVariantMap& configurationMap ) override;
private:
void fillModel( const QVariantList& items );
void hookupModel();
PackageChooserPage* m_widget;
PackageListModel* m_model;
// Configuration
PackageChooserMode m_mode;
QString m_id;
};
CALAMARES_PLUGIN_FACTORY_DECLARATION( PackageChooserViewStepFactory )
#endif // PACKAGECHOOSERVIEWSTEP_H

View File

@ -0,0 +1,374 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019, Adriaan de Groot <groot@kde.org>
*
* Calamares 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 3 of the License, or
* (at your option) any later version.
*
* Calamares 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 Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#include "PackageModel.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#ifdef HAVE_XML
#include <QDomDocument>
#include <QDomNodeList>
#include <QFile>
#endif
const NamedEnumTable< PackageChooserMode >&
roleNames()
{
static const NamedEnumTable< PackageChooserMode > names {
{ "optional", PackageChooserMode::Optional },
{ "required", PackageChooserMode::Required },
{ "optionalmultiple", PackageChooserMode::OptionalMultiple },
{ "requiredmultiple", PackageChooserMode::RequiredMultiple },
// and a bunch of aliases
{ "zero-or-one", PackageChooserMode::Optional },
{ "radio", PackageChooserMode::Required },
{ "one", PackageChooserMode::Required },
{ "set", PackageChooserMode::OptionalMultiple },
{ "zero-or-more", PackageChooserMode::OptionalMultiple },
{ "multiple", PackageChooserMode::RequiredMultiple },
{ "one-or-more", PackageChooserMode::RequiredMultiple }
};
return names;
}
PackageItem::PackageItem() {}
PackageItem::PackageItem( const QString& a_id,
const QString& a_package,
const QString& a_name,
const QString& a_description )
: id( a_id )
, package( a_package )
, name( a_name )
, description( a_description )
{
}
PackageItem::PackageItem( const QString& a_id,
const QString& a_package,
const QString& a_name,
const QString& a_description,
const QString& screenshotPath )
: id( a_id )
, package( a_package )
, name( a_name )
, description( a_description )
, screenshot( screenshotPath )
{
}
PackageItem::PackageItem::PackageItem( const QVariantMap& item_map )
: id( CalamaresUtils::getString( item_map, "id" ) )
, package( CalamaresUtils::getString( item_map, "package" ) )
, name( CalamaresUtils::Locale::TranslatedString( item_map, "name" ) )
, description( CalamaresUtils::Locale::TranslatedString( item_map, "description" ) )
, screenshot( CalamaresUtils::getString( item_map, "screenshot" ) )
{
if ( name.isEmpty() && id.isEmpty() )
{
name = QObject::tr( "No product" );
}
else if ( name.isEmpty() )
{
cWarning() << "PackageChooser item" << id << "has an empty name.";
}
if ( description.isEmpty() )
{
description = QObject::tr( "No description provided." );
}
}
#ifdef HAVE_XML
/** @brief try to load the given @p fileName XML document
*
* Returns a QDomDocument, which will be valid iff the file can
* be read and contains valid XML data.
*/
static inline QDomDocument
loadAppData( const QString& fileName )
{
QFile file( fileName );
if ( !file.open( QIODevice::ReadOnly ) )
{
return QDomDocument();
}
QDomDocument doc( "AppData" );
if ( !doc.setContent( &file ) )
{
file.close();
return QDomDocument();
}
file.close();
return doc;
}
/** @brief gets the text of child element @p tagName
*/
static inline QString
getChildText( const QDomNode& n, const QString& tagName )
{
QDomElement e = n.firstChildElement( tagName );
return e.isNull() ? QString() : e.text();
}
/** @brief Gets a suitable screenshot path
*
* The <screenshots> element contains zero or more <screenshot>
* elements, which can have a *type* associated with them.
* Scan the screenshot elements, return the <image> path
* for the one labeled with type=default or, if there is no
* default, the first element.
*/
static inline QString
getScreenshotPath( const QDomNode& n )
{
QDomElement shotsNode = n.firstChildElement( "screenshots" );
if ( shotsNode.isNull() )
{
return QString();
}
const QDomNodeList shotList = shotsNode.childNodes();
int firstScreenshot = -1; // Use which screenshot node?
for ( int i = 0; i < shotList.count(); ++i )
{
if ( !shotList.at( i ).isElement() )
{
continue;
}
QDomElement e = shotList.at( i ).toElement();
if ( e.tagName() != "screenshot" )
{
continue;
}
// If none has the "type=default" attribute, use the first one
if ( firstScreenshot < 0 )
{
firstScreenshot = i;
}
// But type=default takes precedence.
if ( e.hasAttribute( "type" ) && e.attribute( "type" ) == "default" )
{
firstScreenshot = i;
break;
}
}
if ( firstScreenshot >= 0 )
{
return shotList.at( firstScreenshot ).firstChildElement( "image" ).text();
}
return QString();
}
/** @brief Returns language of the given element @p e
*
* Transforms the attribute value for xml:lang to something
* suitable for TranslatedString (e.g. [lang]).
*/
static inline QString
getLanguage( const QDomElement& e )
{
QString language = e.attribute( "xml:lang" );
if ( !language.isEmpty() )
{
language.replace( '-', '_' );
language.prepend( '[' );
language.append( ']' );
}
return language;
}
/** @brief Scan the list of @p children for @p tagname elements and add them to the map
*
* Uses @p mapname instead of @p tagname for the entries in map @p m
* to allow renaming from XML to map keys (in particular for
* TranslatedString). Also transforms xml:lang attributes to suitable
* key-decorations on @p mapname.
*/
static inline void
fillMap( QVariantMap& m, const QDomNodeList& children, const QString& tagname, const QString& mapname )
{
for ( int i = 0; i < children.count(); ++i )
{
if ( !children.at( i ).isElement() )
{
continue;
}
QDomElement e = children.at( i ).toElement();
if ( e.tagName() != tagname )
{
continue;
}
m[ mapname + getLanguage( e ) ] = e.text();
}
}
/** @brief gets the <name> and <description> elements
*
* Builds up a map of the <name> elements (which may have a *lang*
* attribute to indicate translations and paragraphs of the
* <description> element (also with lang). Uses the <summary>
* elements to supplement the description if no description
* is available for a given language.
*
* Returns a map with keys suitable for use by TranslatedString.
*/
static inline QVariantMap
getNameAndSummary( const QDomNode& n )
{
QVariantMap m;
const QDomNodeList children = n.childNodes();
fillMap( m, children, "name", "name" );
fillMap( m, children, "summary", "description" );
const QDomElement description = n.firstChildElement( "description" );
if ( !description.isNull() )
{
fillMap( m, description.childNodes(), "p", "description" );
}
return m;
}
#endif
PackageItem
PackageItem::fromAppData( const QVariantMap& item_map )
{
#ifdef HAVE_XML
QString fileName = CalamaresUtils::getString( item_map, "appdata" );
if ( fileName.isEmpty() )
{
cWarning() << "Can't load AppData without a suitable key.";
return PackageItem();
}
cDebug() << "Loading AppData XML from" << fileName;
QDomDocument doc = loadAppData( fileName );
if ( doc.isNull() )
{
return PackageItem();
}
QDomElement componentNode = doc.documentElement();
if ( !componentNode.isNull() && componentNode.tagName() == "component" )
{
// An "id" entry in the Calamares config overrides ID in the AppData
QString id = CalamaresUtils::getString( item_map, "id" );
if ( id.isEmpty() )
{
id = getChildText( componentNode, "id" );
}
if ( id.isEmpty() )
{
return PackageItem();
}
// A "screenshot" entry in the Calamares config overrides AppData
QString screenshotPath = CalamaresUtils::getString( item_map, "screenshot" );
if ( screenshotPath.isEmpty() )
{
screenshotPath = getScreenshotPath( componentNode );
}
QVariantMap map = getNameAndSummary( componentNode );
map.insert( "id", id );
map.insert( "screenshot", screenshotPath );
return PackageItem( map );
}
return PackageItem();
#else
cWarning() << "Loading AppData XML is not supported.";
return PackageItem();
#endif
}
PackageListModel::PackageListModel( QObject* parent )
: QAbstractListModel( parent )
{
}
PackageListModel::PackageListModel( PackageList&& items, QObject* parent )
: QAbstractListModel( parent )
, m_packages( std::move( items ) )
{
}
PackageListModel::~PackageListModel() {}
void
PackageListModel::addPackage( PackageItem&& p )
{
// Only add valid packages
if ( p.isValid() )
{
int c = m_packages.count();
beginInsertRows( QModelIndex(), c, c );
m_packages.append( p );
endInsertRows();
}
}
int
PackageListModel::rowCount( const QModelIndex& index ) const
{
// For lists, valid indexes have zero children; only the root index has them
return index.isValid() ? 0 : m_packages.count();
}
QVariant
PackageListModel::data( const QModelIndex& index, int role ) const
{
if ( !index.isValid() )
{
return QVariant();
}
int row = index.row();
if ( row >= m_packages.count() || row < 0 )
{
return QVariant();
}
if ( role == Qt::DisplayRole /* Also PackageNameRole */ )
{
return m_packages[ row ].name.get();
}
else if ( role == DescriptionRole )
{
return m_packages[ row ].description.get();
}
else if ( role == ScreenshotRole )
{
return m_packages[ row ].screenshot;
}
else if ( role == IdRole )
{
return m_packages[ row ].id;
}
return QVariant();
}

View File

@ -0,0 +1,128 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019, Adriaan de Groot <groot@kde.org>
*
* Calamares 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 3 of the License, or
* (at your option) any later version.
*
* Calamares 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 Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PACKAGEMODEL_H
#define PACKAGEMODEL_H
#include "locale/TranslatableConfiguration.h"
#include "utils/NamedEnum.h"
#include <QAbstractListModel>
#include <QObject>
#include <QPixmap>
#include <QVector>
enum class PackageChooserMode
{
Optional, // zero or one
Required, // exactly one
OptionalMultiple, // zero or more
RequiredMultiple // one or more
};
const NamedEnumTable< PackageChooserMode >& roleNames();
struct PackageItem
{
QString id;
// FIXME: unused
QString package;
CalamaresUtils::Locale::TranslatedString name;
CalamaresUtils::Locale::TranslatedString description;
QPixmap screenshot;
/// @brief Create blank PackageItem
PackageItem();
/** @brief Creates a PackageItem from given strings
*
* This constructor sets all the text members,
* but leaves the screenshot blank. Set that separately.
*/
PackageItem( const QString& id, const QString& package, const QString& name, const QString& description );
/** @brief Creates a PackageItem from given strings.
*
* Set all the text members and load the screenshot from the given
* @p screenshotPath, which may be a QRC path (:/path/in/qrc) or
* a filesystem path, whatever QPixmap understands.
*/
PackageItem( const QString& id,
const QString& package,
const QString& name,
const QString& description,
const QString& screenshotPath );
/** @brief Creates a PackageItem from a QVariantMap
*
* This is intended for use when loading PackageItems from a
* configuration map. It will look up the various keys in the map
* and handle translation strings as well.
*/
PackageItem( const QVariantMap& map );
/** @brief Is this item valid?
*
* A valid item has an untranslated name available.
*/
bool isValid() const { return !name.isEmpty(); }
/** @brief Loads an AppData XML file and returns a PackageItem
*
* The @p map must have a key *appdata*. That is used as the
* primary source of information, but keys *id* and *screenshotPath*
* may be used to override parts of the AppData -- so that the
* ID is under the control of Calamares, and the screenshot can be
* forced to a local path available on the installation medium.
*
* Requires XML support in libcalamares, if not present will
* return invalid PackageItems.
*/
static PackageItem fromAppData( const QVariantMap& map );
};
using PackageList = QVector< PackageItem >;
class PackageListModel : public QAbstractListModel
{
public:
PackageListModel( PackageList&& items, QObject* parent );
PackageListModel( QObject* parent );
virtual ~PackageListModel() override;
/** @brief Add a package @p to the model
*
* Only valid packages are added -- that is, they must have a name.
*/
void addPackage( PackageItem&& p );
int rowCount( const QModelIndex& index ) const override;
QVariant data( const QModelIndex& index, int role ) const override;
enum Roles : int
{
NameRole = Qt::DisplayRole,
DescriptionRole = Qt::UserRole,
ScreenshotRole,
IdRole
};
private:
PackageList m_packages;
};
#endif

View File

@ -0,0 +1,77 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019, Adriaan de Groot <groot@kde.org>
*
* Calamares 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 3 of the License, or
* (at your option) any later version.
*
* Calamares 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 Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Tests.h"
#include "PackageModel.h"
#include "utils/Logger.h"
#include <QtTest/QtTest>
QTEST_MAIN( PackageChooserTests )
PackageChooserTests::PackageChooserTests() {}
PackageChooserTests::~PackageChooserTests() {}
void
PackageChooserTests::initTestCase()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
}
void
PackageChooserTests::testBogus()
{
QVERIFY( true );
}
void
PackageChooserTests::testAppData()
{
// Path from the build-dir
QString appdataName( "../io.calamares.calamares.appdata.xml" );
QVERIFY( QFile::exists( appdataName ) );
QVariantMap m;
m.insert( "appdata", appdataName );
PackageItem p1 = PackageItem::fromAppData( m );
#ifdef HAVE_XML
QVERIFY( p1.isValid() );
QCOMPARE( p1.id, "io.calamares.calamares.desktop" );
QCOMPARE( p1.name.get(), "Calamares" );
// The <description> entry has precedence
QCOMPARE( p1.description.get(), "Calamares is an installer program for Linux distributions." );
// .. but en_GB doesn't have an entry in description, so uses <summary>
QCOMPARE( p1.description.get( QLocale( "en_GB" ) ), "Calamares Linux Installer" );
QCOMPARE( p1.description.get( QLocale( "nl" ) ), "Calamares is een installatieprogramma voor Linux distributies." );
QVERIFY( p1.screenshot.isNull() );
m.insert( "id", "calamares" );
m.insert( "screenshot", ":/images/calamares.png" );
PackageItem p2= PackageItem::fromAppData( m );
QVERIFY( p2.isValid() );
QCOMPARE( p2.id, "calamares" );
QCOMPARE( p2.description.get( QLocale( "nl" ) ), "Calamares is een installatieprogramma voor Linux distributies." );
QVERIFY( !p2.screenshot.isNull() );
#else
QVERIFY( !p1.isValid() );
#endif
}

View File

@ -0,0 +1,37 @@
/* === This file is part of Calamares - <https://github.com/calamares> ===
*
* Copyright 2019, Adriaan de Groot <groot@kde.org>
*
* Calamares 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 3 of the License, or
* (at your option) any later version.
*
* Calamares 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 Calamares. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PACKAGECHOOSERTESTS_H
#define PACKAGECHOOSERTESTS_H
#include <QObject>
class PackageChooserTests : public QObject
{
Q_OBJECT
public:
PackageChooserTests();
~PackageChooserTests() override;
private Q_SLOTS:
void initTestCase();
void testBogus();
void testAppData();
};
#endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,84 @@
# Configuration for the low-density software chooser
---
# The packagechooser writes a GlobalStorage value for the choice that
# has been made. The key is *packagechooser_<id>*. If *id* is set here,
# it is substituted into the key name. If it is not set, the module's
# instance name is used; see the *instances* section of `settings.conf`.
# If there is just one packagechooser module, and no *id* is set,
# resulting GS key is probably *packagechooser_packagechooser*.
#
# The GS value is a comma-separated list of the IDs of the selected
# packages, or an empty string if none is selected.
#
# id: ""
# Software selection mode, to set whether the software packages
# can be chosen singly, or multiply.
#
# Possible modes are "optional", "required" (for zero or one)
# or "optionalmultiple", "requiredmultiple" (for zero-or-more
# or one-or-more).
mode: required
# Items to display in the chooser. In general, this should be a
# pretty short list to avoid overwhelming the UI. This is a list
# of objects, and the items are displayed in list order.
#
# Either provide the data for an item in the list (using the keys
# below), or use existing AppData XML files as a source for the data.
#
# For data provided by the list: the item has an id, which is used in
# setting the value of *packagechooser_<module-id>*). The following fields
# are mandatory:
#
# - *id* ID for the product. The ID "" is special, and is used for
# "no package selected". Only include this if the mode allows
# selecting none.
# - *package* Package name for the product. While mandatory, this is
# not actually used anywhere.
# - *name* Human-readable name of the product. To provide translations,
# add a *[lang]* decoration as part of the key name, e.g. `name[nl]`
# for Dutch. The list of usable languages can be found in
# `CMakeLists.txt` or as part of the debug output of Calamares.
# - *description* Human-readable description. These can be translated
# as well.
# - *screenshot* Path to a single screenshot of the product. May be
# a filesystem path or a QRC path (e.g. ":/images/no-selection.png").
#
# Use the empty string "" as ID / key for the "no selection" item if
# you want to customize the display of that item as well.
#
# For data provided by AppData XML: the item has an *appdata*
# key which points to an AppData XML file in the local filesystem.
# This file is parsed to provide the id (from AppData id), name
# (from AppData name), description (from AppData description paragraphs
# or the summary entries), and a screenshot (the defautl screenshot
# from AppData). No package is set (but that is unused anyway).
#
# AppData may contain IDs that are not useful inside Calamares,
# and the screenshot URL may be remote -- a remote URL will not
# be loaded and the screenshot will be missing. An item with *appdata*
# **may** specify an ID or screenshot path, as above. This will override
# the settings from AppData.
items:
- id: ""
package: ""
name: "No Desktop"
name[nl]: "Geen desktop"
description: "Please pick a desktop environment from the list. If you don't want to install a desktop, that's fine, your system will start up in text-only mode and you can install a desktop environment later."
description[nl]: "Kies eventueel een desktop-omgeving uit deze lijst. Als u geen desktop-omgeving wenst te gebruiken, kies er dan geen. In dat geval start het systeem straks op in tekst-modus en kunt u later alsnog een desktop-omgeving installeren."
screenshot: ":/images/no-selection.png"
- id: kde
package: kde
name: Plasma Desktop
description: "KDE Plasma Desktop, simple by default, a clean work area for real-world usage which intends to stay out of your way. Plasma is powerful when needed, enabling the user to create the workflow that makes them more effective to complete their tasks."
screenshot: ":/images/kde.png"
- id: gnome
package: gnome
name: GNOME
description: GNU Networked Object Modeling Environment Desktop
screenshot: ":/images/gnome.png"
- id: calamares
appdata: ../io.calamares.calamares.appdata.xml
screenshot: ":/images/calamares.png"

View File

@ -0,0 +1,8 @@
<RCC>
<qresource prefix="/">
<file>images/no-selection.png</file>
<file>images/kde.png</file>
<file>images/gnome.png</file>
<file>images/calamares.png</file>
</qresource>
</RCC>

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PackageChooserPage</class>
<widget class="QWidget" name="PackageChooserPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>500</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,2">
<item>
<widget class="QListView" name="products">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>1</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout" stretch="1,3,1">
<item>
<widget class="QLabel" name="productName">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="productScreenshot">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="productDescription">
<property name="text">
<string>TextLabel</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>