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

This commit is contained in:
Philip Müller 2019-08-20 21:08:07 +02:00
commit 07dd0e832f
16 changed files with 658 additions and 336 deletions

View File

@ -23,6 +23,9 @@ This release contains contributions from (alphabetically by first name):
## Modules ## ## Modules ##
- The *packagechooser* module can load data from the config-file,
from AppData XML files referred by the config-file, and (new) also
from AppStream caches by referring to an application's AppStream id. #1212
- The *partition* module now understands the units *KB*, *MB*, *GB* which - The *partition* module now understands the units *KB*, *MB*, *GB* which
are powers-of-ten sizes, alongside the powers-of-two sizes that it already are powers-of-ten sizes, alongside the powers-of-two sizes that it already
used. (thanks to Arnaud) used. (thanks to Arnaud)

View File

@ -25,6 +25,9 @@
# SKIP_MODULES : a space or semicolon-separated list of directory names # SKIP_MODULES : a space or semicolon-separated list of directory names
# under src/modules that should not be built. # under src/modules that should not be built.
# USE_<foo> : fills in SKIP_MODULES for modules called <foo>-<something> # USE_<foo> : fills in SKIP_MODULES for modules called <foo>-<something>
# WITH_<foo> : try to enable <foo> (these usually default to ON). For
# a list of WITH_<foo> grep CMakeCache.txt after running
# CMake once.
# BUILD_<foo> : choose additional things to build # BUILD_<foo> : choose additional things to build
# DEBUG_<foo> : special developer flags for debugging # DEBUG_<foo> : special developer flags for debugging
# #

View File

