Merge branch 'master' of https://github.com/calamares/calamares into development

This commit is contained in:
Philip Müller 2019-08-07 07:16:41 +02:00
commit ac5c2a041d
14 changed files with 462 additions and 48 deletions

13
CHANGES
View File

@ -18,12 +18,25 @@ This release contains contributions from (alphabetically by first name):
number of jobs. (Thanks to Bill Auger)
- Preliminary work has been added to post the installation log to a
pastebin for bug reporting. (Thanks to Bill Auger)
- Support for translated human-readable strings in Calamares
config files has been added. This is used only in the *packagechooser*
module (see below) but will expand to those modules that need
user-visible strings from the configuration file (existing
solutions need either gettext or Qt support).
- Esperanto is now available when Qt version 5.12.2 or later is used.
## Modules ##
- *fstab* A new configuration key *efiMountOptions* has been added, to
allow setting filesystem options specifically for the EFI partition.
(Thanks to apt-ghetto)
- *packagechooser* is a new module for low-density package choices,
e.g. for selecting a default desktop environment, or adding some
proprietary drivers, or chosing browsers of office suites. It presents
**one** collection of items -- at most ten or so, because of the UI --
and the user can select zero or more of them. The behavior is
configurable, and package information can be set through the Calamares
configuration file or by reading AppData files for the packages. #426
# 3.2.11 (2019-07-06) #

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
LocaleTests::testTranslatableLanguages()
{
@ -108,12 +112,19 @@ LocaleTests::testTranslatableLanguages()
}
}
/** @brief Test strings with no translations
*/
void
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
CalamaresUtils::Locale::TranslatedString ts1( "Hello" );
QCOMPARE( ts1.count(), 1 );
QVERIFY( !ts1.isEmpty() );
QCOMPARE( ts1.get(), "Hello" );
QCOMPARE( ts1.get( QLocale( "nl" ) ), "Hello" );
@ -122,11 +133,14 @@ LocaleTests::testTranslatableConfig1()
map.insert( "description", "description (no language)" );
CalamaresUtils::Locale::TranslatedString ts2( map, "description" );
QCOMPARE( ts2.count(), 1 );
QVERIFY( !ts2.isEmpty() );
QCOMPARE( ts2.get(), "description (no language)" );
QCOMPARE( ts2.get( QLocale( "nl" ) ), "description (no language)" );
}
/** @bref Test strings with translations.
*/
void
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" );
// The +1 is because "" is always also inserted
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)" );
for ( const auto& language : someLanguages() )
{
@ -167,4 +192,10 @@ LocaleTests::testTranslatableConfig2()
CalamaresUtils::Locale::TranslatedString ts2( map, "name" );
// We skipped dutch this time
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
QString value = CalamaresUtils::getString( map, key );
if ( value.isEmpty() )
{
value = key;
}
m_strings[ QString() ] = value;
for ( auto it = map.constKeyValueBegin(); it != map.constKeyValueEnd(); ++it )

View File

@ -46,9 +46,23 @@ public:
TranslatedString( const QString& string );
/// @brief Empty string
TranslatedString()
: TranslatedString( QString() ) {}
: 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(); }
/** @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
QString get() const;

View File

@ -1,4 +1,15 @@
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
@ -13,6 +24,7 @@ calamares_add_plugin( packagechooser
page_package.ui
LINK_PRIVATE_LIBRARIES
calamaresui
${_extra_libraries}
SHARED_LIB
)
@ -23,8 +35,11 @@ if( ECM_FOUND AND BUILD_TESTING )
packagechoosertest
LINK_LIBRARIES
${CALAMARES_LIBRARIES}
calamares_viewmodule_packagechooser
Qt5::Core
Qt5::Test
Qt5::Gui
${_extra_libraries}
)
calamares_automoc( packagechoosertest)
endif()

View File

@ -232,31 +232,14 @@ PackageChooserViewStep::fillModel( const QVariantList& items )
continue;
}
QString id = CalamaresUtils::getString( item_map, "id" );
QString package = CalamaresUtils::getString( item_map, "package" );
QString name = CalamaresUtils::getString( item_map, "name" );
QString description = CalamaresUtils::getString( item_map, "description" );
QString screenshot = CalamaresUtils::getString( item_map, "screenshot" );
if ( name.isEmpty() && id.isEmpty() )
if ( item_map.contains( "appdata" ) )
{
name = tr( "No product" );
m_model->addPackage( PackageItem::fromAppData( item_map ) );
}
else if ( name.isEmpty() )
else
{
cWarning() << "PackageChooser item" << id << "has an empty name.";
continue;
m_model->addPackage( PackageItem( item_map ) );
}
if ( description.isEmpty() )
{
description = tr( "No description provided." );
}
if ( screenshot.isEmpty() )
{
screenshot = QStringLiteral( ":/images/no-selection.png" );
}
m_model->addPackage( PackageItem { id, package, name, description, screenshot } );
}
}

View File

@ -19,6 +19,13 @@
#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()
@ -40,13 +47,6 @@ roleNames()
return names;
}
PackageItem
PackageItem::fromAppStream( const QString& filename )
{
// TODO: implement this
return PackageItem {};
}
PackageItem::PackageItem() {}
PackageItem::PackageItem( const QString& a_id,
@ -73,6 +73,239 @@ PackageItem::PackageItem( const QString& a_id,
{
}
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 )
@ -90,10 +323,14 @@ PackageListModel::~PackageListModel() {}
void
PackageListModel::addPackage( PackageItem&& p )
{
int c = m_packages.count();
beginInsertRows( QModelIndex(), c, c );
m_packages.append( p );
endInsertRows();
// Only add valid packages
if ( p.isValid() )
{
int c = m_packages.count();
beginInsertRows( QModelIndex(), c, c );
m_packages.append( p );
endInsertRows();
}
}
int

View File

@ -40,11 +40,10 @@ const NamedEnumTable< PackageChooserMode >& roleNames();
struct PackageItem
{
QString id;
// TODO: may need more than one
// FIXME: unused
QString package;
CalamaresUtils::Locale::TranslatedString name;
CalamaresUtils::Locale::TranslatedString description;
// TODO: may be more than one
QPixmap screenshot;
/// @brief Create blank PackageItem
@ -56,14 +55,44 @@ struct PackageItem
*/
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 );
// TODO: implement this
PackageItem fromAppStream( const QString& filename );
/** @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 >;
@ -75,6 +104,10 @@ public:
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;

View File

@ -18,9 +18,13 @@
#include "Tests.h"
#include "PackageModel.h"
#include "utils/Logger.h"
#include <QtTest/QtTest>
QTEST_GUILESS_MAIN( PackageChooserTests )
QTEST_MAIN( PackageChooserTests )
PackageChooserTests::PackageChooserTests() {}
@ -29,6 +33,7 @@ PackageChooserTests::~PackageChooserTests() {}
void
PackageChooserTests::initTestCase()
{
Logger::setupLogLevel( Logger::LOGDEBUG );
}
void
@ -36,3 +41,37 @@ 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

@ -31,6 +31,7 @@ public:
private Q_SLOTS:
void initTestCase();
void testBogus();
void testAppData();
};
#endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -24,8 +24,11 @@ mode: required
# pretty short list to avoid overwhelming the UI. This is a list
# of objects, and the items are displayed in list order.
#
# Each item has an id, which is used in setting # the value of
# *packagechooser_<module-id>*). The following fields
# 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
@ -33,13 +36,30 @@ mode: required
# selecting none.
# - *package* Package name for the product. While mandatory, this is
# not actually used anywhere.
# - *name* Human-readable, but untranslated, name of the product.
# - *description* Human-readable, but untranslated, description.
# - *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: ""
@ -58,5 +78,7 @@ items:
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

@ -3,5 +3,6 @@
<file>images/no-selection.png</file>
<file>images/kde.png</file>
<file>images/gnome.png</file>
<file>images/calamares.png</file>
</qresource>
</RCC>