@ -29,6 +29,7 @@ namespace Locale
LabelModel::LabelModel( const QStringList& locales, QObject* parent ) LabelModel::LabelModel( const QStringList& locales, QObject* parent )
: QAbstractListModel( parent ) : QAbstractListModel( parent )
, m_localeIds( locales )
{ {
Q_ASSERT( locales.count() > 0 ); Q_ASSERT( locales.count() > 0 );
m_locales.reserve( locales.count() ); m_locales.reserve( locales.count() );
@ -132,7 +133,7 @@ LabelModel::find( const QString& countryCode ) const
LabelModel* LabelModel*
availableTranslations() availableTranslations()
{ {
static LabelModel* model = new LabelModel( QString( CALAMARES_TRANSLATION_LANGUAGES ).split( ';' ) ); static LabelModel* model = new LabelModel( QStringLiteral( CALAMARES_TRANSLATION_LANGUAGES ).split( ';' ) );
return model; return model;
} }

View File

@ -54,6 +54,9 @@ public:
*/ */
const Label& locale( int row ) const; const Label& locale( int row ) const;
/// @brief Returns all of the locale Ids (e.g. en_US) put into this model.
const QStringList& localeIds() const { return m_localeIds; }
/** @brief Searches for an item that matches @p predicate /** @brief Searches for an item that matches @p predicate
* *
* Returns the row number of the first match, or -1 if there isn't one. * Returns the row number of the first match, or -1 if there isn't one.
@ -67,6 +70,7 @@ public:
private: private:
QVector< Label > m_locales; QVector< Label > m_locales;
QStringList m_localeIds;
}; };
/** @brief Returns a model with all available translations. /** @brief Returns a model with all available translations.

View File

@ -58,8 +58,8 @@ public:
template < class impl, class ParentType > template < class impl, class ParentType >
static QObject* createInstance( QWidget* parentWidget, QObject* parent, const QVariantList& args ) static QObject* createInstance( QWidget* parentWidget, QObject* parent, const QVariantList& args )
{ {
Q_UNUSED( parentWidget ); Q_UNUSED( parentWidget )
Q_UNUSED( args ); Q_UNUSED( args )
ParentType* p = nullptr; ParentType* p = nullptr;
if ( parent ) if ( parent )
{ {

View File

@ -37,17 +37,13 @@ _ = gettext.translation("calamares-python",
def pretty_name(): def pretty_name():
return _("Mounting partitions.") return _("Mounting partitions.")
def mount_partition(root_mount_point, partition, partitions):
def mount_partitions(root_mount_point, partitions):
""" """
Pass back mount point and filesystem for each partition. Do a single mount of @p partition inside @p root_mount_point.
:param root_mount_point: The @p partitions are used to handle btrfs special-cases:
:param partitions: then subvolumes are created for root and home.
""" """
for partition in partitions:
if "mountPoint" not in partition or not partition["mountPoint"]:
continue
# Create mount point with `+` rather than `os.path.join()` because # Create mount point with `+` rather than `os.path.join()` because
# `partition["mountPoint"]` starts with a '/'. # `partition["mountPoint"]` starts with a '/'.
raw_mount_point = partition["mountPoint"] raw_mount_point = partition["mountPoint"]
@ -139,9 +135,8 @@ def mount_partitions(root_mount_point, partitions):
def run(): def run():
""" """
Define mountpoints. Mount all the partitions from GlobalStorage and from the job configuration.
Partitions are mounted in-lexical-order of their mountPoint.
:return:
""" """
partitions = libcalamares.globalstorage.value("partitions") partitions = libcalamares.globalstorage.value("partitions")
@ -158,17 +153,19 @@ def run():
if not extra_mounts and not extra_mounts_efi: if not extra_mounts and not extra_mounts_efi:
libcalamares.utils.warning("No extra mounts defined. Does mount.conf exist?") libcalamares.utils.warning("No extra mounts defined. Does mount.conf exist?")
# Sort by mount points to ensure / is mounted before the rest
partitions.sort(key=lambda x: x["mountPoint"])
mount_partitions(root_mount_point, partitions)
mount_partitions(root_mount_point, extra_mounts)
all_extra_mounts = extra_mounts
if libcalamares.globalstorage.value("firmwareType") == "efi": if libcalamares.globalstorage.value("firmwareType") == "efi":
mount_partitions(root_mount_point, extra_mounts_efi) extra_mounts.extend(extra_mounts_efi)
all_extra_mounts.extend(extra_mounts_efi)
# Add extra mounts to the partitions list and sort by mount points.
# This way, we ensure / is mounted before the rest, and every mount point
# is created on the right partition (e.g. if a partition is to be mounted
# under /tmp, we make sure /tmp is mounted before the partition)
mountable_partitions = [ p for p in partitions + extra_mounts if "mountPoint" in p and p["mountPoint"] ]
mountable_partitions.sort(key=lambda x: x["mountPoint"])
for partition in mountable_partitions:
mount_partition(root_mount_point, partition, partitions)
libcalamares.globalstorage.insert("rootMountPoint", root_mount_point) libcalamares.globalstorage.insert("rootMountPoint", root_mount_point)
# Remember the extra mounts for the unpackfs module # Remember the extra mounts for the unpackfs module
libcalamares.globalstorage.insert("extraMounts", all_extra_mounts) libcalamares.globalstorage.insert("extraMounts", extra_mounts)

View File

@ -1,15 +1,39 @@
find_package( Qt5 COMPONENTS Core Gui Widgets REQUIRED ) find_package( Qt5 COMPONENTS Core Gui Widgets REQUIRED )
set( _extra_libraries "" ) set( _extra_libraries "" )
set( _extra_src "" )
### OPTIONAL AppData XML support in PackageModel ### OPTIONAL AppData XML support in PackageModel
# #
# #
find_package(Qt5 COMPONENTS Xml) option( WITH_APPDATA "Support appdata: items in PackageChooser (requires QtXml)" ON )
if ( Qt5Xml_FOUND ) if ( WITH_APPDATA )
find_package(Qt5 COMPONENTS Xml)
if ( Qt5Xml_FOUND )
add_definitions( -DHAVE_XML ) add_definitions( -DHAVE_XML )
list( APPEND _extra_libraries Qt5::Xml ) list( APPEND _extra_libraries Qt5::Xml )
list( APPEND _extra_src ItemAppData.cpp )
endif()
endif() endif()
### OPTIONAL AppStream support in PackageModel
#
#
option( WITH_APPSTREAM "Support appstream: items in PackageChooser (requires libappstream-qt)" ON )
if ( WITH_APPSTREAM )
find_package(AppStreamQt)
set_package_properties(
AppStreamQt PROPERTIES
DESCRIPTION "Support for AppStream (cache) data"
URL "https://github.com/ximion/appstream"
PURPOSE "AppStream provides package data"
TYPE OPTIONAL
)
if ( AppStreamQt_FOUND )
add_definitions( -DHAVE_APPSTREAM )
list( APPEND _extra_libraries AppStreamQt )
list( APPEND _extra_src ItemAppStream.cpp )
endif()
endif()
calamares_add_plugin( packagechooser calamares_add_plugin( packagechooser
TYPE viewmodule TYPE viewmodule
@ -18,6 +42,7 @@ calamares_add_plugin( packagechooser
PackageChooserPage.cpp PackageChooserPage.cpp
PackageChooserViewStep.cpp PackageChooserViewStep.cpp
PackageModel.cpp PackageModel.cpp
${_extra_src}
RESOURCES RESOURCES
packagechooser.qrc packagechooser.qrc
UI UI

View File

@ -0,0 +1,234 @@
/* === 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/>.
*/
/** @brief Loading items from AppData XML files.
*
* Only used if QtXML is found, implements PackageItem::fromAppData().
*/
#include "PackageModel.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include <QDomDocument>
#include <QDomNodeList>
#include <QFile>
/** @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;
}
PackageItem
fromAppData( const QVariantMap& item_map )
{
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();
}

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 ITEMAPPDATA_H
#define ITEMAPPDATA_H
#include "PackageModel.h"
/** @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.
*/
PackageItem fromAppData( const QVariantMap& map );
#endif

View File

@ -0,0 +1,159 @@
/* === 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/>.
*/
/** @brief Loading items from AppData XML files.
*
* Only used if QtXML is found, implements PackageItem::fromAppData().
*/
#include "PackageModel.h"
#include "locale/LabelModel.h"
#include "utils/Logger.h"
#include "utils/Variant.h"
#include <AppStreamQt/image.h>
#include <AppStreamQt/pool.h>
#include <AppStreamQt/screenshot.h>
/// @brief Return number of pixels in a size, for < ordering purposes
static inline quint64
sizeOrder( const QSize& size )
{
return size.width() * size.height();
}
/// @brief Sets a screenshot in @p map from @p screenshot, if a usable one is found
static void
setScreenshot( QVariantMap& map, const AppStream::Screenshot& screenshot )
{
if ( screenshot.images().count() < 1 )
{
return;
}
// Pick the smallest
QUrl url;
quint64 size = sizeOrder( screenshot.images().first().size() );
for ( const auto& img : screenshot.images() )
{
if ( sizeOrder( img.size() ) <= size )
{
url = img.url();
}
}
if ( url.isValid() )
{
map.insert( "screenshot", url.toString() );
}
}
/// @brief Interpret an AppStream Component
static PackageItem
fromComponent( AppStream::Component& component )
{
QVariantMap map;
map.insert( "id", component.id() );
map.insert( "package", component.packageNames().join( "," ) );
// Assume that the pool has loaded "ALL" locales, but it might be set
// to any of them; get the en_US locale as "untranslated" and then
// loop over Calamares locales (since there is no way to query for
// available locales in the Component) to see if there's anything else.
component.setActiveLocale( QStringLiteral( "en_US" ) );
QString en_name = component.name();
QString en_description = component.description();
map.insert( "name", en_name );
map.insert( "description", en_description );
for ( const QString& locale : CalamaresUtils::Locale::availableTranslations()->localeIds() )
{
component.setActiveLocale( locale );
QString name = component.name();
if ( name != en_name )
{
map.insert( QStringLiteral( "name[%1]" ).arg( locale ), name );
}
QString description = component.description();
if ( description != en_description )
{
map.insert( QStringLiteral( "description[%1]" ).arg( locale ), description );
}
}
auto screenshots = component.screenshots();
if ( screenshots.count() > 0 )
{
bool done = false;
for ( const auto& s : screenshots )
{
if ( s.isDefault() )
{
setScreenshot( map, s );
done = true;
break;
}
}
if ( !done )
{
setScreenshot( map, screenshots.first() );
}
}
return PackageItem( map );
}
PackageItem
fromAppStream( AppStream::Pool& pool, const QVariantMap& item_map )
{
QString appstreamId = CalamaresUtils::getString( item_map, "appstream" );
if ( appstreamId.isEmpty() )
{
cWarning() << "Can't load AppStream without a suitable appstreamId.";
return PackageItem();
}
cDebug() << "Loading AppStream data for" << appstreamId;
auto itemList = pool.componentsById( appstreamId );
if ( itemList.count() < 1 )
{
cWarning() << "No AppStream data for" << appstreamId;
return PackageItem();
}
if ( itemList.count() > 1 )
{
cDebug() << "Multiple AppStream data for" << appstreamId << "using first.";
}
auto r = fromComponent( itemList.first() );
if ( r.isValid() )
{
QString id = CalamaresUtils::getString( item_map, "id" );
QString screenshotPath = CalamaresUtils::getString( item_map, "screenshot" );
if ( !id.isEmpty() )
{
r.id = id;
}
if ( !screenshotPath.isEmpty() )
{
r.screenshot = screenshotPath;
}
}
return r;
}

View File

@ -0,0 +1,43 @@
/* === 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 ITEMAPPSTREAM_H
#define ITEMAPPSTREAM_H
#include "PackageModel.h"
namespace AppStream
{
class Pool;
}
/** @brief Loads an item from AppStream data.
*
* The @p map must have a key *appstream*. That is used as the
* primary source of information from the AppStream cache, but
* keys *id* and *screenshotPath* may be used to override parts
* of the AppStream data -- 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 AppStreamQt, if not present will return invalid
* PackageItems.
*/
PackageItem fromAppStream( AppStream::Pool& pool, const QVariantMap& map );
#endif

View File

@ -18,6 +18,14 @@
#include "PackageChooserViewStep.h" #include "PackageChooserViewStep.h"
#ifdef HAVE_XML
#include "ItemAppData.h"
#endif
#ifdef HAVE_APPSTREAM
#include "ItemAppStream.h"
#include <AppStreamQt/pool.h>
#include <memory>
#endif
#include "PackageChooserPage.h" #include "PackageChooserPage.h"
#include "PackageModel.h" #include "PackageModel.h"
@ -203,6 +211,11 @@ PackageChooserViewStep::fillModel( const QVariantList& items )
return; return;
} }
#ifdef HAVE_APPSTREAM
std::unique_ptr< AppStream::Pool > pool;
bool poolOk = false;
#endif
cDebug() << "Loading PackageChooser model items from config"; cDebug() << "Loading PackageChooser model items from config";
int item_index = 0; int item_index = 0;
for ( const auto& item_it : items ) for ( const auto& item_it : items )
@ -217,7 +230,28 @@ PackageChooserViewStep::fillModel( const QVariantList& items )
if ( item_map.contains( "appdata" ) ) if ( item_map.contains( "appdata" ) )
{ {
m_model->addPackage( PackageItem::fromAppData( item_map ) ); #ifdef HAVE_XML
m_model->addPackage( fromAppData( item_map ) );
#else
cWarning() << "Loading AppData XML is not supported.";
#endif
}
else if ( item_map.contains( "appstream" ) )
{
#ifdef HAVE_APPSTREAM
if ( !pool )
{
pool = std::make_unique< AppStream::Pool >();
pool->setLocale( QStringLiteral( "ALL" ) );
poolOk = pool->load();
}
if ( pool && poolOk )
{
m_model->addPackage( fromAppStream( *pool, item_map ) );
}
#else
cWarning() << "Loading AppStream data is not supported.";
#endif
} }
else else
{ {

View File

@ -21,12 +21,6 @@
#include "utils/Logger.h" #include "utils/Logger.h"
#include "utils/Variant.h" #include "utils/Variant.h"
#ifdef HAVE_XML
#include <QDomDocument>
#include <QDomNodeList>
#include <QFile>
#endif
const NamedEnumTable< PackageChooserMode >& const NamedEnumTable< PackageChooserMode >&
roleNames() roleNames()
{ {
@ -94,219 +88,6 @@ PackageItem::PackageItem::PackageItem( const QVariantMap& item_map )
} }
} }
#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 ) PackageListModel::PackageListModel( QObject* parent )
: QAbstractListModel( parent ) : QAbstractListModel( parent )
{ {

View File

@ -80,19 +80,6 @@ struct PackageItem
* A valid item has an untranslated name available. * A valid item has an untranslated name available.
*/ */
bool isValid() const { return !name.isEmpty(); } 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 >; using PackageList = QVector< PackageItem >;

View File

@ -18,6 +18,12 @@
#include "Tests.h" #include "Tests.h"
#ifdef HAVE_XML
#include "ItemAppData.h"
#endif
#ifdef HAVE_APPSTREAM
#include "ItemAppStream.h"
#endif
#include "PackageModel.h" #include "PackageModel.h"
#include "utils/Logger.h" #include "utils/Logger.h"
@ -62,8 +68,8 @@ PackageChooserTests::testAppData()
QVariantMap m; QVariantMap m;
m.insert( "appdata", appdataName ); m.insert( "appdata", appdataName );
PackageItem p1 = PackageItem::fromAppData( m );
#ifdef HAVE_XML #ifdef HAVE_XML
PackageItem p1 = fromAppData( m );
QVERIFY( p1.isValid() ); QVERIFY( p1.isValid() );
QCOMPARE( p1.id, "io.calamares.calamares.desktop" ); QCOMPARE( p1.id, "io.calamares.calamares.desktop" );
QCOMPARE( p1.name.get(), "Calamares" ); QCOMPARE( p1.name.get(), "Calamares" );
@ -76,12 +82,10 @@ PackageChooserTests::testAppData()
m.insert( "id", "calamares" ); m.insert( "id", "calamares" );
m.insert( "screenshot", ":/images/calamares.png" ); m.insert( "screenshot", ":/images/calamares.png" );
PackageItem p2 = PackageItem::fromAppData( m ); PackageItem p2 = fromAppData( m );
QVERIFY( p2.isValid() ); QVERIFY( p2.isValid() );
QCOMPARE( p2.id, "calamares" ); QCOMPARE( p2.id, "calamares" );
QCOMPARE( p2.description.get( QLocale( "nl" ) ), "Calamares is een installatieprogramma voor Linux distributies." ); QCOMPARE( p2.description.get( QLocale( "nl" ) ), "Calamares is een installatieprogramma voor Linux distributies." );
QVERIFY( !p2.screenshot.isNull() ); QVERIFY( !p2.screenshot.isNull() );
#else
QVERIFY( !p1.isValid() );
#endif #endif
} }

View File

@ -25,7 +25,8 @@ mode: required
# of objects, and the items are displayed in list order. # of objects, and the items are displayed in list order.
# #
# Either provide the data for an item in the list (using the keys # 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. # below), or use existing AppData XML files, or use AppStream cache
# as a source for the data.
# #
# For data provided by the list: the item has an id, which is used in # 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 # setting the value of *packagechooser_<module-id>*). The following fields
@ -60,6 +61,14 @@ mode: required
# be loaded and the screenshot will be missing. An item with *appdata* # be loaded and the screenshot will be missing. An item with *appdata*
# **may** specify an ID or screenshot path, as above. This will override # **may** specify an ID or screenshot path, as above. This will override
# the settings from AppData. # the settings from AppData.
#
# For data provided by AppStream cache: the item has an *appstream*
# key which matches the AppStream identifier in the cache (e.g.
# *org.kde.kwrite.desktop*). Data is retrieved from the AppStream
# cache for that ID. The package name is set from the AppStream data.
#
# An item for AppStream may also contain an *id* and a *screenshot*
# key which will override the data from AppStream.
items: items:
- id: "" - id: ""
package: "" package: ""
@ -81,4 +90,5 @@ items:
- id: calamares - id: calamares
appdata: ../io.calamares.calamares.appdata.xml appdata: ../io.calamares.calamares.appdata.xml
screenshot: ":/images/calamares.png" screenshot: ":/images/calamares.png"
- id: kate
appstream: org.kde.kwrite.desktop